Compare commits

...
1 Commits
Author SHA1 Message Date
sneak 30b59325fa test: the e2e suite waits for each save before it closes the popup (closes #446)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
The Settings round trip switched the theme and the network and closed
the popup at once. A close before the change handler's save lands loses
the switch, and the suite then ran on Sepolia.

tests/e2e/run.js now has one helper that polls a field of the stored
record until it holds the expected value, in place of the wait that
only read viewStack. Each Settings switch and spam-filter toggle waits
for its save, the recovery-phrase reopen waits for its saved view, and
reopenPopup() waits until the view it expects to reopen on is the saved
one.

Model: opus-5-5
2026-10-05 01:47:00 +00:00
2 changed files with 116 additions and 32 deletions
+10
View File
@@ -45,6 +45,16 @@ but the review is broader than any of them.
# Completed Steps # Completed Steps
- 2026-10-05: The e2e suite waits for a save to land before it closes the popup
([#446](https://git.eeqj.de/sneak/AutistMask/issues/446)). The Settings round
trip switched the theme and the network and closed the popup at once, and a
close before the save lands loses the switch; with the network left on
Sepolia, a dozen later tests failed too. Each Settings switch and spam-filter
toggle is now waited for in storage before the close, and `reopenPopup()`
waits until the view it expects to reopen on is the saved one. A change made
while an earlier save from the same page is still running is lost even without
a close; that is [#448](https://git.eeqj.de/sneak/AutistMask/issues/448).
- 2026-10-05: The lost-password delete confirmation refuses an empty field and - 2026-10-05: The lost-password delete confirmation refuses an empty field and
ignores characters that paint nothing ignores characters that paint nothing
([#336](https://git.eeqj.de/sneak/AutistMask/issues/336)). A wallet named only ([#336](https://git.eeqj.de/sneak/AutistMask/issues/336)). A wallet named only
+106 -32
View File
@@ -204,40 +204,51 @@ async function goHome(page) {
await visible(page, "#view-main"); await visible(page, "#view-main");
} }
// The navigation stack as it was actually persisted, read out of extension // One field of the popup's state as it was actually persisted, read out of
// storage rather than inferred from which screen is showing. A stale entry // extension storage rather than inferred from what is on screen.
// left behind by a forward navigation that threw is invisible on screen function persistedField(page, field) {
// 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( return page.evaluate(
() => (key) =>
new Promise((resolve) => { new Promise((resolve) => {
chrome.storage.local.get("autistmask", (r) => { chrome.storage.local.get("autistmask", (r) => {
resolve((r.autistmask && r.autistmask.viewStack) || []); resolve(r.autistmask ? r.autistmask[key] : undefined);
}); });
}), }),
field,
); );
} }
// saveState() is fired from showView() without being awaited, so the write // The navigation stack as it was actually persisted. A stale entry left
// lands shortly after the screen does. Polling for the expected stack keeps // behind by a forward navigation that threw is invisible on screen until
// that race out of the assertion; a stack that never becomes the expected // the user presses Back one time too many — which is exactly the
// one fails with what it actually was. // second-order damage #150 did — so the stack itself is what gets asserted.
const VIEW_STACK_SETTLE_MS = 5000; async function persistedViewStack(page) {
return (await persistedField(page, "viewStack")) || [];
}
async function waitForViewStack(page, expected, where) { // Nothing here can await the popup's saves. showView() fires saveState()
// without awaiting it, and a Settings control's "change" handler awaits its
// save only after click() or selectOption() has already returned. So the
// write lands shortly after the screen or the control changes, and a popup
// closed before then loses it. Polling for the expected value keeps that
// race out of the assertion and out of the close; a value that never
// arrives fails with what it actually was.
const SAVE_SETTLE_MS = 5000;
async function waitForPersisted(page, field, expected, where) {
const want = JSON.stringify(expected); const want = JSON.stringify(expected);
const deadline = Date.now() + VIEW_STACK_SETTLE_MS; const deadline = Date.now() + SAVE_SETTLE_MS;
let seen; let seen;
for (;;) { for (;;) {
seen = await persistedViewStack(page); seen = await persistedField(page, field);
if (JSON.stringify(seen) === want) return; if (JSON.stringify(seen) === want) return;
if (Date.now() >= deadline) break; if (Date.now() >= deadline) break;
await sleep(50); await sleep(50);
} }
throw new Error( throw new Error(
"navigation stack " + "the persisted " +
field +
" " +
where + where +
" is " + " is " +
JSON.stringify(seen) + JSON.stringify(seen) +
@@ -257,12 +268,18 @@ test("Back from Add Token unwinds the stack exactly once (#150)", async (env) =>
await env.page.locator("#wallet-list .btn-addr-info").first().click(); await env.page.locator("#wallet-list .btn-addr-info").first().click();
await visible(env.page, "#view-address"); await visible(env.page, "#view-address");
await waitForViewStack(env.page, base.concat("main"), "on address detail"); await waitForPersisted(
env.page,
"viewStack",
base.concat("main"),
"on address detail",
);
await env.page.click("#btn-add-token"); await env.page.click("#btn-add-token");
await visible(env.page, "#view-add-token"); await visible(env.page, "#view-add-token");
await waitForViewStack( await waitForPersisted(
env.page, env.page,
"viewStack",
base.concat("main", "address"), base.concat("main", "address"),
"on the add token screen", "on the add token screen",
); );
@@ -273,15 +290,16 @@ test("Back from Add Token unwinds the stack exactly once (#150)", async (env) =>
!(await env.page.isVisible("#view-add-token")), !(await env.page.isVisible("#view-add-token")),
"the add token screen is still showing after Back", "the add token screen is still showing after Back",
); );
await waitForViewStack( await waitForPersisted(
env.page, env.page,
"viewStack",
base.concat("main"), base.concat("main"),
"after Back from add token", "after Back from add token",
); );
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 waitForViewStack(env.page, base, "after a second Back"); await waitForPersisted(env.page, "viewStack", base, "after a second Back");
}); });
test("a common-token quick-pick fills in the contract address (#150)", async (env) => { test("a common-token quick-pick fills in the contract address (#150)", async (env) => {
@@ -679,6 +697,12 @@ test("reopening the popup never lands on the phrase screen (#161)", async (env)
await openPhraseScreen(env.page); await openPhraseScreen(env.page);
await revealPhrase(env.page); await revealPhrase(env.page);
await waitForPersisted(
env.page,
"currentView",
"show-phrase",
"before closing the popup",
);
await env.page.close(); await env.page.close();
env.page = await openPopup(env.ctx, env.popupUrl); env.page = await openPopup(env.ctx, env.popupUrl);
await visible(env.page, "#view-main"); await visible(env.page, "#view-main");
@@ -714,10 +738,20 @@ function addressScreenState(page) {
// Close and reopen the page rather than reload it: that is what the toolbar // Close and reopen the page rather than reload it: that is what the toolbar
// popup does, and it is the only thing that produces the unrendered views. // popup does, and it is the only thing that produces the unrendered views.
async function reopenPopup(env, restoredView) { // The popup reopens on the view it last saved, so the close waits until
// `view` is the one saved. That wait cannot see a save that leaves the value
// as it was: a caller whose popup already had `view` saved waits for a
// screen in between first.
async function reopenPopup(env, view) {
await waitForPersisted(
env.page,
"currentView",
view,
"before closing the popup",
);
await env.page.close(); await env.page.close();
env.page = await openPopup(env.ctx, env.popupUrl); env.page = await openPopup(env.ctx, env.popupUrl);
await visible(env.page, restoredView); await visible(env.page, "#view-" + view);
} }
// The reproduction from the issue, step for step. // The reproduction from the issue, step for step.
@@ -732,7 +766,7 @@ test("Back after reopening the popup renders the address screen (#268)", async (
await env.page.click("#btn-settings"); await env.page.click("#btn-settings");
await visible(env.page, "#view-settings"); await visible(env.page, "#view-settings");
await reopenPopup(env, "#view-settings"); await reopenPopup(env, "settings");
await env.page.click("#btn-settings-back"); await env.page.click("#btn-settings-back");
await visible(env.page, "#view-address"); await visible(env.page, "#view-address");
@@ -789,10 +823,18 @@ test("Back after reopening the popup renders the Receive screen (#268)", async (
JSON.stringify(before.address), JSON.stringify(before.address),
); );
// The test above left `settings` saved too, so reopenPopup() could not
// tell this page's save of it from that one without a save in between.
await waitForPersisted(
env.page,
"currentView",
"receive",
"on the Receive screen",
);
await env.page.click("#btn-settings"); await env.page.click("#btn-settings");
await visible(env.page, "#view-settings"); await visible(env.page, "#view-settings");
await reopenPopup(env, "#view-settings"); await reopenPopup(env, "settings");
await env.page.click("#btn-settings-back"); await env.page.click("#btn-settings-back");
await visible(env.page, "#view-receive"); await visible(env.page, "#view-receive");
@@ -1175,11 +1217,24 @@ const NONDEFAULT_NETWORK = "sepolia";
test("the theme and network selectors carry a non-default persisted value (#229)", async (env) => { test("the theme and network selectors carry a non-default persisted value (#229)", async (env) => {
await openSettings(env.page); await openSettings(env.page);
// selectOption() fires "change", which is what the handlers bind. // selectOption() fires "change", which is what the handlers bind. It
// returns before the handler's save lands, hence each wait.
await env.page.selectOption("#settings-theme", NONDEFAULT_THEME); await env.page.selectOption("#settings-theme", NONDEFAULT_THEME);
await waitForPersisted(
env.page,
"theme",
NONDEFAULT_THEME,
"after the switch",
);
await env.page.selectOption("#settings-network", NONDEFAULT_NETWORK); await env.page.selectOption("#settings-network", NONDEFAULT_NETWORK);
await waitForPersisted(
env.page,
"networkId",
NONDEFAULT_NETWORK,
"after the switch",
);
await reopenPopup(env, "#view-settings"); await reopenPopup(env, "settings");
assertSelectors( assertSelectors(
await selectorValues(env.page), await selectorValues(env.page),
@@ -1198,9 +1253,16 @@ test("the theme and network selectors carry a non-default persisted value (#229)
// fixture never customised and so are the mainnet defaults // fixture never customised and so are the mainnet defaults
// src/shared/state.js starts with. // src/shared/state.js starts with.
await env.page.selectOption("#settings-theme", "system"); await env.page.selectOption("#settings-theme", "system");
await waitForPersisted(env.page, "theme", "system", "after the restore");
await env.page.selectOption("#settings-network", "mainnet"); await env.page.selectOption("#settings-network", "mainnet");
await waitForPersisted(
env.page,
"networkId",
"mainnet",
"after the restore",
);
await reopenPopup(env, "#view-settings"); await reopenPopup(env, "settings");
assertSelectors( assertSelectors(
await selectorValues(env.page), await selectorValues(env.page),
@@ -1225,8 +1287,14 @@ test("a spam filter toggled in Settings survives a popup reopen (#229)", async (
immediately.checked === false, immediately.checked === false,
"clicking #" + TOGGLED_FILTER + " did not clear it", "clicking #" + TOGGLED_FILTER + " did not clear it",
); );
await waitForPersisted(
env.page,
"hideDustTransactions",
false,
"after clearing #" + TOGGLED_FILTER,
);
await reopenPopup(env, "#view-settings"); await reopenPopup(env, "settings");
const after = await checkboxStates(env.page); const after = await checkboxStates(env.page);
for (const { id } of SPAM_FILTER_CHECKBOXES) { for (const { id } of SPAM_FILTER_CHECKBOXES) {
@@ -1243,8 +1311,14 @@ test("a spam filter toggled in Settings survives a popup reopen (#229)", async (
test("turning the same filter back on survives a reopen too (#229)", async (env) => { test("turning the same filter back on survives a reopen too (#229)", async (env) => {
await openSettings(env.page); await openSettings(env.page);
await env.page.click("#" + TOGGLED_FILTER); await env.page.click("#" + TOGGLED_FILTER);
await waitForPersisted(
env.page,
"hideDustTransactions",
true,
"after setting #" + TOGGLED_FILTER + " again",
);
await reopenPopup(env, "#view-settings"); await reopenPopup(env, "settings");
// Restores the fixture the later sections inherit, and rules out a // Restores the fixture the later sections inherit, and rules out a
// checkbox that persists "off" only because it is stuck there. // checkbox that persists "off" only because it is stuck there.
@@ -2365,7 +2439,7 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
// Close and reopen so the refresh that runs on open fetches balances // Close and reopen so the refresh that runs on open fetches balances
// with the hostile symbol in them. // with the hostile symbol in them.
await reopenPopup(env, "#view-address"); await reopenPopup(env, "address");
await env.page.waitForFunction( await env.page.waitForFunction(
(addr) => (addr) =>
!!document.querySelector( !!document.querySelector(
@@ -2431,7 +2505,7 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
// Put the fixture back before the next test reads it, and let the // Put the fixture back before the next test reads it, and let the
// stored balances be rewritten with the honest symbol. // stored balances be rewritten with the honest symbol.
env.routeOpts.tokenSymbolOverride = null; env.routeOpts.tokenSymbolOverride = null;
await reopenPopup(env, "#view-main"); await reopenPopup(env, "main");
await env.page.waitForFunction( await env.page.waitForFunction(
(addr) => { (addr) => {
const row = document.querySelector( const row = document.querySelector(