test: a failing e2e test leaves no fixture switch or send screen behind (closes #318)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s

A test that turns a fixture switch on for itself alone turns it off in a
finally, so a failure no longer reddens the tests after it. The two tests
that drive the popup's own send also return it to the address screen,
reopening the popup to leave a wait for a receipt. The lying-decimals()
test asserts that nothing was broadcast as soon as the send ends, before
waiting for the failure screen. ethCallResult() answers an override of 0
instead of falling back to the explorer's scale.

Model: opus-5-5
This commit is contained in:
2026-10-05 06:38:15 +00:00
parent 6613a1f6bc
commit 7de9dd26fe
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: A prompt raised while another approval window has focus opens a - 2026-10-05: A prompt raised while another approval window has focus opens a
window of its own ([#290](https://git.eeqj.de/sneak/AutistMask/issues/290)). window of its own ([#290](https://git.eeqj.de/sneak/AutistMask/issues/290)).
The background centred each approval window on the last focused window, which The background centred each approval window on the last focused window, which
+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.
+404 -337
View File
@@ -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 // 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;
await env.page.reload(); try {
await openAddressDetail(env.page); await env.page.reload();
await openAddressDetail(env.page);
const row = env.page const row = env.page
.locator("#tx-list .tx-row") .locator("#tx-list .tx-row")
.filter({ hasText: NATIVE_ROW_TEXT }); .filter({ hasText: NATIVE_ROW_TEXT });
await row.waitFor({ state: "visible", timeout: 30000 }); await row.waitFor({ state: "visible", timeout: 30000 });
await row.click(); await row.click();
await visible(env.page, "#view-transaction"); await visible(env.page, "#view-transaction");
const hash = await env.page.locator("#tx-detail-hash").innerText(); const hash = await env.page.locator("#tx-detail-hash").innerText();
assert( assert(
hash.includes(STUB_NATIVE_TX_HASH), hash.includes(STUB_NATIVE_TX_HASH),
"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 = (
assert( await env.page.locator("#tx-detail-type").innerText()
type === "Native ETH Transfer", ).trim();
"the native transaction was classified " + JSON.stringify(type), assert(
); type === "Native ETH Transfer",
"the native transaction was classified " + JSON.stringify(type),
);
const value = await env.page.locator("#tx-detail-value").innerText(); const value = await env.page.locator("#tx-detail-value").innerText();
assert( assert(
value.includes(NATIVE_DETAIL_TEXT), value.includes(NATIVE_DETAIL_TEXT),
"the native transaction detail shows " + "the native transaction detail shows " +
JSON.stringify(value) + JSON.stringify(value) +
", expected it to contain " + ", expected it to contain " +
NATIVE_DETAIL_TEXT, NATIVE_DETAIL_TEXT,
); );
const native = await env.page.locator("#tx-detail-native").innerText(); const native = await env.page.locator("#tx-detail-native").innerText();
assert( assert(
native.includes(STUB_NATIVE_VALUE_WEI + " wei"), native.includes(STUB_NATIVE_VALUE_WEI + " wei"),
"the raw quantity row shows " + "the raw quantity row shows " +
JSON.stringify(native) + JSON.stringify(native) +
", expected the value in wei", ", expected the value in wei",
); );
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
env.routeOpts.seedNativeTransfer = false; // alone.
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) => {
@@ -1771,6 +1776,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",
@@ -1933,102 +1939,117 @@ 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;
await goToConfirm(env.page, { try {
token: "ETH", await goToConfirm(env.page, {
balance: FUNDED_ETH_TEXT + " ETH", token: "ETH",
amount: COMFORTABLE_AMOUNT, balance: FUNDED_ETH_TEXT + " ETH",
}); amount: COMFORTABLE_AMOUNT,
});
const pending = await confirmState(env.page); const pending = await confirmState(env.page);
assert( assert(
pending.fee === "Estimating..." && pending.sendDisabled, pending.fee === "Estimating..." && pending.sendDisabled,
"the screen is not in the pending state before the estimate fails", "the screen is not in the pending state before the estimate fails",
); );
env.routeOpts.holdGasEstimate = false; env.routeOpts.holdGasEstimate = false;
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: " +
"the fee line does not report the failure: " + JSON.stringify(st.fee), JSON.stringify(st.fee),
); );
assert( assert(
!st.reserveShown, !st.reserveShown,
"the reserve line is shown after a failed estimate", "the reserve line is shown after a failed estimate",
); );
assert( assert(
st.feeUnknownError, st.feeUnknownError,
"the estimate-failed message is not shown after a failed estimate", "the estimate-failed message is not shown after a failed estimate",
); );
assert( assert(
!st.amountFeeError && !st.gasError && st.errors === "", !st.amountFeeError && !st.gasError && st.errors === "",
"a balance message is shown for an estimate that simply failed", "a balance message is shown for an estimate that simply failed",
); );
assert( assert(
st.sendDisabled, st.sendDisabled,
"Send is enabled with no usable fee estimate — an unknown fee is being treated as zero", "Send is enabled with no usable fee estimate — an unknown fee is being treated as zero",
); );
assert( assert(
st.height === pending.height, st.height === pending.height,
"the view changed height when the estimate failed: " + "the view changed height when the estimate failed: " +
pending.height + pending.height +
"px -> " + "px -> " +
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;
await goToConfirm(env.page, { try {
token: STUB_TOKEN.address, await goToConfirm(env.page, {
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, token: STUB_TOKEN.address,
amount: TOKEN_AMOUNT, balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
}); amount: TOKEN_AMOUNT,
});
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(
assert( "# confirm-tx ERC-20 view height: " + pending.height + "px",
pending.type === "ERC-20 token transfer (" + STUB_TOKEN.symbol + ")", );
"unexpected transaction type: " + JSON.stringify(pending.type), assert(
); pending.type ===
assert( "ERC-20 token transfer (" + STUB_TOKEN.symbol + ")",
pending.balance === TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, "unexpected transaction type: " + JSON.stringify(pending.type),
"the ERC-20 screen shows the wrong balance: " + );
JSON.stringify(pending.balance), assert(
); pending.balance === TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
assert( "the ERC-20 screen shows the wrong balance: " +
pending.fee === "Estimating..." && pending.sendDisabled, JSON.stringify(pending.balance),
"the ERC-20 screen does not block sending while its estimate is pending", );
); assert(
pending.fee === "Estimating..." && pending.sendDisabled,
"the ERC-20 screen does not block sending while its estimate is pending",
);
env.routeOpts.holdGasEstimate = false; env.routeOpts.holdGasEstimate = false;
await waitForEstimate(env.page); await waitForEstimate(env.page);
const st = await confirmState(env.page); const st = await confirmState(env.page);
assert( assert(
st.fee === "~" + feeEth(FEE_ESTIMATE_WEI), st.fee === "~" + feeEth(FEE_ESTIMATE_WEI),
"the ERC-20 fee line does not quote the estimate: " + "the ERC-20 fee line does not quote the estimate: " +
JSON.stringify(st.fee), JSON.stringify(st.fee),
); );
assert( assert(
st.reserveShown && st.reserveShown &&
st.reserve === "up to " + feeEth(FEE_RESERVE_WEI) + " reserved", st.reserve === "up to " + feeEth(FEE_RESERVE_WEI) + " reserved",
"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(
assert( !st.sendDisabled,
st.height === pending.height, "Send is disabled for a funded ERC-20 transfer",
"the ERC-20 view changed height when the estimate landed: " + );
pending.height + assert(
"px -> " + st.height === pending.height,
st.height + "the ERC-20 view changed height when the estimate landed: " +
"px", 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) => { 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/, /gas estimation failed/,
); );
env.routeOpts.failGasEstimate = true; env.routeOpts.failGasEstimate = true;
await goToConfirm(env.page, { try {
token: STUB_TOKEN.address, await goToConfirm(env.page, {
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol, token: STUB_TOKEN.address,
amount: TOKEN_AMOUNT, balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
}); amount: TOKEN_AMOUNT,
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: " + JSON.stringify(st.fee),
JSON.stringify(st.fee), );
); assert(
assert( st.feeUnknownError,
st.feeUnknownError, "the estimate-failed message is not shown on the ERC-20 path",
"the estimate-failed message is not shown on the ERC-20 path", );
); assert(
assert( !st.gasError,
!st.gasError, "the ERC-20 network-fee message is shown for a fee that is unknown rather than unaffordable",
"the ERC-20 network-fee message is shown for a fee that is unknown rather than unaffordable", );
); assert(
assert( st.sendDisabled,
st.sendDisabled, "Send is enabled on the ERC-20 path with no usable fee estimate",
"Send is enabled on the ERC-20 path with no usable fee estimate", );
); assert(
assert( st.height === env.erc20PendingHeight,
st.height === env.erc20PendingHeight, "the ERC-20 estimate-failed state is a different height than its pending state: " +
"the ERC-20 estimate-failed state is a different height than its pending state: " + env.erc20PendingHeight +
env.erc20PendingHeight + "px -> " +
"px -> " + 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)
@@ -2220,6 +2244,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,
@@ -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 // 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);
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 // 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. // resolves to the success view instead of polling for the rest of the run.
env.routeOpts.seedReceipt = true; env.routeOpts.seedReceipt = true;
await fillPasswordAndSend(env.page); try {
await visible(env.page, "#view-wait-tx", 60000); await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
const shown = await goToTokenConfirm(env);
const broadcast = env.routeOpts.broadcastTransactions; const before = env.routeOpts.broadcastTransactions.length;
assert( await fillPasswordAndSend(env.page);
broadcast.length === before + 1, await visible(env.page, "#view-wait-tx", 60000);
"expected exactly one raw transaction to reach the RPC, got " +
(broadcast.length - before),
);
const { signed, recipient, rawAmount } = decodeTransfer(
broadcast[broadcast.length - 1],
);
// The measurement, printed on every run: the amount the user read, and const broadcast = env.routeOpts.broadcastTransactions;
// what the signed bytes mean at each of the two candidate scales. Under assert(
// the defect these three lines disagree. broadcast.length === before + 1,
console.log( "expected exactly one raw transaction to reach the RPC, got " +
"# erc-20 send artifact: displayed=" + (broadcast.length - before),
JSON.stringify(shown) + );
" rawAmount=" + const { signed, recipient, rawAmount } = decodeTransfer(
rawAmount + broadcast[broadcast.length - 1],
" asIf" + );
TOKEN_DECIMALS +
"Decimals=" +
formatUnits(rawAmount, TOKEN_DECIMALS) +
" asIf" +
LYING_DECIMALS +
"Decimals=" +
formatUnits(rawAmount, Number(LYING_DECIMALS)),
);
assert( // The measurement, printed on every run: the amount the user read,
getAddress(signed.to) === getAddress(STUB_TOKEN.address), // and what the signed bytes mean at each of the two candidate scales.
"the broadcast transaction does not call the token contract: " + // Under the defect these three lines disagree.
signed.to, console.log(
); "# erc-20 send artifact: displayed=" +
assert( JSON.stringify(shown) +
recipient === getAddress(STUB_COUNTERPARTY), " rawAmount=" +
"the broadcast transfer goes to " + recipient, rawAmount +
); " asIf" +
// What the whole issue turns on: the signed amount, read back at the TOKEN_DECIMALS +
// scale the SCREEN rendered with, is the number the screen rendered. "Decimals=" +
const wanted = parseUnits(shown.split(" ")[0], TOKEN_DECIMALS); formatUnits(rawAmount, TOKEN_DECIMALS) +
assert( " asIf" +
rawAmount === wanted, LYING_DECIMALS +
"the broadcast transfer moves " + "Decimals=" +
rawAmount + formatUnits(rawAmount, Number(LYING_DECIMALS)),
" 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)",
);
const summary = ( assert(
await env.page.locator("#wait-tx-summary").innerText() getAddress(signed.to) === getAddress(STUB_TOKEN.address),
).trim(); "the broadcast transaction does not call the token contract: " +
assert( signed.to,
summary === shown, );
"the wait screen summarises the send as " + assert(
JSON.stringify(summary) + recipient === getAddress(STUB_COUNTERPARTY),
", not as the approved " + "the broadcast transfer goes to " + recipient,
JSON.stringify(shown), );
); // 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); const summary = (
await env.page.click("#btn-success-tx-done"); await env.page.locator("#wait-tx-summary").innerText()
await visible(env.page, "#view-address"); ).trim();
env.routeOpts.seedReceipt = false; 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) => { 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 // 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;
assert( // The count is asserted as soon as the send has ended either way, and
env.routeOpts.broadcastTransactions.length === before, // the screen only after it: a transfer that got through shows as the
"a transfer encoded against a contract that contradicts the " + // wait for its receipt, never as the failure screen expected here.
"confirmation screen still reached the RPC (#305)", 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 = ( const message = (
await env.page.locator("#error-tx-message").innerText() await env.page.locator("#error-tx-message").innerText()
).trim(); ).trim();
console.log( console.log(
"# erc-20 decimals refusal: displayed=" + "# erc-20 decimals refusal: displayed=" +
JSON.stringify(shown) + JSON.stringify(shown) +
" contract=" + " contract=" +
LYING_DECIMALS + LYING_DECIMALS +
" message=" + " message=" +
JSON.stringify(message), JSON.stringify(message),
); );
assert( assert(
message.includes("reports " + LYING_DECIMALS + " decimal places") && message.includes("reports " + LYING_DECIMALS + " decimal places") &&
message.includes("displayed using " + STUB_TOKEN.decimals), message.includes("displayed using " + STUB_TOKEN.decimals),
"the refusal does not name both scales it is refusing over: " + "the refusal does not name both scales it is refusing over: " +
JSON.stringify(message), JSON.stringify(message),
); );
assert( assert(
/^[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)
@@ -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), "# stub token symbol() now returns: " + JSON.stringify(HOSTILE_SYMBOL),
); );
// Close and reopen so the refresh that runs on open fetches balances try {
// with the hostile symbol in them. // Close and reopen so the refresh that runs on open fetches balances
await reopenPopup(env, "address"); // with the hostile symbol in them.
await env.page.waitForFunction( await reopenPopup(env, "address");
(addr) => await env.page.waitForFunction(
!!document.querySelector( (addr) =>
'#address-balances [data-token="' + addr + '"]', !!document.querySelector(
), '#address-balances [data-token="' + addr + '"]',
STUB_TOKEN.address, ),
{ timeout: 60000 }, STUB_TOKEN.address,
); { timeout: 60000 },
const onAddress = await env.page.evaluate(() => ({ );
iframes: document.querySelectorAll("iframe").length, const onAddress = await env.page.evaluate(() => ({
pwnPresent: !!document.getElementById("pwn"), iframes: document.querySelectorAll("iframe").length,
})); pwnPresent: !!document.getElementById("pwn"),
console.log("# address-detail iframes = " + onAddress.iframes); }));
assert( console.log("# address-detail iframes = " + onAddress.iframes);
onAddress.iframes === 0 && !onAddress.pwnPresent, assert(
"the address screen contains " + onAddress.iframes === 0 && !onAddress.pwnPresent,
onAddress.iframes + "the address screen contains " +
" iframe(s) after a hostile symbol rendered (#307)", onAddress.iframes +
); " iframe(s) after a hostile symbol rendered (#307)",
);
await env.page.click("#btn-address-back"); await env.page.click("#btn-address-back");
await visible(env.page, "#view-main"); await visible(env.page, "#view-main");
await visible( await visible(
env.page, env.page,
'#wallet-list [data-token="' + STUB_TOKEN.address + '"]', '#wallet-list [data-token="' + STUB_TOKEN.address + '"]',
60000, 60000,
); );
const st = await hostileSymbolState(env.page, STUB_TOKEN.address); const st = await hostileSymbolState(env.page, STUB_TOKEN.address);
console.log( console.log(
"# iframes in the popup DOM = " + "# iframes in the popup DOM = " +
st.iframes + st.iframes +
" | #pwn present = " + " | #pwn present = " +
st.pwnPresent + st.pwnPresent +
" | symbol = " + " | symbol = " +
JSON.stringify(st.symbolText), JSON.stringify(st.symbolText),
); );
assert(st.rowFound, "the hostile token never rendered a row at all"); assert(st.rowFound, "the hostile token never rendered a row at all");
assert( assert(
st.iframes === 0, st.iframes === 0,
"the popup DOM contains " + st.iframes + " iframe(s) (#307)", "the popup DOM contains " + st.iframes + " iframe(s) (#307)",
); );
assert(!st.pwnPresent, "the injected #pwn element is in the popup DOM"); assert(!st.pwnPresent, "the injected #pwn element is in the popup DOM");
assert( assert(
st.symbolElementChildren === 0, st.symbolElementChildren === 0,
"the symbol span grew " + "the symbol span grew " +
st.symbolElementChildren + st.symbolElementChildren +
" element children out of a token symbol (#307)", " element children out of a token symbol (#307)",
); );
assert( assert(
st.symbolText === HOSTILE_SYMBOL_DISPLAYED, st.symbolText === HOSTILE_SYMBOL_DISPLAYED,
"the symbol did not render as the literal capped text " + "the symbol did not render as the literal capped text " +
JSON.stringify(HOSTILE_SYMBOL_DISPLAYED) + JSON.stringify(HOSTILE_SYMBOL_DISPLAYED) +
": " + ": " +
JSON.stringify(st.symbolText), JSON.stringify(st.symbolText),
); );
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.
env.routeOpts.tokenSymbolOverride = null;
}
// Put the fixture back before the next test reads it, and let the // Let the stored balances be rewritten with the honest symbol.
// stored balances be rewritten with the honest symbol.
env.routeOpts.tokenSymbolOverride = null;
await reopenPopup(env, "main"); await reopenPopup(env, "main");
await env.page.waitForFunction( await env.page.waitForFunction(
(addr) => { (addr) => {