diff --git a/TODO.md b/TODO.md index 73b5b7b..ae94c7c 100644 --- a/TODO.md +++ b/TODO.md @@ -45,6 +45,18 @@ but the review is broader than any of them. # 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: Dead code removed and copied view helpers shared ([#168](https://git.eeqj.de/sneak/AutistMask/issues/168)). AddressDetail and AddressToken each defined their own `isoDate()` and `timeAgo()` in place of diff --git a/tests/e2e/network.js b/tests/e2e/network.js index 49d76ea..1200edb 100644 --- a/tests/e2e/network.js +++ b/tests/e2e/network.js @@ -256,14 +256,15 @@ const SELECTOR_DECIMALS = "0x313ce567"; // 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 // 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) { const call = Array.isArray(req.params) ? req.params[0] : null; if (!call || typeof call !== "object") return ZERO_WORD; const data = String(call.data || call.input || "").toLowerCase(); const to = String(call.to || "").toLowerCase(); 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; } @@ -597,9 +598,10 @@ function traceEnabled(raw) { * eth_estimateGas until this is cleared again. * @param {string[]} [opts.broadcastTransactions] every raw signed * transaction handed to eth_sendRawTransaction, appended in order. - * @param {string} [opts.tokenDecimalsOverride] what decimals() answers for - * the stub token, in place of the value Blockscout reports for it. This is - * the token that lies about its scale; read at request time. + * @param {number|string|null} [opts.tokenDecimalsOverride] the scale + * decimals() answers for the stub token, in place of the value Blockscout + * 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 * the stub token's symbol, in place of "E2E". This is the token whose * symbol is markup; read at request time. diff --git a/tests/e2e/run.js b/tests/e2e/run.js index 191c405..01c118e 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -352,54 +352,59 @@ test("the native ETH transaction detail still renders (#151)", async (env) => { // the assertions below are as much about that row staying hidden as // about the screen coming up. env.routeOpts.seedNativeTransfer = true; - await env.page.reload(); - await openAddressDetail(env.page); + try { + await env.page.reload(); + await openAddressDetail(env.page); - const row = env.page - .locator("#tx-list .tx-row") - .filter({ hasText: NATIVE_ROW_TEXT }); - await row.waitFor({ state: "visible", timeout: 30000 }); - await row.click(); - await visible(env.page, "#view-transaction"); + const row = env.page + .locator("#tx-list .tx-row") + .filter({ hasText: NATIVE_ROW_TEXT }); + await row.waitFor({ state: "visible", timeout: 30000 }); + await row.click(); + await visible(env.page, "#view-transaction"); - const hash = await env.page.locator("#tx-detail-hash").innerText(); - assert( - hash.includes(STUB_NATIVE_TX_HASH), - "the native transaction detail shows the wrong hash: " + hash, - ); + const hash = await env.page.locator("#tx-detail-hash").innerText(); + assert( + hash.includes(STUB_NATIVE_TX_HASH), + "the native transaction detail shows the wrong hash: " + hash, + ); - const type = (await env.page.locator("#tx-detail-type").innerText()).trim(); - assert( - type === "Native ETH Transfer", - "the native transaction was classified " + JSON.stringify(type), - ); + const type = ( + await env.page.locator("#tx-detail-type").innerText() + ).trim(); + assert( + type === "Native ETH Transfer", + "the native transaction was classified " + JSON.stringify(type), + ); - const value = await env.page.locator("#tx-detail-value").innerText(); - assert( - value.includes(NATIVE_DETAIL_TEXT), - "the native transaction detail shows " + - JSON.stringify(value) + - ", expected it to contain " + - NATIVE_DETAIL_TEXT, - ); + const value = await env.page.locator("#tx-detail-value").innerText(); + assert( + value.includes(NATIVE_DETAIL_TEXT), + "the native transaction detail shows " + + JSON.stringify(value) + + ", expected it to contain " + + NATIVE_DETAIL_TEXT, + ); - const native = await env.page.locator("#tx-detail-native").innerText(); - assert( - native.includes(STUB_NATIVE_VALUE_WEI + " wei"), - "the raw quantity row shows " + - JSON.stringify(native) + - ", expected the value in wei", - ); + const native = await env.page.locator("#tx-detail-native").innerText(); + assert( + native.includes(STUB_NATIVE_VALUE_WEI + " wei"), + "the raw quantity row shows " + + JSON.stringify(native) + + ", expected the value in wei", + ); - assert( - !(await env.page.isVisible("#tx-detail-token-contract-section")), - "the token contract row is showing on a transfer that has no token " + - "contract", - ); - - // Back to one seeded transaction for everything after this: the tests - // below were written against a list holding the token transfer alone. - env.routeOpts.seedNativeTransfer = false; + assert( + !(await env.page.isVisible("#tx-detail-token-contract-section")), + "the token contract row is showing on a transfer that has no " + + "token contract", + ); + } finally { + // Back to one seeded transaction for everything after this: the + // tests below were written against a list holding the token transfer + // alone. + env.routeOpts.seedNativeTransfer = false; + } }); test("tap-to-copy on the transaction detail screen copies the address (#151)", async (env) => { @@ -1771,6 +1776,7 @@ test("ConfirmTx blocks sending while the fee estimate is pending (#238)", async expectToken: true, }); + // Released by the next test, which watches the estimate land. env.routeOpts.holdGasEstimate = true; await goToConfirm(env.page, { token: "ETH", @@ -1933,102 +1939,117 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag ); env.routeOpts.failGasEstimate = true; env.routeOpts.holdGasEstimate = true; - await goToConfirm(env.page, { - token: "ETH", - balance: FUNDED_ETH_TEXT + " ETH", - amount: COMFORTABLE_AMOUNT, - }); + try { + await goToConfirm(env.page, { + token: "ETH", + balance: FUNDED_ETH_TEXT + " ETH", + amount: COMFORTABLE_AMOUNT, + }); - const pending = await confirmState(env.page); - assert( - pending.fee === "Estimating..." && pending.sendDisabled, - "the screen is not in the pending state before the estimate fails", - ); + const pending = await confirmState(env.page); + assert( + pending.fee === "Estimating..." && pending.sendDisabled, + "the screen is not in the pending state before the estimate fails", + ); - env.routeOpts.holdGasEstimate = false; - await waitForEstimate(env.page); + env.routeOpts.holdGasEstimate = false; + await waitForEstimate(env.page); - const st = await confirmState(env.page); - env.routeOpts.failGasEstimate = false; - assert( - st.fee === "Unable to estimate", - "the fee line does not report the failure: " + JSON.stringify(st.fee), - ); - assert( - !st.reserveShown, - "the reserve line is shown after a failed estimate", - ); - assert( - st.feeUnknownError, - "the estimate-failed message is not shown after a failed estimate", - ); - assert( - !st.amountFeeError && !st.gasError && st.errors === "", - "a balance message is shown for an estimate that simply failed", - ); - assert( - st.sendDisabled, - "Send is enabled with no usable fee estimate — an unknown fee is being treated as zero", - ); - assert( - st.height === pending.height, - "the view changed height when the estimate failed: " + - pending.height + - "px -> " + - st.height + - "px", - ); + const st = await confirmState(env.page); + assert( + st.fee === "Unable to estimate", + "the fee line does not report the failure: " + + JSON.stringify(st.fee), + ); + assert( + !st.reserveShown, + "the reserve line is shown after a failed estimate", + ); + assert( + st.feeUnknownError, + "the estimate-failed message is not shown after a failed estimate", + ); + assert( + !st.amountFeeError && !st.gasError && st.errors === "", + "a balance message is shown for an estimate that simply failed", + ); + assert( + st.sendDisabled, + "Send is enabled with no usable fee estimate — an unknown fee is being treated as zero", + ); + assert( + st.height === pending.height, + "the view changed height when the estimate failed: " + + pending.height + + "px -> " + + st.height + + "px", + ); + } finally { + env.routeOpts.failGasEstimate = false; + env.routeOpts.holdGasEstimate = false; + } }); test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (env) => { env.routeOpts.holdGasEstimate = true; - await goToConfirm(env.page, { - token: STUB_TOKEN.address, - balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, - amount: TOKEN_AMOUNT, - }); + try { + await goToConfirm(env.page, { + token: STUB_TOKEN.address, + balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, + amount: TOKEN_AMOUNT, + }); - const pending = await confirmState(env.page); - env.erc20PendingHeight = pending.height; - console.log("# confirm-tx ERC-20 view height: " + pending.height + "px"); - assert( - pending.type === "ERC-20 token transfer (" + STUB_TOKEN.symbol + ")", - "unexpected transaction type: " + JSON.stringify(pending.type), - ); - assert( - pending.balance === TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, - "the ERC-20 screen shows the wrong balance: " + - JSON.stringify(pending.balance), - ); - assert( - pending.fee === "Estimating..." && pending.sendDisabled, - "the ERC-20 screen does not block sending while its estimate is pending", - ); + const pending = await confirmState(env.page); + env.erc20PendingHeight = pending.height; + console.log( + "# confirm-tx ERC-20 view height: " + pending.height + "px", + ); + assert( + pending.type === + "ERC-20 token transfer (" + STUB_TOKEN.symbol + ")", + "unexpected transaction type: " + JSON.stringify(pending.type), + ); + assert( + pending.balance === TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, + "the ERC-20 screen shows the wrong balance: " + + JSON.stringify(pending.balance), + ); + assert( + pending.fee === "Estimating..." && pending.sendDisabled, + "the ERC-20 screen does not block sending while its estimate is pending", + ); - env.routeOpts.holdGasEstimate = false; - await waitForEstimate(env.page); + env.routeOpts.holdGasEstimate = false; + await waitForEstimate(env.page); - const st = await confirmState(env.page); - assert( - st.fee === "~" + feeEth(FEE_ESTIMATE_WEI), - "the ERC-20 fee line does not quote the estimate: " + - JSON.stringify(st.fee), - ); - assert( - st.reserveShown && - st.reserve === "up to " + feeEth(FEE_RESERVE_WEI) + " reserved", - "the ERC-20 fee block does not quote the reserve: " + - JSON.stringify(st.reserve), - ); - assert(!st.sendDisabled, "Send is disabled for a funded ERC-20 transfer"); - assert( - st.height === pending.height, - "the ERC-20 view changed height when the estimate landed: " + - pending.height + - "px -> " + - st.height + - "px", - ); + const st = await confirmState(env.page); + assert( + st.fee === "~" + feeEth(FEE_ESTIMATE_WEI), + "the ERC-20 fee line does not quote the estimate: " + + JSON.stringify(st.fee), + ); + assert( + st.reserveShown && + st.reserve === "up to " + feeEth(FEE_RESERVE_WEI) + " reserved", + "the ERC-20 fee block does not quote the reserve: " + + JSON.stringify(st.reserve), + ); + assert( + !st.sendDisabled, + "Send is disabled for a funded ERC-20 transfer", + ); + assert( + st.height === pending.height, + "the ERC-20 view changed height when the estimate landed: " + + pending.height + + "px -> " + + st.height + + "px", + ); + } finally { + env.routeOpts.holdGasEstimate = false; + } }); test("ConfirmTx refuses an ERC-20 send that exceeds the token balance (#238)", async (env) => { @@ -2130,40 +2151,43 @@ test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee proble /gas estimation failed/, ); env.routeOpts.failGasEstimate = true; - await goToConfirm(env.page, { - token: STUB_TOKEN.address, - balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, - amount: TOKEN_AMOUNT, - }); - await waitForEstimate(env.page); + try { + await goToConfirm(env.page, { + token: STUB_TOKEN.address, + balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, + amount: TOKEN_AMOUNT, + }); + await waitForEstimate(env.page); - const st = await confirmState(env.page); - env.routeOpts.failGasEstimate = false; - assert( - st.fee === "Unable to estimate", - "the ERC-20 fee line does not report the failure: " + - JSON.stringify(st.fee), - ); - assert( - st.feeUnknownError, - "the estimate-failed message is not shown on the ERC-20 path", - ); - assert( - !st.gasError, - "the ERC-20 network-fee message is shown for a fee that is unknown rather than unaffordable", - ); - assert( - st.sendDisabled, - "Send is enabled on the ERC-20 path with no usable fee estimate", - ); - assert( - st.height === env.erc20PendingHeight, - "the ERC-20 estimate-failed state is a different height than its pending state: " + - env.erc20PendingHeight + - "px -> " + - st.height + - "px", - ); + const st = await confirmState(env.page); + assert( + st.fee === "Unable to estimate", + "the ERC-20 fee line does not report the failure: " + + JSON.stringify(st.fee), + ); + assert( + st.feeUnknownError, + "the estimate-failed message is not shown on the ERC-20 path", + ); + assert( + !st.gasError, + "the ERC-20 network-fee message is shown for a fee that is unknown rather than unaffordable", + ); + assert( + st.sendDisabled, + "Send is enabled on the ERC-20 path with no usable fee estimate", + ); + assert( + st.height === env.erc20PendingHeight, + "the ERC-20 estimate-failed state is a different height than its pending state: " + + env.erc20PendingHeight + + "px -> " + + st.height + + "px", + ); + } finally { + env.routeOpts.failGasEstimate = false; + } }); // ------------------------- the popup's own send, end to end (#305) @@ -2220,6 +2244,34 @@ async function fillPasswordAndSend(page) { 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) { await goToConfirm(env.page, { token: STUB_TOKEN.address, @@ -2240,132 +2292,143 @@ 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 // blocks sending outright; this one has to be able to press Send. env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI); - await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true }); - const shown = await goToTokenConfirm(env); - - 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 visible(env.page, "#view-wait-tx", 60000); + try { + await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true }); + const shown = await goToTokenConfirm(env); - const broadcast = env.routeOpts.broadcastTransactions; - assert( - broadcast.length === before + 1, - "expected exactly one raw transaction to reach the RPC, got " + - (broadcast.length - before), - ); - const { signed, recipient, rawAmount } = decodeTransfer( - broadcast[broadcast.length - 1], - ); + const before = env.routeOpts.broadcastTransactions.length; + await fillPasswordAndSend(env.page); + await visible(env.page, "#view-wait-tx", 60000); - // The measurement, printed on every run: the amount the user read, and - // what the signed bytes mean at each of the two candidate scales. Under - // the defect these three lines disagree. - console.log( - "# erc-20 send artifact: displayed=" + - JSON.stringify(shown) + - " rawAmount=" + - rawAmount + - " asIf" + - TOKEN_DECIMALS + - "Decimals=" + - formatUnits(rawAmount, TOKEN_DECIMALS) + - " asIf" + - LYING_DECIMALS + - "Decimals=" + - formatUnits(rawAmount, Number(LYING_DECIMALS)), - ); + const broadcast = env.routeOpts.broadcastTransactions; + assert( + broadcast.length === before + 1, + "expected exactly one raw transaction to reach the RPC, got " + + (broadcast.length - before), + ); + const { signed, recipient, rawAmount } = decodeTransfer( + broadcast[broadcast.length - 1], + ); - assert( - getAddress(signed.to) === getAddress(STUB_TOKEN.address), - "the broadcast transaction does not call the token contract: " + - signed.to, - ); - assert( - recipient === getAddress(STUB_COUNTERPARTY), - "the broadcast transfer goes to " + recipient, - ); - // What the whole issue turns on: the signed amount, read back at the - // scale the SCREEN rendered with, is the number the screen rendered. - const wanted = parseUnits(shown.split(" ")[0], TOKEN_DECIMALS); - assert( - rawAmount === wanted, - "the broadcast transfer moves " + - rawAmount + - " base units, which is " + - formatUnits(rawAmount, TOKEN_DECIMALS) + - " " + - STUB_TOKEN.symbol + - " at the scale the confirmation screen displayed — but the screen" + - " displayed " + - JSON.stringify(shown) + - ", i.e. " + - wanted + - " base units (#305)", - ); + // The measurement, printed on every run: the amount the user read, + // and what the signed bytes mean at each of the two candidate scales. + // Under the defect these three lines disagree. + console.log( + "# erc-20 send artifact: displayed=" + + JSON.stringify(shown) + + " rawAmount=" + + rawAmount + + " asIf" + + TOKEN_DECIMALS + + "Decimals=" + + formatUnits(rawAmount, TOKEN_DECIMALS) + + " asIf" + + LYING_DECIMALS + + "Decimals=" + + formatUnits(rawAmount, Number(LYING_DECIMALS)), + ); - const summary = ( - await env.page.locator("#wait-tx-summary").innerText() - ).trim(); - assert( - summary === shown, - "the wait screen summarises the send as " + - JSON.stringify(summary) + - ", not as the approved " + - JSON.stringify(shown), - ); + assert( + getAddress(signed.to) === getAddress(STUB_TOKEN.address), + "the broadcast transaction does not call the token contract: " + + signed.to, + ); + assert( + recipient === getAddress(STUB_COUNTERPARTY), + "the broadcast transfer goes to " + recipient, + ); + // What the whole issue turns on: the signed amount, read back at the + // scale the SCREEN rendered with, is the number the screen rendered. + const wanted = parseUnits(shown.split(" ")[0], TOKEN_DECIMALS); + assert( + rawAmount === wanted, + "the broadcast transfer moves " + + rawAmount + + " base units, which is " + + formatUnits(rawAmount, TOKEN_DECIMALS) + + " " + + STUB_TOKEN.symbol + + " at the scale the confirmation screen displayed — but the screen" + + " displayed " + + JSON.stringify(shown) + + ", i.e. " + + wanted + + " base units (#305)", + ); - await visible(env.page, "#view-success-tx", 60000); - await env.page.click("#btn-success-tx-done"); - await visible(env.page, "#view-address"); - env.routeOpts.seedReceipt = false; + const summary = ( + await env.page.locator("#wait-tx-summary").innerText() + ).trim(); + assert( + summary === shown, + "the wait screen summarises the send as " + + JSON.stringify(summary) + + ", not as the approved " + + JSON.stringify(shown), + ); + + await visible(env.page, "#view-success-tx", 60000); + } finally { + env.routeOpts.seedReceipt = false; + await backToAddressAfterSend(env); + } }); test("a token that lies about decimals() at signing time broadcasts nothing (#305)", async (env) => { - const shown = await goToTokenConfirm(env); + try { + const shown = await goToTokenConfirm(env); - // Only now, with the screen already built and its estimate already taken - // at the explorer's scale, does the contract start answering differently. - // This is the whole shape of the defect: a value read at signing time that - // nothing on screen was ever derived from. - env.routeOpts.tokenDecimalsOverride = LYING_DECIMALS; - const before = env.routeOpts.broadcastTransactions.length; - await fillPasswordAndSend(env.page); - await visible(env.page, "#view-error-tx", 60000); - env.routeOpts.tokenDecimalsOverride = null; + // Only now, with the screen already built and its estimate already + // taken at the explorer's scale, does the contract start answering + // differently. This is the whole shape of the defect: a value read at + // signing time that nothing on screen was ever derived from. + env.routeOpts.tokenDecimalsOverride = LYING_DECIMALS; + const before = env.routeOpts.broadcastTransactions.length; + await fillPasswordAndSend(env.page); - assert( - env.routeOpts.broadcastTransactions.length === before, - "a transfer encoded against a contract that contradicts the " + - "confirmation screen still reached the RPC (#305)", - ); + // 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( + env.routeOpts.broadcastTransactions.length === before, + "a transfer encoded against a contract that contradicts the " + + "confirmation screen still reached the RPC (#305)", + ); + await visible(env.page, "#view-error-tx"); - const message = ( - await env.page.locator("#error-tx-message").innerText() - ).trim(); - console.log( - "# erc-20 decimals refusal: displayed=" + - JSON.stringify(shown) + - " contract=" + - LYING_DECIMALS + - " message=" + - JSON.stringify(message), - ); - assert( - message.includes("reports " + LYING_DECIMALS + " decimal places") && - message.includes("displayed using " + STUB_TOKEN.decimals), - "the refusal does not name both scales it is refusing over: " + - JSON.stringify(message), - ); - assert( - /^[A-Z].*\.$/s.test(message), - "the refusal is not a full sentence: " + JSON.stringify(message), - ); - - await env.page.click("#btn-error-tx-done"); - await visible(env.page, "#view-address"); + const message = ( + await env.page.locator("#error-tx-message").innerText() + ).trim(); + console.log( + "# erc-20 decimals refusal: displayed=" + + JSON.stringify(shown) + + " contract=" + + LYING_DECIMALS + + " message=" + + JSON.stringify(message), + ); + assert( + message.includes("reports " + LYING_DECIMALS + " decimal places") && + message.includes("displayed using " + STUB_TOKEN.decimals), + "the refusal does not name both scales it is refusing over: " + + JSON.stringify(message), + ); + assert( + /^[A-Z].*\.$/s.test(message), + "the refusal is not a full sentence: " + JSON.stringify(message), + ); + } finally { + env.routeOpts.tokenDecimalsOverride = null; + await backToAddressAfterSend(env); + } }); // ------------------------------------------- hostile token symbol (#307) @@ -2437,74 +2500,78 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env) "# stub token symbol() now returns: " + JSON.stringify(HOSTILE_SYMBOL), ); - // Close and reopen so the refresh that runs on open fetches balances - // with the hostile symbol in them. - await reopenPopup(env, "address"); - await env.page.waitForFunction( - (addr) => - !!document.querySelector( - '#address-balances [data-token="' + addr + '"]', - ), - STUB_TOKEN.address, - { timeout: 60000 }, - ); - const onAddress = await env.page.evaluate(() => ({ - iframes: document.querySelectorAll("iframe").length, - pwnPresent: !!document.getElementById("pwn"), - })); - console.log("# address-detail iframes = " + onAddress.iframes); - assert( - onAddress.iframes === 0 && !onAddress.pwnPresent, - "the address screen contains " + - onAddress.iframes + - " iframe(s) after a hostile symbol rendered (#307)", - ); + try { + // Close and reopen so the refresh that runs on open fetches balances + // with the hostile symbol in them. + await reopenPopup(env, "address"); + await env.page.waitForFunction( + (addr) => + !!document.querySelector( + '#address-balances [data-token="' + addr + '"]', + ), + STUB_TOKEN.address, + { timeout: 60000 }, + ); + const onAddress = await env.page.evaluate(() => ({ + iframes: document.querySelectorAll("iframe").length, + pwnPresent: !!document.getElementById("pwn"), + })); + console.log("# address-detail iframes = " + onAddress.iframes); + assert( + onAddress.iframes === 0 && !onAddress.pwnPresent, + "the address screen contains " + + onAddress.iframes + + " iframe(s) after a hostile symbol rendered (#307)", + ); - await env.page.click("#btn-address-back"); - await visible(env.page, "#view-main"); - await visible( - env.page, - '#wallet-list [data-token="' + STUB_TOKEN.address + '"]', - 60000, - ); + await env.page.click("#btn-address-back"); + await visible(env.page, "#view-main"); + await visible( + env.page, + '#wallet-list [data-token="' + STUB_TOKEN.address + '"]', + 60000, + ); - const st = await hostileSymbolState(env.page, STUB_TOKEN.address); - console.log( - "# iframes in the popup DOM = " + - st.iframes + - " | #pwn present = " + - st.pwnPresent + - " | symbol = " + - JSON.stringify(st.symbolText), - ); + const st = await hostileSymbolState(env.page, STUB_TOKEN.address); + console.log( + "# iframes in the popup DOM = " + + st.iframes + + " | #pwn present = " + + st.pwnPresent + + " | symbol = " + + JSON.stringify(st.symbolText), + ); - assert(st.rowFound, "the hostile token never rendered a row at all"); - assert( - st.iframes === 0, - "the popup DOM contains " + st.iframes + " iframe(s) (#307)", - ); - assert(!st.pwnPresent, "the injected #pwn element is in the popup DOM"); - assert( - st.symbolElementChildren === 0, - "the symbol span grew " + - st.symbolElementChildren + - " element children out of a token symbol (#307)", - ); - assert( - st.symbolText === HOSTILE_SYMBOL_DISPLAYED, - "the symbol did not render as the literal capped text " + - JSON.stringify(HOSTILE_SYMBOL_DISPLAYED) + - ": " + - JSON.stringify(st.symbolText), - ); - assert( - !st.rowText.includes("z-index"), - "the uncapped symbol reached the screen: " + JSON.stringify(st.rowText), - ); + assert(st.rowFound, "the hostile token never rendered a row at all"); + assert( + st.iframes === 0, + "the popup DOM contains " + st.iframes + " iframe(s) (#307)", + ); + assert(!st.pwnPresent, "the injected #pwn element is in the popup DOM"); + assert( + st.symbolElementChildren === 0, + "the symbol span grew " + + st.symbolElementChildren + + " element children out of a token symbol (#307)", + ); + assert( + st.symbolText === HOSTILE_SYMBOL_DISPLAYED, + "the symbol did not render as the literal capped text " + + JSON.stringify(HOSTILE_SYMBOL_DISPLAYED) + + ": " + + JSON.stringify(st.symbolText), + ); + assert( + !st.rowText.includes("z-index"), + "the uncapped symbol reached the screen: " + + JSON.stringify(st.rowText), + ); + } finally { + // Put the fixture back before the next test reads it. + env.routeOpts.tokenSymbolOverride = null; + } - // Put the fixture back before the next test reads it, and let the - // stored balances be rewritten with the honest symbol. - env.routeOpts.tokenSymbolOverride = null; + // Let the stored balances be rewritten with the honest symbol. await reopenPopup(env, "main"); await env.page.waitForFunction( (addr) => {