test: the e2e suite waits for each save before it closes the popup (closes #446)
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
This commit is contained in:
@@ -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
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user