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