When the background saves a network switch after the user approves a site's request, or another popup page saves one, an open popup takes the new network from the stored record inside a save, holding the lock, and checks the record once at start-up for a switch saved before it listened. It then leaves the page as a switch in Settings does: the cut and the redraw are two Settings functions both paths call, and both reset the popup's caches through one function. A switch in Settings is now made inside its own save too, so one picked while a follow waits for the lock is made after it. The popup never switches the network itself. Model: opus-5-5
107 lines
4.2 KiB
JavaScript
107 lines
4.2 KiB
JavaScript
// A chrome.storage stub that behaves like the real one.
|
|
//
|
|
// The real extension storage API is a serialization boundary: `set` writes a
|
|
// structured clone of what it is given, and `get` hands back a structured
|
|
// clone of what is stored. Nothing an extension page holds is ever the object
|
|
// storage holds.
|
|
//
|
|
// A stub that skips the clone aliases them together, and that hides an entire
|
|
// class of defect rather than merely being imprecise. loadState() assigns
|
|
// nested references straight out of the get result, so over an aliasing stub a
|
|
// test can assert "the endpoint was persisted" and pass on a build that never
|
|
// called saveState() at all: the in-memory mutation IS the stored record.
|
|
// Measured, not theorised — with an aliasing `get` restored over the handler
|
|
// fixed in https://git.eeqj.de/sneak/AutistMask/pulls/319, the whole suite
|
|
// passed 794/794 (https://git.eeqj.de/sneak/AutistMask/issues/324).
|
|
//
|
|
// So every test that drives real persistence uses this, and nothing rebuilds
|
|
// a storage stub by hand.
|
|
|
|
// One storage area: get, set and remove over its own store, cloning both ways.
|
|
// `changed` is handed what each set or remove changed, in the shape
|
|
// storage.onChanged reports it.
|
|
function makeArea(store, tick, changed) {
|
|
const get = jest.fn(async (key) => {
|
|
tick();
|
|
if (key === undefined || key === null) return structuredClone(store);
|
|
const keys = Array.isArray(key) ? key : [key];
|
|
const out = {};
|
|
for (const k of keys) {
|
|
if (Object.prototype.hasOwnProperty.call(store, k)) {
|
|
out[k] = structuredClone(store[k]);
|
|
}
|
|
}
|
|
return out;
|
|
});
|
|
|
|
const set = jest.fn(async (items) => {
|
|
tick();
|
|
const changes = {};
|
|
for (const k of Object.keys(items)) {
|
|
changes[k] = structuredClone({
|
|
oldValue: store[k],
|
|
newValue: items[k],
|
|
});
|
|
store[k] = structuredClone(items[k]);
|
|
}
|
|
changed(changes);
|
|
});
|
|
|
|
const remove = jest.fn(async (key) => {
|
|
tick();
|
|
const changes = {};
|
|
for (const k of Array.isArray(key) ? key : [key]) {
|
|
changes[k] = { oldValue: store[k] };
|
|
delete store[k];
|
|
}
|
|
changed(changes);
|
|
});
|
|
|
|
return { get, set, remove };
|
|
}
|
|
|
|
// `initial` is the starting contents of `storage.local`, keyed as storage is:
|
|
// { autistmask: {...} }. `storage.session` starts empty.
|
|
// `onOp` runs before each operation on either area, for a test that needs to
|
|
// advance a clock or count round trips.
|
|
function makeStorageStub(initial, onOp) {
|
|
const store = initial ? structuredClone(initial) : {};
|
|
const tick = onOp || (() => {});
|
|
// storage.onChanged: after a set or remove has finished, every listener is
|
|
// called with what it changed and the area's name, as the browser calls
|
|
// them for a write by any page or the background.
|
|
const listeners = [];
|
|
const announce = (areaName) => (changes) => {
|
|
Promise.resolve().then(() => {
|
|
for (const fn of listeners) fn(structuredClone(changes), areaName);
|
|
});
|
|
};
|
|
const local = makeArea(store, tick, announce("local"));
|
|
|
|
return {
|
|
// Drop this straight in as chrome.storage.
|
|
local,
|
|
session: makeArea({}, tick, announce("session")),
|
|
onChanged: { addListener: (fn) => listeners.push(fn) },
|
|
get: local.get,
|
|
set: local.set,
|
|
remove: local.remove,
|
|
// What is stored in `storage.local`, cloned on the way out: a test can
|
|
// neither observe a later write through an object it read nor reach
|
|
// into the store by mutating one.
|
|
read: (key) =>
|
|
key === undefined
|
|
? structuredClone(store)
|
|
: structuredClone(store[key]),
|
|
// Seed or replace a record without going through the module under
|
|
// test — for standing in as "another page wrote this". Nothing is
|
|
// announced to storage.onChanged, so it also stands in for the time
|
|
// before the browser tells this page of such a write.
|
|
write: (key, value) => {
|
|
store[key] = structuredClone(value);
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = { makeStorageStub };
|