test: assert the #150 and #151 items the harness did not cover (closes #188)
All checks were successful
check / check (push) Successful in 33s
All checks were successful
check / check (push) Successful in 33s
The suite asserted that the two screens those issues broke now open without throwing, which is narrower than their definition of done. The four remaining items are asserted here, additively; nothing existing was restructured. Back navigation out of Add Token is checked against the persisted navigation stack, read from extension storage, as a delta: the round trip Home -> AddressDetail -> AddToken -> Back -> Back must leave the stack exactly as it found it. A stale entry is invisible on screen until the user presses Back one time too many, which is precisely the second-order damage of #150, so the stack rather than the visible view is what gets asserted. Stating it as a delta keeps it independent of whatever depth earlier tests leave behind. The quick-pick test clicks a button and requires the address field to hold that button's contract address; the old assertion only counted the buttons rendered. The native ETH detail path needed a fixture: the normal-transactions endpoint answered with an empty list unconditionally, so there was no non-ERC-20 row to open at all. seedNativeTransfer serves one, and the detail screen must show the native type, the value, the raw wei quantity and no token contract row - the row whose branch is where a regression of the non-ERC-20 case would land. Tap-to-copy reads the real clipboard back rather than watching the handler run, after seeding a sentinel so an untouched clipboard cannot pass. Clipboard permissions are granted context-wide because an origin-scoped grant is refused for chrome-extension: URLs. Each of the four was demonstrated failing against a deliberately broken build; the captured output is in the pull request.
This commit is contained in:
261
tests/e2e/run.js
261
tests/e2e/run.js
@@ -36,6 +36,8 @@ const {
|
||||
FEE_ESTIMATE_WEI,
|
||||
FEE_RESERVE_WEI,
|
||||
STUB_COUNTERPARTY,
|
||||
STUB_NATIVE_TX_HASH,
|
||||
STUB_NATIVE_VALUE_WEI,
|
||||
STUB_TOKEN,
|
||||
STUB_TX_HASH,
|
||||
} = require("./network");
|
||||
@@ -169,6 +171,264 @@ test("transaction detail renders an ERC-20 transfer (#151)", async (env) => {
|
||||
assert(dots > 0, "token contract row rendered without its colour dot");
|
||||
});
|
||||
|
||||
// --------------------- the rest of the #150 and #151 definition of done
|
||||
//
|
||||
// The two tests above assert that the screens #150 and #151 broke now open
|
||||
// without throwing, which is narrower than what those issues asked for.
|
||||
// The four items below are the remainder (#188): the navigation stack out
|
||||
// of Add Token, the quick-pick actually populating the field, the native
|
||||
// ETH detail path the ERC-20 fix could have regressed, and tap-to-copy.
|
||||
|
||||
// Leave the transaction detail screen for the address screen it was opened
|
||||
// from. The two tests above finish on it, and so does the last test here.
|
||||
async function leaveTransactionDetail(page) {
|
||||
if (await page.isVisible("#view-transaction")) {
|
||||
await page.click("#btn-tx-back");
|
||||
}
|
||||
await openAddressDetail(page);
|
||||
}
|
||||
|
||||
// Back out to Home from wherever the previous test finished.
|
||||
async function goHome(page) {
|
||||
await leaveTransactionDetail(page);
|
||||
await page.click("#btn-address-back");
|
||||
await visible(page, "#view-main");
|
||||
}
|
||||
|
||||
// The navigation stack as it was actually persisted, read out of extension
|
||||
// storage rather than inferred from which screen is showing. A stale entry
|
||||
// left behind by a forward navigation that threw is invisible on screen
|
||||
// until the user presses Back one time too many — which is exactly the
|
||||
// second-order damage #150 did — so the stack itself is what gets asserted.
|
||||
function persistedViewStack(page) {
|
||||
return page.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
chrome.storage.local.get("autistmask", (r) => {
|
||||
resolve((r.autistmask && r.autistmask.viewStack) || []);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// saveState() is fired from showView() without being awaited, so the write
|
||||
// lands shortly after the screen does. Polling for the expected stack keeps
|
||||
// that race out of the assertion; a stack that never becomes the expected
|
||||
// one fails with what it actually was.
|
||||
const VIEW_STACK_SETTLE_MS = 5000;
|
||||
|
||||
async function waitForViewStack(page, expected, where) {
|
||||
const want = JSON.stringify(expected);
|
||||
const deadline = Date.now() + VIEW_STACK_SETTLE_MS;
|
||||
let seen;
|
||||
for (;;) {
|
||||
seen = await persistedViewStack(page);
|
||||
if (JSON.stringify(seen) === want) return;
|
||||
if (Date.now() >= deadline) break;
|
||||
await sleep(50);
|
||||
}
|
||||
throw new Error(
|
||||
"navigation stack " +
|
||||
where +
|
||||
" is " +
|
||||
JSON.stringify(seen) +
|
||||
", expected " +
|
||||
want,
|
||||
);
|
||||
}
|
||||
|
||||
// The invariant is stated as a delta against whatever the earlier tests
|
||||
// left on the stack, not as an absolute: a round trip into Add Token and
|
||||
// back out must leave the stack exactly as it found it. That is what "no
|
||||
// duplicated or orphaned stack entry" means, and it holds whatever the
|
||||
// starting depth is.
|
||||
test("Back from Add Token unwinds the stack exactly once (#150)", 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 waitForViewStack(env.page, base.concat("main"), "on address detail");
|
||||
|
||||
await env.page.click("#btn-add-token");
|
||||
await visible(env.page, "#view-add-token");
|
||||
await waitForViewStack(
|
||||
env.page,
|
||||
base.concat("main", "address"),
|
||||
"on the add token screen",
|
||||
);
|
||||
|
||||
await env.page.click("#btn-add-token-back");
|
||||
await visible(env.page, "#view-address");
|
||||
assert(
|
||||
!(await env.page.isVisible("#view-add-token")),
|
||||
"the add token screen is still showing after Back",
|
||||
);
|
||||
await waitForViewStack(
|
||||
env.page,
|
||||
base.concat("main"),
|
||||
"after Back from add token",
|
||||
);
|
||||
|
||||
await env.page.click("#btn-address-back");
|
||||
await visible(env.page, "#view-main");
|
||||
await waitForViewStack(env.page, base, "after a second Back");
|
||||
});
|
||||
|
||||
test("a common-token quick-pick fills in the contract address (#150)", async (env) => {
|
||||
await openAddressDetail(env.page);
|
||||
await env.page.click("#btn-add-token");
|
||||
await visible(env.page, "#view-add-token");
|
||||
|
||||
const before = await env.page.inputValue("#add-token-address");
|
||||
assert(
|
||||
before === "",
|
||||
"the add token screen opened with the address field already filled: " +
|
||||
JSON.stringify(before),
|
||||
);
|
||||
|
||||
const pick = env.page.locator("#common-token-list .common-token").first();
|
||||
const wanted = await pick.getAttribute("data-address");
|
||||
assert(
|
||||
/^0x[0-9a-fA-F]{40}$/.test(wanted || ""),
|
||||
"the first quick-pick button carries no contract address: " +
|
||||
JSON.stringify(wanted),
|
||||
);
|
||||
|
||||
await pick.click();
|
||||
const after = await env.page.inputValue("#add-token-address");
|
||||
assert(
|
||||
after === wanted,
|
||||
"clicking the " +
|
||||
(await pick.innerText()).trim() +
|
||||
" quick-pick left the address field as " +
|
||||
JSON.stringify(after) +
|
||||
", expected " +
|
||||
JSON.stringify(wanted),
|
||||
);
|
||||
|
||||
await env.page.click("#btn-add-token-back");
|
||||
await visible(env.page, "#view-address");
|
||||
});
|
||||
|
||||
// The native amount as the transaction list writes it (four decimals) and
|
||||
// as the detail screen writes it (full precision). Both are rendered here
|
||||
// from the fixture rather than read off the screen, so the assertions
|
||||
// compare against the wei the stub served.
|
||||
const NATIVE_ROW_TEXT =
|
||||
parseFloat(formatEther(STUB_NATIVE_VALUE_WEI)).toFixed(4) + " ETH";
|
||||
const NATIVE_DETAIL_TEXT = formatEther(STUB_NATIVE_VALUE_WEI) + " ETH";
|
||||
|
||||
test("the native ETH transaction detail still renders (#151)", async (env) => {
|
||||
// The ERC-20 fix could only have regressed this path by making the
|
||||
// token-contract branch run for a transfer that has no contract, so
|
||||
// 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);
|
||||
|
||||
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 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 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;
|
||||
});
|
||||
|
||||
test("tap-to-copy on the transaction detail screen copies the address (#151)", async (env) => {
|
||||
// Read the clipboard back rather than watching the handler run: what
|
||||
// #151 asks for is the address reaching the clipboard, and a spy on
|
||||
// navigator.clipboard would assert the call and not the effect.
|
||||
//
|
||||
// Granted context-wide rather than for the popup's origin: an
|
||||
// origin-scoped grant is refused for chrome-extension: URLs, which
|
||||
// both Playwright and Chrome treat as opaque here.
|
||||
await env.ctx.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
|
||||
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");
|
||||
|
||||
// Seed a sentinel first, so a clipboard that nothing writes to cannot
|
||||
// pass on whatever was left in it.
|
||||
const SENTINEL = "e2e-clipboard-untouched";
|
||||
await env.page.evaluate((s) => navigator.clipboard.writeText(s), SENTINEL);
|
||||
const seeded = await env.page.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
assert(
|
||||
seeded === SENTINEL,
|
||||
"the harness could not seed the clipboard, so the assertion below " +
|
||||
"would prove nothing; it read back " +
|
||||
JSON.stringify(seeded),
|
||||
);
|
||||
|
||||
await env.page.locator("#tx-detail-token-contract [data-copy]").click();
|
||||
|
||||
const copied = await env.page.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
assert(
|
||||
copied.toLowerCase() === STUB_TOKEN.address,
|
||||
"tapping the token contract address put " +
|
||||
JSON.stringify(copied) +
|
||||
" on the clipboard, expected " +
|
||||
STUB_TOKEN.address,
|
||||
);
|
||||
|
||||
const flash = await env.page.locator("#flash-msg").innerText();
|
||||
assert(
|
||||
flash.trim() === "Copied!",
|
||||
"the copy gave no confirmation, flash line reads " +
|
||||
JSON.stringify(flash),
|
||||
);
|
||||
});
|
||||
|
||||
// -------------------------------------------- recovery phrase (#161)
|
||||
|
||||
// The gear toggles, so pressing it while Settings is already up leaves it.
|
||||
@@ -2371,6 +2631,7 @@ async function main() {
|
||||
// starting state of a run is readable without hunting through tests.
|
||||
const routeOpts = {
|
||||
seedTokenTransfer: false,
|
||||
seedNativeTransfer: false,
|
||||
seedTokenBalance: false,
|
||||
ethBalanceWei: null,
|
||||
failGasEstimate: false,
|
||||
|
||||
Reference in New Issue
Block a user