Compare commits
2
Commits
4a2d0b4a2a
...
3ddea28ed8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ddea28ed8 | ||
|
|
9bd607b411 |
@@ -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
|
what [#154](https://git.eeqj.de/sneak/AutistMask/issues/154) was, and it was
|
||||||
previously correct by reading only.
|
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
|
It also covers the **dApp approval round trips** — the one place where the
|
||||||
content script, the inpage provider, the background worker and the approval
|
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
|
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
|
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
|
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
|
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`)
|
### Element id guard (part of `make check`)
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,27 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# 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
|
||||||
|
([#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
|
- 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
|
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,
|
([#254](https://git.eeqj.de/sneak/AutistMask/issues/254)): Send on the main,
|
||||||
|
|||||||
+30
-6
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { Transaction } = require("ethers");
|
const { AbiCoder, Transaction } = require("ethers");
|
||||||
|
|
||||||
// Fictional ERC-20 used to seed the transaction-detail test. The symbol
|
// Fictional ERC-20 used to seed the transaction-detail test. The symbol
|
||||||
// must not collide with any entry in src/shared/tokenList.js, or
|
// 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_DECIMALS = "0x313ce567";
|
||||||
|
const SELECTOR_SYMBOL = "0x95d89b41";
|
||||||
|
const SELECTOR_NAME = "0x06fdde03";
|
||||||
|
|
||||||
// Every eth_call still answers with a zero word except decimals() on the
|
// Every eth_call still answers with a zero word except decimals(), symbol()
|
||||||
// stub token, which the wallet reads back at signing time to compare with
|
// and name() on the stub token. The wallet reads decimals() back at signing
|
||||||
// the scale the confirmation screen rendered (issue #305).
|
// 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()
|
// opts.tokenDecimalsOverride is the lying contract: set it and decimals()
|
||||||
// answers something other than the value this same fixture reports through
|
// 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;
|
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 (to !== STUB_TOKEN.address) return ZERO_WORD;
|
||||||
|
if (data.startsWith(SELECTOR_DECIMALS)) {
|
||||||
return word(opts.tokenDecimalsOverride ?? STUB_TOKEN.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;
|
return ZERO_WORD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +460,16 @@ function rpcReply(req, opts, report) {
|
|||||||
return Object.assign(envelope, { result: ethCallResult(req, opts) });
|
return Object.assign(envelope, { result: ethCallResult(req, opts) });
|
||||||
}
|
}
|
||||||
if (req.method === "eth_getTransactionReceipt") {
|
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;
|
const hash = Array.isArray(req.params) ? req.params[0] : null;
|
||||||
return Object.assign(envelope, {
|
return Object.assign(envelope, {
|
||||||
result: opts.seedReceipt && hash ? transactionReceipt(hash) : null,
|
result: opts.seedReceipt && hash ? transactionReceipt(hash) : null,
|
||||||
@@ -607,6 +629,8 @@ function traceEnabled(raw) {
|
|||||||
* symbol is markup; read at request time.
|
* symbol is markup; read at request time.
|
||||||
* @param {boolean} [opts.seedReceipt] answer eth_getTransactionReceipt with a
|
* @param {boolean} [opts.seedReceipt] answer eth_getTransactionReceipt with a
|
||||||
* confirmed receipt instead of null, so a wait screen resolves.
|
* 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) =>
|
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
|
||||||
* Promise<string|null>}>}
|
* Promise<string|null>}>}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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)
|
// -------------------------------------------- recovery phrase (#161)
|
||||||
|
|
||||||
// The gear toggles, so pressing it while Settings is already up leaves it.
|
// The gear toggles, so pressing it while Settings is already up leaves it.
|
||||||
@@ -2699,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)
|
// ------------------------------------------- hostile token symbol (#307)
|
||||||
//
|
//
|
||||||
// The reproduction from the issue, in the real browser against the real
|
// The reproduction from the issue, in the real browser against the real
|
||||||
@@ -4302,6 +4480,8 @@ async function main() {
|
|||||||
// Whether eth_getTransactionReceipt confirms a transaction rather than
|
// Whether eth_getTransactionReceipt confirms a transaction rather than
|
||||||
// answering "not mined yet".
|
// answering "not mined yet".
|
||||||
seedReceipt: false,
|
seedReceipt: false,
|
||||||
|
// Whether eth_getTransactionReceipt fails instead of answering (#315).
|
||||||
|
failReceiptLookup: false,
|
||||||
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
||||||
// order. The dApp transaction round trip asserts against these bytes
|
// order. The dApp transaction round trip asserts against these bytes
|
||||||
// rather than against anything the extension reported about them.
|
// rather than against anything the extension reported about them.
|
||||||
|
|||||||
Reference in New Issue
Block a user