// A popup save reads the stored record, merges this page's changes into it // and writes it back. The background's write of a network the user approved // used to be able to land between that read and that write, and the popup's // write then put the old network back // (https://git.eeqj.de/sneak/AutistMask/issues/641). Every write of the record // now holds one lock from its read to its write. // // The popup and the background run here as the real modules over one shared // storage stub, and share the lock the way the extension's pages and its // background share the browser's (tests/support/navigatorLocks.js). const { makeStorageStub } = require("./support/storageStub"); function loadContexts(storage) { jest.resetModules(); globalThis.chrome = { storage: { local: storage.local } }; return { popup: require("../src/shared/state"), background: require("../src/background/state"), applyChainSwitchFields: require("../src/shared/chainSwitchFields") .applyChainSwitchFields, }; } // Lets every pending promise callback run, and then some. function nextMacrotask() { return new Promise((resolve) => setImmediate(resolve)); } afterEach(() => { delete globalThis.chrome; jest.restoreAllMocks(); }); test("a network the background switches to during a popup save stays stored", async () => { const storage = makeStorageStub({ autistmask: { networkId: "sepolia" } }); const { popup, background, applyChainSwitchFields } = loadContexts(storage); await popup.loadState(); // The popup navigates, so its save has a change of its own to write. popup.state.currentView = "settings"; // The popup's save has read the record and is about to write it back when // the user's approval of a site's switch to mainnet reaches the // background. Without the lock the background's whole update runs before // the next macrotask; with it, the update waits for the popup's write. let backgroundSwitch; const realSet = storage.local.set.getMockImplementation(); storage.local.set.mockImplementationOnce(async (items) => { backgroundSwitch = background.updateState((fresh) => applyChainSwitchFields(fresh, "mainnet"), ); await nextMacrotask(); return realSet(items); }); await popup.saveState(); await backgroundSwitch; const stored = storage.read("autistmask"); expect(stored.networkId).toBe("mainnet"); expect(stored.currentView).toBe("settings"); }); test("a save that fails holding the lock lets it go", async () => { jest.spyOn(console, "error").mockImplementation(() => {}); const storage = makeStorageStub({ autistmask: { networkId: "sepolia" } }); const { popup, background, applyChainSwitchFields } = loadContexts(storage); await popup.loadState(); storage.local.set.mockRejectedValueOnce(new Error("quota exceeded")); await expect(popup.saveState()).rejects.toThrow("quota exceeded"); await background.updateState((fresh) => applyChainSwitchFields(fresh, "mainnet"), ); expect(storage.read("autistmask").networkId).toBe("mainnet"); });