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 2s
e2e / e2e-firefox (push) Failing after 3s

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 stops the ten-second refresh, runs the
leave cleanup of the current screen, and raises the recovery screen. From then
on showView() shows nothing else in that popup, so a transaction wait or a later
save cannot take the user off it or clear an export or a typed confirmation.
That is held in memory, never as the saved current view, so a popup opened
after the record is erased elsewhere opens normally. 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 21:43:53 +00:00
parent 6127fd9432
commit d732f1aafe
7 changed files with 297 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 = {