test: drive the StateRecovery screen in both browser suites #463

Merged
clawbot merged 1 commits from issue-361-state-recovery-e2e into next 2026-10-05 11:43:08 +02:00
4 changed files with 265 additions and 6 deletions
Showing only changes of commit 46f9cbaeae - Show all commits
+10 -4
View File
@@ -342,6 +342,11 @@ fixtures in `tests/e2e/network.js`, so the run is deterministic and fully
offline; unrecognised outbound requests are reported as failures rather than
silently allowed.
It also covers the StateRecovery screen, under the shipped CSP: a stored record
this build cannot read opens the popup on it, its export text box holds that
record exactly as stored, a near-miss confirmation phrase erases nothing, and
the exact one erases the record and reloads into Welcome.
It also covers the **Settings screen**, which holds the densest run of element
id lookups in the codebase and where one wrong id leaves the whole popup blank
rather than only degrading Settings: that the screen renders populated — the
@@ -464,10 +469,11 @@ Chrome that ever changes this fails the run instead of passing it.
`make test-e2e-firefox` builds `dist/firefox/` and drives the **real popup in a
real Firefox**, installed as an unpacked MV2 temporary add-on via geckodriver.
It covers popup load, wallet creation through the UI, the Add Token screen, and
the four dApp round trips — `eth_requestAccounts`, `personal_sign`,
`eth_sendTransaction`, and a closed approval window rejecting with EIP-1193 4001
— driven through the real content script, background page and approval windows.
It covers popup load, the StateRecovery screen (the same cases as the Chrome
suite), wallet creation through the UI, the Add Token screen, and the four dApp
round trips — `eth_requestAccounts`, `personal_sign`, `eth_sendTransaction`, and
a closed approval window rejecting with EIP-1193 4001 — driven through the real
content script, background page and approval windows.
The suite lives in `tests/e2e/firefox/`. Its WebDriver client (`driver.js`) has
**no npm dependencies at all**: it is built on global `fetch` and
+10
View File
@@ -45,6 +45,16 @@ but the review is broader than any of them.
# Completed Steps
- 2026-10-05: The StateRecovery screen is driven in a real browser under the
shipped CSP, in both end-to-end suites
([#361](https://git.eeqj.de/sneak/AutistMask/issues/361)). A stored record
this build cannot read opens the popup on it; its export text box holds the
record exactly as stored; a near-miss confirmation phrase erases nothing; and
the exact phrase erases the record and reloads into Welcome. The cases run
before any wallet exists: with no wallet nothing saves on a timer, so no save
can write a good record over the unreadable one, and the erase leaves the
popup on Welcome for wallet creation.
- 2026-10-05: Escaping in the popup's views follows its own rule with no
exceptions ([#329](https://git.eeqj.de/sneak/AutistMask/issues/329)). The
decimals and holder count on a token's screen, and every USD figure (the ETH
+122
View File
@@ -46,6 +46,7 @@
const fs = require("fs");
const path = require("path");
const { isDeepStrictEqual } = require("util");
const {
Transaction,
@@ -62,6 +63,10 @@ const {
const { ConsoleErrors, EXTENSION_ORIGIN, start, sleep } = require("./driver");
const { startDappServer } = require("./dapp");
const { STUB_COUNTERPARTY } = require("../network");
const {
STATE_SCHEMA_VERSION,
stateProblem,
} = require("../../../src/shared/stateSchema");
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
const POPUP_URL = EXTENSION_ORIGIN + "/src/popup/index.html";
@@ -122,6 +127,123 @@ step("the popup is drawn in the monospace font it declares", async (env) => {
);
});
// The recovery screen (#361): the Chrome suite's four cases, run before any
// wallet exists for the same reason. With no wallet nothing saves on a timer,
// so no save can write a good record over the unreadable one. The last of them
// erases it, which leaves the popup on Welcome for wallet creation.
// A profile a newer build wrote: a wallet with its encrypted secret, under a
// schema version this build refuses to read.
const UNREADABLE_RECORD = {
schemaVersion: STATE_SCHEMA_VERSION + 1,
wallets: [
{
type: "hd",
name: "Main",
xpub: "xpub-written-by-a-newer-build",
encryptedSecret: "ciphertext-written-by-a-newer-build",
nextIndex: 1,
addresses: [{ address: STUB_COUNTERPARTY }],
},
],
};
// The whole stored record, read on the popup page.
function storedRecord(d) {
return d.executeAsync(
`const done = arguments[arguments.length - 1];
browser.storage.local.get("autistmask").then((r) => done(r.autistmask));`,
);
}
step(
"an unreadable stored record opens the popup on the recovery screen",
async (env) => {
const d = env.driver;
// The popup the first step opened saves once, as it shows Welcome.
// Stored before that save lands, the record would be written over.
const deadline = Date.now() + 15000;
for (;;) {
const stored = await storedRecord(d);
if (stored && stored.currentView === "welcome") break;
assert(
Date.now() < deadline,
"the Welcome screen's save never landed: " +
JSON.stringify(stored),
);
await sleep(100);
}
await d.executeAsync(
`const done = arguments[arguments.length - 1];
browser.storage.local.set({ autistmask: arguments[0] }).then(() => done());`,
[UNREADABLE_RECORD],
);
await d.navigate(POPUP_URL);
await d.waitVisible("#view-state-recovery");
const problem = await d.text("#state-recovery-problem");
assert(
problem === stateProblem(UNREADABLE_RECORD),
"the recovery screen names the problem as " +
JSON.stringify(problem),
);
},
);
step("Export Saved Data shows the stored record verbatim", async (env) => {
const d = env.driver;
await d.click("#btn-state-recovery-export");
await d.waitVisible("#state-recovery-blob");
const exported = await d.value("#state-recovery-blob");
assert(exported !== "", "Export Saved Data left the text box empty");
assert(
isDeepStrictEqual(JSON.parse(exported), UNREADABLE_RECORD),
"the text box does not hold the stored record: " + exported,
);
});
step("a near-miss confirmation phrase erases nothing", async (env) => {
const d = env.driver;
await d.fill("#state-recovery-reset-input", "ERASE MY WALLETS");
await d.click("#btn-state-recovery-reset");
await d.waitFor(
"the refusal on the error line",
`return document.getElementById("state-recovery-flash").textContent ===
"Type ERASE MY WALLET to confirm. Nothing was erased.";`,
);
const stored = await storedRecord(d);
assert(
isDeepStrictEqual(stored, UNREADABLE_RECORD),
"the stored record changed: " + JSON.stringify(stored),
);
});
step(
"the exact confirmation phrase erases the record and reloads into Welcome",
async (env) => {
const d = env.driver;
try {
await d.fill("#state-recovery-reset-input", "ERASE MY WALLET");
await d.click("#btn-state-recovery-reset");
// Welcome is the proof of the erase: the record still stored
// would put the recovery screen up again, and its wallet would
// open Home.
await d.waitVisible("#view-welcome");
} finally {
// Whatever failed in these four steps, wallet creation starts
// from Welcome. The record left stored would fail every step
// after this.
if (!(await d.isVisible("#view-welcome"))) {
await d.executeAsync(
`const done = arguments[arguments.length - 1];
browser.storage.local.remove("autistmask").then(() => done());`,
);
await d.navigate(POPUP_URL);
}
}
},
);
step("wallet creation through the UI reaches the main view", async (env) => {
const d = env.driver;
await d.click("#btn-welcome-add");
+123 -2
View File
@@ -9,6 +9,8 @@
"use strict";
const { isDeepStrictEqual } = require("util");
const {
Transaction,
formatEther,
@@ -47,6 +49,10 @@ const {
} = require("./network");
const { DUST_THRESHOLD_MESSAGE } = require("../../src/popup/dustThreshold");
const { NETWORKS } = require("../../src/shared/networks");
const {
STATE_SCHEMA_VERSION,
stateProblem,
} = require("../../src/shared/stateSchema");
const TEST_TIMEOUT_MS = 120000;
@@ -115,8 +121,8 @@ test("the popup is drawn in the monospace font it declares (#418)", async (env)
// so a recurrence fails whichever test it lands in rather than being
// tolerated. Since libsodium's WASM module is embedded in the bundle and
// needs no fetch, a realm that compiles WASM is a realm where libsodium
// takes the WASM path, and the next test drives a real vault encryption
// through it.
// takes the WASM path, and wallet creation below drives a real vault
// encryption through it.
test("the popup compiles WebAssembly under the shipped CSP (#182)", async (env) => {
const ok = await pageCompilesWasm(env.page);
assert(
@@ -128,6 +134,121 @@ test("the popup compiles WebAssembly under the shipped CSP (#182)", async (env)
);
});
// The screen the popup shows when it cannot read the stored profile
// (src/popup/views/stateRecovery.js), driven under the shipped CSP (#361).
//
// These run before any wallet exists, on purpose. With no wallet neither the
// popup nor the background refreshes balances, so nothing saves while they run
// and no save can write a good record over the unreadable one. The last of
// them erases it, which leaves the popup on Welcome for wallet creation.
// A profile a newer build wrote: a wallet with its encrypted secret, under a
// schema version this build refuses to read.
const UNREADABLE_RECORD = {
schemaVersion: STATE_SCHEMA_VERSION + 1,
wallets: [
{
type: "hd",
name: "Main",
xpub: "xpub-written-by-a-newer-build",
encryptedSecret: "ciphertext-written-by-a-newer-build",
nextIndex: 1,
addresses: [{ address: STUB_COUNTERPARTY }],
},
],
};
// The whole stored record, read out of extension storage.
function storedRecord(page) {
return page.evaluate(
() =>
new Promise((resolve) => {
chrome.storage.local.get("autistmask", (r) =>
resolve(r.autistmask),
);
}),
);
}
test("an unreadable stored record opens the popup on the recovery screen (#361)", async (env) => {
// The popup the first test opened saves once, as it shows Welcome. Stored
// before that save lands, the record would be written over.
await waitForPersisted(
env.page,
"currentView",
"welcome",
"before the unreadable record is stored",
);
await env.page.evaluate(
(record) =>
new Promise((resolve) => {
chrome.storage.local.set({ autistmask: record }, resolve);
}),
UNREADABLE_RECORD,
);
await env.page.close();
env.errors.expect(
"the recovery screen logging the problem as it goes up",
/state is unusable, showing the recovery screen/,
);
env.page = await openPopup(env.ctx, env.popupUrl);
await visible(env.page, "#view-state-recovery");
const problem = await env.page.textContent("#state-recovery-problem");
assert(
problem === stateProblem(UNREADABLE_RECORD),
"the recovery screen names the problem as " + JSON.stringify(problem),
);
});
test("Export Saved Data shows the stored record verbatim (#361)", async (env) => {
await env.page.click("#btn-state-recovery-export");
await visible(env.page, "#state-recovery-blob");
const exported = await env.page.inputValue("#state-recovery-blob");
assert(exported !== "", "Export Saved Data left the text box empty");
assert(
isDeepStrictEqual(JSON.parse(exported), UNREADABLE_RECORD),
"the text box does not hold the stored record: " + exported,
);
});
test("a near-miss confirmation phrase erases nothing (#361)", async (env) => {
await env.page.fill("#state-recovery-reset-input", "ERASE MY WALLETS");
await env.page.click("#btn-state-recovery-reset");
await env.page.waitForFunction(
() =>
document.getElementById("state-recovery-flash").textContent ===
"Type ERASE MY WALLET to confirm. Nothing was erased.",
);
const stored = await storedRecord(env.page);
assert(
isDeepStrictEqual(stored, UNREADABLE_RECORD),
"the stored record changed: " + JSON.stringify(stored),
);
});
test("the exact confirmation phrase erases the record and reloads into Welcome (#361)", async (env) => {
try {
await env.page.fill("#state-recovery-reset-input", "ERASE MY WALLET");
await env.page.click("#btn-state-recovery-reset");
// Welcome is the proof of the erase: the record still stored would
// put the recovery screen up again, and its wallet would open Home.
await visible(env.page, "#view-welcome");
} finally {
// Whatever failed in these four tests, wallet creation starts from
// Welcome. The record left stored would fail every test after this.
if (!(await env.page.isVisible("#view-welcome"))) {
await env.page.evaluate(
() =>
new Promise((resolve) => {
chrome.storage.local.remove("autistmask", resolve);
}),
);
await env.page.reload();
}
}
});
test("wallet creation through the UI reaches the main view", async (env) => {
env.phrase = await createWallet(env.page);
assert(