test: a failing e2e test leaves no fixture switch or send screen behind #455

Merged
clawbot merged 1 commits from issue-318-e2e-hygiene into next 2026-10-05 10:09:07 +02:00
3 changed files with 423 additions and 342 deletions
+12
View File
@@ -45,6 +45,18 @@ but the review is broader than any of them.
# Completed Steps # Completed Steps
- 2026-10-05: A Chrome end-to-end test that fails no longer takes later tests
down with it ([#318](https://git.eeqj.de/sneak/AutistMask/issues/318)). Each
test that turns a fixture switch on for itself alone (a held or failing gas
estimate, a seeded native transfer or receipt, a token's lying `decimals()` or
markup symbol) turns it off again in a `finally`, and the two tests that drive
the popup's own send end on the address screen whether they pass or not,
reopening the popup to leave a wait for a receipt. The lying-`decimals()` test
checks that nothing was broadcast as soon as the send ends, before it waits
for the failure screen, so a broadcast fails it in seconds rather than after a
60-second wait. The fixture's `decimals()` override tells 0 from no override,
so a token with no decimal places can be fixtured.
- 2026-10-05: Chrome draws the popup in its monospace font - 2026-10-05: Chrome draws the popup in its monospace font
([#418](https://git.eeqj.de/sneak/AutistMask/issues/418)), as Firefox does. ([#418](https://git.eeqj.de/sneak/AutistMask/issues/418)), as Firefox does.
Chrome adds a stylesheet of its own to extension pages that sets the font on Chrome adds a stylesheet of its own to extension pages that sets the font on
+7 -5
View File
@@ -256,14 +256,15 @@ const SELECTOR_DECIMALS = "0x313ce567";
// Blockscout, which is exactly the disagreement the wallet must refuse to // Blockscout, which is exactly the disagreement the wallet must refuse to
// sign over. It is read at request time, so a test flips it on the options // sign over. It is read at request time, so a test flips it on the options
// object the route was registered with — after the confirmation screen has // object the route was registered with — after the confirmation screen has
// been built — without re-registering anything. // been built — without re-registering anything. Only null or undefined means
// no override: 0 is a token with no decimal places, and is answered as one.
function ethCallResult(req, opts) { function ethCallResult(req, opts) {
const call = Array.isArray(req.params) ? req.params[0] : null; const call = Array.isArray(req.params) ? req.params[0] : null;
if (!call || typeof call !== "object") return ZERO_WORD; if (!call || typeof call !== "object") return ZERO_WORD;
const data = String(call.data || call.input || "").toLowerCase(); const data = String(call.data || call.input || "").toLowerCase();
const to = String(call.to || "").toLowerCase(); const to = String(call.to || "").toLowerCase();
if (data.startsWith(SELECTOR_DECIMALS) && to === STUB_TOKEN.address) { if (data.startsWith(SELECTOR_DECIMALS) && to === STUB_TOKEN.address) {
return word(opts.tokenDecimalsOverride || STUB_TOKEN.decimals); return word(opts.tokenDecimalsOverride ?? STUB_TOKEN.decimals);
} }
return ZERO_WORD; return ZERO_WORD;
} }
@@ -597,9 +598,10 @@ function traceEnabled(raw) {
* eth_estimateGas until this is cleared again. * eth_estimateGas until this is cleared again.
* @param {string[]} [opts.broadcastTransactions] every raw signed * @param {string[]} [opts.broadcastTransactions] every raw signed
* transaction handed to eth_sendRawTransaction, appended in order. * transaction handed to eth_sendRawTransaction, appended in order.
* @param {string} [opts.tokenDecimalsOverride] what decimals() answers for * @param {number|string|null} [opts.tokenDecimalsOverride] the scale
* the stub token, in place of the value Blockscout reports for it. This is * decimals() answers for the stub token, in place of the value Blockscout
* the token that lies about its scale; read at request time. * reports for it; null for none, while 0 is a scale like any other. This
* is the token that lies about its scale; read at request time.
* @param {string} [opts.tokenSymbolOverride] what the explorer reports as * @param {string} [opts.tokenSymbolOverride] what the explorer reports as
* the stub token's symbol, in place of "E2E". This is the token whose * the stub token's symbol, in place of "E2E". This is the token whose
* symbol is markup; read at request time. * symbol is markup; read at request time.
+100 -33
View File
@@ -366,6 +366,7 @@ test("the native ETH transaction detail still renders (#151)", async (env) => {
// the assertions below are as much about that row staying hidden as // the assertions below are as much about that row staying hidden as
// about the screen coming up. // about the screen coming up.
env.routeOpts.seedNativeTransfer = true; env.routeOpts.seedNativeTransfer = true;
try {
await env.page.reload(); await env.page.reload();
await openAddressDetail(env.page); await openAddressDetail(env.page);
@@ -382,7 +383,9 @@ test("the native ETH transaction detail still renders (#151)", async (env) => {
"the native transaction detail shows the wrong hash: " + hash, "the native transaction detail shows the wrong hash: " + hash,
); );
const type = (await env.page.locator("#tx-detail-type").innerText()).trim(); const type = (
await env.page.locator("#tx-detail-type").innerText()
).trim();
assert( assert(
type === "Native ETH Transfer", type === "Native ETH Transfer",
"the native transaction was classified " + JSON.stringify(type), "the native transaction was classified " + JSON.stringify(type),
@@ -407,13 +410,15 @@ test("the native ETH transaction detail still renders (#151)", async (env) => {
assert( assert(
!(await env.page.isVisible("#tx-detail-token-contract-section")), !(await env.page.isVisible("#tx-detail-token-contract-section")),
"the token contract row is showing on a transfer that has no token " + "the token contract row is showing on a transfer that has no " +
"contract", "token contract",
); );
} finally {
// Back to one seeded transaction for everything after this: the tests // Back to one seeded transaction for everything after this: the
// below were written against a list holding the token transfer alone. // tests below were written against a list holding the token transfer
// alone.
env.routeOpts.seedNativeTransfer = false; env.routeOpts.seedNativeTransfer = false;
}
}); });
test("tap-to-copy on the transaction detail screen copies the address (#151)", async (env) => { test("tap-to-copy on the transaction detail screen copies the address (#151)", async (env) => {
@@ -1785,6 +1790,7 @@ test("ConfirmTx blocks sending while the fee estimate is pending (#238)", async
expectToken: true, expectToken: true,
}); });
// Released by the next test, which watches the estimate land.
env.routeOpts.holdGasEstimate = true; env.routeOpts.holdGasEstimate = true;
await goToConfirm(env.page, { await goToConfirm(env.page, {
token: "ETH", token: "ETH",
@@ -1947,6 +1953,7 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag
); );
env.routeOpts.failGasEstimate = true; env.routeOpts.failGasEstimate = true;
env.routeOpts.holdGasEstimate = true; env.routeOpts.holdGasEstimate = true;
try {
await goToConfirm(env.page, { await goToConfirm(env.page, {
token: "ETH", token: "ETH",
balance: FUNDED_ETH_TEXT + " ETH", balance: FUNDED_ETH_TEXT + " ETH",
@@ -1963,10 +1970,10 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag
await waitForEstimate(env.page); await waitForEstimate(env.page);
const st = await confirmState(env.page); const st = await confirmState(env.page);
env.routeOpts.failGasEstimate = false;
assert( assert(
st.fee === "Unable to estimate", st.fee === "Unable to estimate",
"the fee line does not report the failure: " + JSON.stringify(st.fee), "the fee line does not report the failure: " +
JSON.stringify(st.fee),
); );
assert( assert(
!st.reserveShown, !st.reserveShown,
@@ -1992,10 +1999,15 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag
st.height + st.height +
"px", "px",
); );
} finally {
env.routeOpts.failGasEstimate = false;
env.routeOpts.holdGasEstimate = false;
}
}); });
test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (env) => { test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (env) => {
env.routeOpts.holdGasEstimate = true; env.routeOpts.holdGasEstimate = true;
try {
await goToConfirm(env.page, { await goToConfirm(env.page, {
token: STUB_TOKEN.address, token: STUB_TOKEN.address,
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
@@ -2004,9 +2016,12 @@ test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (en
const pending = await confirmState(env.page); const pending = await confirmState(env.page);
env.erc20PendingHeight = pending.height; env.erc20PendingHeight = pending.height;
console.log("# confirm-tx ERC-20 view height: " + pending.height + "px"); console.log(
"# confirm-tx ERC-20 view height: " + pending.height + "px",
);
assert( assert(
pending.type === "ERC-20 token transfer (" + STUB_TOKEN.symbol + ")", pending.type ===
"ERC-20 token transfer (" + STUB_TOKEN.symbol + ")",
"unexpected transaction type: " + JSON.stringify(pending.type), "unexpected transaction type: " + JSON.stringify(pending.type),
); );
assert( assert(
@@ -2034,7 +2049,10 @@ test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (en
"the ERC-20 fee block does not quote the reserve: " + "the ERC-20 fee block does not quote the reserve: " +
JSON.stringify(st.reserve), JSON.stringify(st.reserve),
); );
assert(!st.sendDisabled, "Send is disabled for a funded ERC-20 transfer"); assert(
!st.sendDisabled,
"Send is disabled for a funded ERC-20 transfer",
);
assert( assert(
st.height === pending.height, st.height === pending.height,
"the ERC-20 view changed height when the estimate landed: " + "the ERC-20 view changed height when the estimate landed: " +
@@ -2043,6 +2061,9 @@ test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (en
st.height + st.height +
"px", "px",
); );
} finally {
env.routeOpts.holdGasEstimate = false;
}
}); });
test("ConfirmTx refuses an ERC-20 send that exceeds the token balance (#238)", async (env) => { test("ConfirmTx refuses an ERC-20 send that exceeds the token balance (#238)", async (env) => {
@@ -2144,6 +2165,7 @@ test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee proble
/gas estimation failed/, /gas estimation failed/,
); );
env.routeOpts.failGasEstimate = true; env.routeOpts.failGasEstimate = true;
try {
await goToConfirm(env.page, { await goToConfirm(env.page, {
token: STUB_TOKEN.address, token: STUB_TOKEN.address,
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
@@ -2152,7 +2174,6 @@ test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee proble
await waitForEstimate(env.page); await waitForEstimate(env.page);
const st = await confirmState(env.page); const st = await confirmState(env.page);
env.routeOpts.failGasEstimate = false;
assert( assert(
st.fee === "Unable to estimate", st.fee === "Unable to estimate",
"the ERC-20 fee line does not report the failure: " + "the ERC-20 fee line does not report the failure: " +
@@ -2178,6 +2199,9 @@ test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee proble
st.height + st.height +
"px", "px",
); );
} finally {
env.routeOpts.failGasEstimate = false;
}
}); });
// ------------------------- the popup's own send, end to end (#305) // ------------------------- the popup's own send, end to end (#305)
@@ -2234,6 +2258,34 @@ async function fillPasswordAndSend(page) {
await page.click("#btn-confirm-send"); await page.click("#btn-confirm-send");
} }
// Back to the address screen from wherever a send stopped, which is where a
// passing send test leaves the popup for the next one. The success and failure
// screens are left by their Done button. The wait for a receipt has no button
// and asks only every ten seconds; a reopened popup resumes it and asks at
// once, and seedReceipt has the stub node confirm the transaction.
async function backToAddressAfterSend(env) {
if (await env.page.isVisible("#view-wait-tx")) {
env.routeOpts.seedReceipt = true;
await waitForPersisted(
env.page,
"currentView",
"wait-tx",
"before closing the popup",
);
await env.page.close();
env.page = await openPopup(env.ctx, env.popupUrl);
await visible(env.page, "#view-success-tx");
env.routeOpts.seedReceipt = false;
}
for (const done of ["#btn-success-tx-done", "#btn-error-tx-done"]) {
if (await env.page.isVisible(done)) {
await env.page.click(done);
await visible(env.page, "#view-address");
}
}
await backToAddress(env.page);
}
async function goToTokenConfirm(env) { async function goToTokenConfirm(env) {
await goToConfirm(env.page, { await goToConfirm(env.page, {
token: STUB_TOKEN.address, token: STUB_TOKEN.address,
@@ -2254,13 +2306,14 @@ test("the popup's own ERC-20 send broadcasts the amount it displayed (#305)", as
// The previous test left the ETH balance at the fee-only fixture, which // The previous test left the ETH balance at the fee-only fixture, which
// blocks sending outright; this one has to be able to press Send. // blocks sending outright; this one has to be able to press Send.
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI); env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
// Confirm the transaction once it is broadcast, so the wait screen
// resolves to the success view instead of polling for the rest of the run.
env.routeOpts.seedReceipt = true;
try {
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true }); await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
const shown = await goToTokenConfirm(env); const shown = await goToTokenConfirm(env);
const before = env.routeOpts.broadcastTransactions.length; const before = env.routeOpts.broadcastTransactions.length;
// Confirm the transaction once it is broadcast, so the wait screen
// resolves to the success view instead of polling for the rest of the run.
env.routeOpts.seedReceipt = true;
await fillPasswordAndSend(env.page); await fillPasswordAndSend(env.page);
await visible(env.page, "#view-wait-tx", 60000); await visible(env.page, "#view-wait-tx", 60000);
@@ -2274,9 +2327,9 @@ test("the popup's own ERC-20 send broadcasts the amount it displayed (#305)", as
broadcast[broadcast.length - 1], broadcast[broadcast.length - 1],
); );
// The measurement, printed on every run: the amount the user read, and // The measurement, printed on every run: the amount the user read,
// what the signed bytes mean at each of the two candidate scales. Under // and what the signed bytes mean at each of the two candidate scales.
// the defect these three lines disagree. // Under the defect these three lines disagree.
console.log( console.log(
"# erc-20 send artifact: displayed=" + "# erc-20 send artifact: displayed=" +
JSON.stringify(shown) + JSON.stringify(shown) +
@@ -2332,29 +2385,38 @@ test("the popup's own ERC-20 send broadcasts the amount it displayed (#305)", as
); );
await visible(env.page, "#view-success-tx", 60000); await visible(env.page, "#view-success-tx", 60000);
await env.page.click("#btn-success-tx-done"); } finally {
await visible(env.page, "#view-address");
env.routeOpts.seedReceipt = false; env.routeOpts.seedReceipt = false;
await backToAddressAfterSend(env);
}
}); });
test("a token that lies about decimals() at signing time broadcasts nothing (#305)", async (env) => { test("a token that lies about decimals() at signing time broadcasts nothing (#305)", async (env) => {
try {
const shown = await goToTokenConfirm(env); const shown = await goToTokenConfirm(env);
// Only now, with the screen already built and its estimate already taken // Only now, with the screen already built and its estimate already
// at the explorer's scale, does the contract start answering differently. // taken at the explorer's scale, does the contract start answering
// This is the whole shape of the defect: a value read at signing time that // differently. This is the whole shape of the defect: a value read at
// nothing on screen was ever derived from. // signing time that nothing on screen was ever derived from.
env.routeOpts.tokenDecimalsOverride = LYING_DECIMALS; env.routeOpts.tokenDecimalsOverride = LYING_DECIMALS;
const before = env.routeOpts.broadcastTransactions.length; const before = env.routeOpts.broadcastTransactions.length;
await fillPasswordAndSend(env.page); await fillPasswordAndSend(env.page);
await visible(env.page, "#view-error-tx", 60000);
env.routeOpts.tokenDecimalsOverride = null;
// The count is asserted as soon as the send has ended either way, and
// the screen only after it: a transfer that got through shows as the
// wait for its receipt, never as the failure screen expected here.
await visible(
env.page,
"#view-wait-tx:not(.hidden), #view-error-tx:not(.hidden)",
60000,
);
assert( assert(
env.routeOpts.broadcastTransactions.length === before, env.routeOpts.broadcastTransactions.length === before,
"a transfer encoded against a contract that contradicts the " + "a transfer encoded against a contract that contradicts the " +
"confirmation screen still reached the RPC (#305)", "confirmation screen still reached the RPC (#305)",
); );
await visible(env.page, "#view-error-tx");
const message = ( const message = (
await env.page.locator("#error-tx-message").innerText() await env.page.locator("#error-tx-message").innerText()
@@ -2377,9 +2439,10 @@ test("a token that lies about decimals() at signing time broadcasts nothing (#30
/^[A-Z].*\.$/s.test(message), /^[A-Z].*\.$/s.test(message),
"the refusal is not a full sentence: " + JSON.stringify(message), "the refusal is not a full sentence: " + JSON.stringify(message),
); );
} finally {
await env.page.click("#btn-error-tx-done"); env.routeOpts.tokenDecimalsOverride = null;
await visible(env.page, "#view-address"); await backToAddressAfterSend(env);
}
}); });
// ------------------------------------------- hostile token symbol (#307) // ------------------------------------------- hostile token symbol (#307)
@@ -2451,6 +2514,7 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
"# stub token symbol() now returns: " + JSON.stringify(HOSTILE_SYMBOL), "# stub token symbol() now returns: " + JSON.stringify(HOSTILE_SYMBOL),
); );
try {
// Close and reopen so the refresh that runs on open fetches balances // Close and reopen so the refresh that runs on open fetches balances
// with the hostile symbol in them. // with the hostile symbol in them.
await reopenPopup(env, "address"); await reopenPopup(env, "address");
@@ -2513,12 +2577,15 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
); );
assert( assert(
!st.rowText.includes("z-index"), !st.rowText.includes("z-index"),
"the uncapped symbol reached the screen: " + JSON.stringify(st.rowText), "the uncapped symbol reached the screen: " +
JSON.stringify(st.rowText),
); );
} finally {
// Put the fixture back before the next test reads it, and let the // Put the fixture back before the next test reads it.
// stored balances be rewritten with the honest symbol.
env.routeOpts.tokenSymbolOverride = null; env.routeOpts.tokenSymbolOverride = null;
}
// Let the stored balances be rewritten with the honest symbol.
await reopenPopup(env, "main"); await reopenPopup(env, "main");
await env.page.waitForFunction( await env.page.waitForFunction(
(addr) => { (addr) => {