Firefox gives an extension page in a private window locks of its own, so a popup there could save between the background's read and its write and undo a network switch the background saved. `manifest/firefox.json` now sets `"incognito": "not_allowed"`, so such a page cannot read or write extension storage, and `tests/manifest.test.js` pins the key. README and the user guide say AutistMask does not work in Firefox private windows. Chrome needs no change: in its default incognito mode an extension page shares the background's lock. A Firefox user on "Never remember history" can no longer run AutistMask at all. Model: opus-5-5
96 lines
3.8 KiB
JavaScript
96 lines
3.8 KiB
JavaScript
// 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 a 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 background and its pages 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("after a save that failed, the next save still keeps a network the background switches to during it", async () => {
|
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
const storage = makeStorageStub({ autistmask: { networkId: "sepolia" } });
|
|
const { popup, background, applyChainSwitchFields } = loadContexts(storage);
|
|
await popup.loadState();
|
|
popup.state.currentView = "settings";
|
|
|
|
// The first save's write is refused, so it gives the lock back having
|
|
// failed.
|
|
storage.local.set.mockRejectedValueOnce(new Error("quota exceeded"));
|
|
await expect(popup.saveState()).rejects.toThrow("quota exceeded");
|
|
|
|
// The next save is between its read and its write when the background
|
|
// switches to mainnet, as in the test above.
|
|
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");
|
|
});
|