test: assert the #150 and #151 items the harness did not cover (closes #188)
All checks were successful
check / check (push) Successful in 27s
All checks were successful
check / check (push) Successful in 27s
Covers the four DoD items #188 scoped: the AddToken quick-pick populating the address field, Back out of AddToken unwinding the persisted navigation stack exactly once, a native ETH transfer rendering in TransactionDetail with no token contract row, and tap-to-copy reading the real clipboard back. The native path needed a fixture: the Blockscout normal-transactions endpoint answered [] unconditionally, so there was no non-ERC-20 row to open. Each assertion demonstrated to discriminate by mutation, one break at a time with the pre-existing tests staying green: a double viewStack push, a no-op quick-pick handler, an un-hidden token contract row, the ERC-20 branch forced onto a native transaction, and a dropped clipboard write each turn exactly the corresponding test red and no other. Four further DoD items from #150 and #151 remain outside this scope and are tracked in #295.
This commit was merged in pull request #288.
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