diff --git a/TODO.md b/TODO.md index 32d1c76..8199b6b 100644 --- a/TODO.md +++ b/TODO.md @@ -45,6 +45,17 @@ but the review is broader than any of them. # Completed Steps +- 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 + ([#295](https://git.eeqj.de/sneak/AutistMask/issues/295)): a token added on + Add Token by its contract address is listed on the address screen; + TransactionDetail opened from the token screen leaves that screen on the + persisted navigation stack, and Back returns to it; and the token contract row + links to the explorer's token page, read off the link rather than followed. + The network stub answers `symbol()` and `name()` for the stub token, which + adding it reads. + - 2026-10-05: Each control that leads to a signature or to the private key has a test that it refuses a defective wallet before asking for a password ([#254](https://git.eeqj.de/sneak/AutistMask/issues/254)): Send on the main, diff --git a/tests/e2e/network.js b/tests/e2e/network.js index 1200edb..60ef50c 100644 --- a/tests/e2e/network.js +++ b/tests/e2e/network.js @@ -22,7 +22,7 @@ "use strict"; -const { Transaction } = require("ethers"); +const { AbiCoder, Transaction } = require("ethers"); // Fictional ERC-20 used to seed the transaction-detail test. The symbol // must not collide with any entry in src/shared/tokenList.js, or @@ -244,12 +244,16 @@ function latestBlock() { }; } -// keccak("decimals()")[0:4]. +// keccak("decimals()")[0:4], and the same for symbol() and name(). const SELECTOR_DECIMALS = "0x313ce567"; +const SELECTOR_SYMBOL = "0x95d89b41"; +const SELECTOR_NAME = "0x06fdde03"; -// Every eth_call still answers with a zero word except decimals() on the -// stub token, which the wallet reads back at signing time to compare with -// the scale the confirmation screen rendered (issue #305). +// Every eth_call still answers with a zero word except decimals(), symbol() +// and name() on the stub token. The wallet reads decimals() back at signing +// time to compare with the scale the confirmation screen rendered (issue +// #305). Adding the token by its contract address reads all three (issue +// #295); symbol() and name() answer what the explorer reports for it. // // opts.tokenDecimalsOverride is the lying contract: set it and decimals() // answers something other than the value this same fixture reports through @@ -263,9 +267,17 @@ function ethCallResult(req, opts) { 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) { + if (to !== STUB_TOKEN.address) return ZERO_WORD; + if (data.startsWith(SELECTOR_DECIMALS)) { return word(opts.tokenDecimalsOverride ?? STUB_TOKEN.decimals); } + const abi = AbiCoder.defaultAbiCoder(); + if (data.startsWith(SELECTOR_SYMBOL)) { + return abi.encode(["string"], [tokenObject(opts).symbol]); + } + if (data.startsWith(SELECTOR_NAME)) { + return abi.encode(["string"], [tokenObject(opts).name]); + } return ZERO_WORD; } diff --git a/tests/e2e/run.js b/tests/e2e/run.js index 5c1ad86..f4106f1 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -596,6 +596,106 @@ test("tap-to-copy on the transaction detail screen copies the address (#151)", a ); }); +// ------------------------- the last of the #150 and #151 items (#295) +// +// Add Token's confirm button, TransactionDetail opened from the token screen +// and Back from it, and the explorer link on the token contract row. + +// The stub token stays tracked for the rest of the run: the next test reaches +// its token screen through the balance row this one adds. +test("a token added by its contract address is listed on the address screen (#150)", async (env) => { + await leaveTransactionDetail(env.page); + await env.page.click("#btn-add-token"); + await visible(env.page, "#view-add-token"); + + await env.page.fill("#add-token-address", STUB_TOKEN.address); + await env.page.click("#btn-add-token-confirm"); + await visible(env.page, "#view-address"); + + // No wait: the confirm renders the balance list before it shows the + // screen, and nothing renders the list again while the screen is up. + const row = env.page.locator( + '#address-balances [data-token="' + STUB_TOKEN.address + '"]', + { hasText: STUB_TOKEN.symbol }, + ); + const balances = await env.page.locator("#address-balances").innerText(); + assert( + (await row.count()) === 1, + "the balance list has no " + + STUB_TOKEN.symbol + + " row for the token just added: " + + JSON.stringify(balances), + ); +}); + +// TransactionDetail looks the same from either entry point. Only the +// persisted stack says which one opened it, so that is what is asserted: from +// the token screen it ends in "address-token", and Back has to land there +// rather than on the address screen beneath it. +test("transaction detail opened from the token screen goes Back to it (#151)", async (env) => { + await goHome(env.page); + const base = await persistedViewStack(env.page); + + await env.page.locator("#wallet-list .btn-addr-info").first().click(); + await visible(env.page, "#view-address"); + await env.page + .locator('#address-balances [data-token="' + STUB_TOKEN.address + '"]') + .click(); + await visible(env.page, "#view-address-token"); + + const row = env.page.locator("#address-token-tx-list .tx-row").first(); + await row.waitFor({ state: "visible", timeout: 30000 }); + await row.click(); + await visible(env.page, "#view-transaction"); + await waitForPersisted( + env.page, + "viewStack", + base.concat("main", "address", "address-token"), + "on transaction detail opened from the token screen", + ); + + // The stack is checked before the screen, so a Back that lands on the + // wrong screen fails by saying what the stack holds. + await env.page.click("#btn-tx-back"); + await waitForPersisted( + env.page, + "viewStack", + base.concat("main", "address"), + "after Back from transaction detail", + ); + await visible(env.page, "#view-address-token"); + + // Onto the address screen, which the next test starts from. + await env.page.click("#btn-address-token-back"); + await visible(env.page, "#view-address"); +}); + +// Read off the anchor rather than followed: where it points is all the popup +// decides, and following it would only load the explorer's page. The suite is +// on mainnet until the Settings section. +test("the token contract row links to the explorer's token page (#151)", async (env) => { + await leaveTransactionDetail(env.page); + const row = env.page + .locator("#tx-list .tx-row") + .filter({ hasText: STUB_TOKEN.symbol }); + await row.waitFor({ state: "visible", timeout: 30000 }); + await row.click(); + await visible(env.page, "#view-transaction"); + await visible(env.page, "#tx-detail-token-contract-section"); + + const href = await env.page + .locator("#tx-detail-token-contract a") + .getAttribute("href"); + const expected = "https://etherscan.io/token/" + STUB_TOKEN.address; + assert( + href === expected, + "the token contract row links to " + + JSON.stringify(href) + + ", expected " + + expected, + ); +}); + // -------------------------------------------- recovery phrase (#161) // The gear toggles, so pressing it while Settings is already up leaves it.