Compare commits

...
1 Commits
Author SHA1 Message Date
sneak 7de9dd26fe 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
2026-10-05 06:38:15 +00: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
- 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
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
+7 -5
View File
@@ -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.
+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
// 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) => {