fix: an open popup moves to the recovery screen when its profile becomes unreadable (closes #373)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s

A popup already open when the stored profile became unreadable stayed on the
last good profile until reopened. Every save already runs the check loadState()
runs at open; a save refused by it now raises the recovery screen, stops the
ten-second refresh, and passes the screen to showView(), which runs the leave
cleanup of the screen it replaces and records it as the current view. From
then on showView() shows nothing else, so a transaction wait or a later save
cannot take the user off it or clear an export or a typed confirmation. Any
other failed save keeps the "NOT SAVED" banner. The popup test harness now
honours clearInterval().

Model: opus-5-5
This commit is contained in:
2026-10-04 20:21:04 +00:00
parent 375998beaf
commit e791e06fb1
7 changed files with 265 additions and 31 deletions
+26 -5
View File
@@ -40,6 +40,10 @@ jest.doMock("libsodium-wrappers-sumo", () => sodium);
jest.doMock("qrcode", () => QRCode);
jest.doMock("ethereum-blockies-base64", () => makeBlockie);
// Taken before any boot replaces them; see bootPopup().
const realSetInterval = globalThis.setInterval;
const realClearInterval = globalThis.clearInterval;
const POPUP_HTML = fs.readFileSync(
path.join(__dirname, "..", "..", "src", "popup", "index.html"),
"utf8",
@@ -292,9 +296,20 @@ async function bootPopup(stored, options) {
}),
addEventListener: () => {},
};
// The 10s refresh loop init() starts would outlive the test.
const realSetInterval = globalThis.setInterval;
globalThis.setInterval = () => 0;
// The ten-second refresh init() starts, and a transaction wait's timers,
// would outlive the test. So every interval is recorded rather than
// started, clearInterval() removes it as a browser would, and tick() below
// runs the ones still set. Put back by cleanupPopup().
const intervals = new Map();
let lastId = 0;
globalThis.setInterval = (fn) => {
lastId += 1;
intervals.set(lastId, fn);
return lastId;
};
globalThis.clearInterval = (id) => {
intervals.delete(id);
};
require("../../src/popup/index");
@@ -316,8 +331,6 @@ async function bootPopup(stored, options) {
}
await settle();
globalThis.setInterval = realSetInterval;
return {
storage,
document,
@@ -337,6 +350,12 @@ async function bootPopup(stored, options) {
for (const fn of fns) await fn();
await settle();
},
// Every interval still set runs once: the ten-second refresh, and a
// transaction wait's receipt poll and elapsed counter while one runs.
tick: async () => {
for (const fn of intervals.values()) await fn();
await settle();
},
settle,
// The view ids whose section is not hidden, as the audit measured them.
visibleViews: () => {
@@ -354,6 +373,8 @@ function cleanupPopup() {
delete globalThis.chrome;
delete globalThis.document;
delete globalThis.window;
globalThis.setInterval = realSetInterval;
globalThis.clearInterval = realClearInterval;
}
module.exports = {