test: drive ConfirmTx in the e2e suite, gate assertion included (closes #238)
All checks were successful
check / check (push) Successful in 38s

ConfirmTx -- the screen that decides what gets signed -- had no automated
coverage of its own behaviour. The arithmetic underneath was well tested; the
wiring was not, so a mutant making the spend gate read the displayed fee
estimate instead of the reserve would have reintroduced the #154 overspend with
the suite still green.

Nine end-to-end tests now drive it for both the native and ERC-20 paths,
covering the pending, funded, over-balance and estimate-failed states, and
asserting that the gate reads the reserve rather than the estimate. Swapping the
two makes the suite fail. The view height is asserted constant across every
state transition rather than merely printed.

Reaching the screen needs a funded balance and a gas estimate, so the route
interception gains fixtures for both. Testing the estimate-failed state means
provoking the console error the code is supposed to emit, which the harness
otherwise fails a run on; an expectation mechanism consumes exactly one matching
record, is scoped to the declaring test, and fails that test if nothing matched,
so it cannot mask an unrelated error.
This commit was merged in pull request #258.
This commit is contained in:
2026-08-12 11:35:20 +02:00
parent c6a1f97247
commit 5af89a1b63
5 changed files with 857 additions and 23 deletions

View File

@@ -53,15 +53,47 @@ function isAllowed(text) {
// after that — the route handler and the console listeners are gone with
// the context — so there is no post-teardown phase to collect, and this
// class deliberately offers no mechanism pretending to cover one.
//
// One narrow exception exists, and it is not a mute: expect(). A test that
// drives a failure path on purpose — a refused gas estimate, say — provokes
// the console.error the code is supposed to emit, and that error is the
// behaviour under test rather than an escape. Declaring it consumes exactly
// one matching record and no more, and an expectation nothing matched fails
// its test just as an unexpected error does. So it cannot be used to
// silence anything: it can only assert that a specific error happened.
class ErrorCollector {
constructor() {
this.entries = [];
this.taken = 0;
this.expectations = [];
}
// Declare a console.error this test is about to cause deliberately.
// `label` names it in the failure message if it never arrives.
expect(label, pattern) {
this.expectations.push({ label, pattern, matched: false });
}
// Declared expectations that nothing matched, clearing the list so each
// test starts with none outstanding.
unmatchedExpectations() {
const out = this.expectations
.filter((e) => !e.matched)
.map((e) => e.label);
this.expectations = [];
return out;
}
record(kind, text) {
const line = kind + ": " + String(text).split("\n")[0];
if (isAllowed(line)) return;
const expected = this.expectations.find(
(e) => !e.matched && e.pattern.test(line),
);
if (expected) {
expected.matched = true;
return;
}
this.entries.push(line);
}
@@ -290,13 +322,18 @@ async function createWallet(page) {
return phrase;
}
// Reach the address detail screen from wherever the popup restored to.
// Clicking .address-row does not open it; the [info] button does.
// Reach the address detail screen of the FIRST address of the first wallet,
// from wherever the popup restored to. Clicking .address-row does not open
// it; the [info] button does.
//
// .first() rather than a bare selector because the suite adds a second
// wallet partway through, and every later test would otherwise die in
// Playwright's strict mode rather than on an assertion.
async function openAddressDetail(page) {
const onAddress = await page.isVisible("#view-address");
if (!onAddress) {
await visible(page, "#view-main");
await page.click("#wallet-list .btn-addr-info");
await page.locator("#wallet-list .btn-addr-info").first().click();
}
await visible(page, "#view-address");
}