diff --git a/tests/firstWalletRefresh.test.js b/tests/firstWalletRefresh.test.js index c7aac41..232cb30 100644 --- a/tests/firstWalletRefresh.test.js +++ b/tests/firstWalletRefresh.test.js @@ -61,16 +61,12 @@ async function importOn(page, { tab, field, secret }) { await page.click("btn-add-wallet-confirm"); } -// Fake timers hold the timer of the flash message an import ends with, which -// would otherwise fire after cleanupPopup() has removed the page and throw. beforeEach(() => { - jest.useFakeTimers(); chainBalance = "0.0"; }); afterEach(() => { cleanupPopup(); - jest.useRealTimers(); }); describe("a popup that opened on Welcome", () => { diff --git a/tests/popupBoot.test.js b/tests/popupBoot.test.js new file mode 100644 index 0000000..bbc0067 --- /dev/null +++ b/tests/popupBoot.test.js @@ -0,0 +1,42 @@ +// A timer the popup started and that fires after cleanupPopup() has removed +// the page throws "document is not defined", and the throw fails whatever test +// the jest worker is running by then, in this file or another +// (https://git.eeqj.de/sneak/AutistMask/issues/578). The flash message's +// timer is the one that did it. A throw from a timer during a test fails that +// test, so each test here passes only if nothing throws. + +const { + bootPopup, + cleanupPopup, + unversionedValidProfile, +} = require("./support/popupBoot"); + +const FLASH_MS = 10; + +function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +afterEach(() => { + cleanupPopup(); +}); + +test("a flash message clears while the page is up", async () => { + const page = await bootPopup(unversionedValidProfile()); + const { showFlash } = require("../src/popup/views/helpers"); + showFlash("Copied.", FLASH_MS); + expect(page.text("flash-msg")).toBe("Copied."); + + await wait(FLASH_MS * 5); + + expect(page.text("flash-msg")).toBe(""); +}); + +test("a flash message's timer does not fire after cleanupPopup()", async () => { + await bootPopup(unversionedValidProfile()); + const { showFlash } = require("../src/popup/views/helpers"); + showFlash("Copied.", FLASH_MS); + cleanupPopup(); + + await wait(FLASH_MS * 5); +}); diff --git a/tests/support/popupBoot.js b/tests/support/popupBoot.js index 7d00df4..02f98d0 100644 --- a/tests/support/popupBoot.js +++ b/tests/support/popupBoot.js @@ -43,6 +43,12 @@ jest.doMock("ethereum-blockies-base64", () => makeBlockie); // Taken before any boot replaces them; see bootPopup(). const realSetInterval = globalThis.setInterval; const realClearInterval = globalThis.clearInterval; +const realSetTimeout = globalThis.setTimeout; +const realClearTimeout = globalThis.clearTimeout; + +// Every timeout set since the last cleanupPopup() that has neither fired nor +// been cleared. cleanupPopup() clears them all. +const pendingTimeouts = new Set(); const POPUP_HTML = fs.readFileSync( path.join(__dirname, "..", "..", "src", "popup", "index.html"), @@ -342,6 +348,24 @@ async function bootPopup(stored, options) { globalThis.clearInterval = (id) => { intervals.delete(id); }; + // Timeouts, such as the one that clears a flash message, run for real, but + // each is recorded until it fires or is cleared, so cleanupPopup() can + // clear the ones still pending. One that fired after the page was removed + // would throw "document is not defined" into whatever test the worker runs + // by then. jest.useFakeTimers() in a test does not reach them. Put back by + // cleanupPopup(). + globalThis.setTimeout = (fn, ms, ...args) => { + const timer = realSetTimeout(() => { + pendingTimeouts.delete(timer); + fn(...args); + }, ms); + pendingTimeouts.add(timer); + return timer; + }; + globalThis.clearTimeout = (timer) => { + pendingTimeouts.delete(timer); + realClearTimeout(timer); + }; require("../../src/popup/index"); @@ -407,6 +431,10 @@ function cleanupPopup() { delete globalThis.window; globalThis.setInterval = realSetInterval; globalThis.clearInterval = realClearInterval; + for (const timer of pendingTimeouts) realClearTimeout(timer); + pendingTimeouts.clear(); + globalThis.setTimeout = realSetTimeout; + globalThis.clearTimeout = realClearTimeout; } module.exports = {