test: a failing e2e test leaves no fixture switch or send screen behind (closes #318)
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:
@@ -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: Dead code removed and copied view helpers shared
|
- 2026-10-05: Dead code removed and copied view helpers shared
|
||||||
([#168](https://git.eeqj.de/sneak/AutistMask/issues/168)). AddressDetail and
|
([#168](https://git.eeqj.de/sneak/AutistMask/issues/168)). AddressDetail and
|
||||||
AddressToken each defined their own `isoDate()` and `timeAgo()` in place of
|
AddressToken each defined their own `isoDate()` and `timeAgo()` in place of
|
||||||
|
|||||||
@@ -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
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user