From 3ddea28ed8a88002dcc4587b5c65080a0fcd3b4b Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 5 Oct 2026 10:39:40 +0000 Subject: [PATCH] test: drive WaitTx's timeout and failed-lookup exits end to end (closes #315) Two Chrome end-to-end cases send ETH and leave the wait for its receipt running. In one, lookups answer "no receipt" until the 60-second deadline ends the wait with the timeout message. In the other, a new fixture switch makes every receipt lookup fail, and the sixth failure in a row ends the wait with the message naming the unreachable network. Both check the exact message and that Done returns to the address screen. Both wait in real time: Playwright's clock would apply to every later test, and backdating the stored broadcast time races the popup's own save. Model: opus-5-5 --- README.md | 10 +++++- TODO.md | 10 ++++++ tests/e2e/network.js | 12 +++++++ tests/e2e/run.js | 80 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a0cebb1..99f7b9e 100644 --- a/README.md +++ b/README.md @@ -376,6 +376,12 @@ reserve while sitting on the same side of the estimate, so swapping the two in what [#154](https://git.eeqj.de/sneak/AutistMask/issues/154) was, and it was previously correct by reading only. +It also covers both ways the wait for a sent transaction's receipt ends on the +error screen: lookups that still find no receipt 60 seconds after the broadcast, +and six lookups in a row that fail. Each must show its own message, and Done +must lead back to the address screen. Both wait in real time, about a minute +each. + It also covers the **dApp approval round trips** — the one place where the content script, the inpage provider, the background worker and the approval popup all have to work together. A local test page is served by the route @@ -640,7 +646,9 @@ suite outright if its network interception is not in effect. Measured on this repo's runner: `e2e-chrome` about 1m55s cold, almost all of it the one-time pull of the pinned ~800MB Playwright layer, and well under a minute once that layer is cached. `e2e-firefox` about 1m05s cold, and it caches its -Firefox and geckodriver downloads the same way. +Firefox and geckodriver downloads the same way. The `e2e-chrome` figures predate +the two cases that wait for a receipt to end in error, which add about two +minutes of real waiting. ### Element id guard (part of `make check`) diff --git a/TODO.md b/TODO.md index 8199b6b..6fd0b5f 100644 --- a/TODO.md +++ b/TODO.md @@ -45,6 +45,16 @@ but the review is broader than any of them. # Completed Steps +- 2026-10-05: The Chrome end-to-end suite drives both ways the wait for a + transaction's receipt ends on the error screen + ([#315](https://git.eeqj.de/sneak/AutistMask/issues/315)): lookups that still + find no receipt 60 seconds after the broadcast end it with the timeout + message, and six lookups that fail in a row end it with the message naming the + unreachable network. Done then returns to the address screen. Both cases wait + in real time, about a minute each. Playwright's clock would apply to every + later test in the run and cannot be removed, and moving the stored broadcast + time back can be undone by the save the popup makes every ten seconds. + - 2026-10-05: The Chrome end-to-end suite covers the last of the [#150](https://git.eeqj.de/sneak/AutistMask/issues/150) and [#151](https://git.eeqj.de/sneak/AutistMask/issues/151) items diff --git a/tests/e2e/network.js b/tests/e2e/network.js index 60ef50c..1b91cf4 100644 --- a/tests/e2e/network.js +++ b/tests/e2e/network.js @@ -460,6 +460,16 @@ function rpcReply(req, opts, report) { return Object.assign(envelope, { result: ethCallResult(req, opts) }); } if (req.method === "eth_getTransactionReceipt") { + // A lookup that fails, which the wait screen counts differently from + // one that answers "not mined yet" (README.md, WaitTx). + if (opts.failReceiptLookup) { + return Object.assign(envelope, { + error: { + code: -32000, + message: "e2e fixture: receipt lookup failed", + }, + }); + } const hash = Array.isArray(req.params) ? req.params[0] : null; return Object.assign(envelope, { result: opts.seedReceipt && hash ? transactionReceipt(hash) : null, @@ -619,6 +629,8 @@ function traceEnabled(raw) { * symbol is markup; read at request time. * @param {boolean} [opts.seedReceipt] answer eth_getTransactionReceipt with a * confirmed receipt instead of null, so a wait screen resolves. + * @param {boolean} [opts.failReceiptLookup] answer eth_getTransactionReceipt + * with an error, so every receipt lookup fails; read at request time. * @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) => * Promise}>} */ diff --git a/tests/e2e/run.js b/tests/e2e/run.js index f4106f1..b71997e 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -2799,6 +2799,84 @@ test("a token that lies about decimals() at signing time broadcasts nothing (#30 } }); +// ------------------------------ the wait for a receipt ending in error (#315) +// +// README.md (WaitTx) documents two ways the wait ends on the error screen: a +// lookup that answers "no receipt" 60 seconds or more after the broadcast, and +// six lookups in a row that fail. They are different facts with different +// messages, so each is driven to its own. +// +// Both wait in real time, about a minute each. The wait reads the popup's own +// clock and its own ten-second timer. Playwright's clock would move both, but +// it is installed on the whole browser context and cannot be removed, so every +// later test would run on it. Moving the stored broadcast time back instead can +// be undone by the save the popup makes every ten seconds. + +// Send ETH from the address screen and stop on the wait for its receipt. +async function sendEthToWait(env) { + await goToConfirm(env.page, { + token: "ETH", + balance: FUNDED_ETH_TEXT + " ETH", + amount: COMFORTABLE_AMOUNT, + }); + await waitForEstimate(env.page); + await fillPasswordAndSend(env.page); + await visible(env.page, "#view-wait-tx", 60000); +} + +test("a wait still without a receipt after 60 seconds ends on the timeout message (#315)", async (env) => { + try { + await sendEthToWait(env); + // Lookups run every ten seconds and answer "no receipt", so the one + // that ends the wait comes about 60 seconds after the broadcast. + await visible(env.page, "#view-error-tx", 90000); + const message = ( + await env.page.locator("#error-tx-message").innerText() + ).trim(); + assert( + message === + "Transaction was not confirmed within 60 seconds. It may still confirm later — check Etherscan.", + "the wait did not end on the timeout message: " + + JSON.stringify(message), + ); + await env.page.click("#btn-error-tx-done"); + await visible(env.page, "#view-address"); + } finally { + await backToAddressAfterSend(env); + } +}); + +test("six failed receipt lookups in a row end on the unreachable-network message (#315)", async (env) => { + // Each failed lookup is logged through log.errorf, i.e. console.error. + // Exactly six are declared: a wait that ended sooner leaves one unmatched, + // and one that went on logs a seventh, and either fails this test. + for (let i = 1; i <= 6; i++) { + env.errors.expect( + "failed receipt lookup " + i + " of 6", + /poll receipt failed/, + ); + } + env.routeOpts.failReceiptLookup = true; + try { + await sendEthToWait(env); + await visible(env.page, "#view-error-tx", 90000); + const message = ( + await env.page.locator("#error-tx-message").innerText() + ).trim(); + assert( + message === + "The network could not be reached to check this transaction — 6 lookups failed in a row. Check the RPC URL in Settings. The transaction may still have confirmed — check Etherscan.", + "the wait did not end on the unreachable-network message: " + + JSON.stringify(message), + ); + await env.page.click("#btn-error-tx-done"); + await visible(env.page, "#view-address"); + } finally { + env.routeOpts.failReceiptLookup = false; + await backToAddressAfterSend(env); + } +}); + // ------------------------------------------- hostile token symbol (#307) // // The reproduction from the issue, in the real browser against the real @@ -4402,6 +4480,8 @@ async function main() { // Whether eth_getTransactionReceipt confirms a transaction rather than // answering "not mined yet". seedReceipt: false, + // Whether eth_getTransactionReceipt fails instead of answering (#315). + failReceiptLookup: false, // Every raw signed transaction handed to eth_sendRawTransaction, in // order. The dApp transaction round trip asserts against these bytes // rather than against anything the extension reported about them.