A connection allowed without "Remember my choice for this site" lived in a module-level object in the background, so it ended the first time Chrome stopped the idle worker. Each such connection is now one item in the browser's session storage, which outlives the worker and is emptied when the browser closes, read and written through new wrappers in src/shared/browserApi.js. Removal in Settings, address switch, address removal and wallet removal still end it; Settings now waits for the removal before redrawing. manifest/firefox.json now declares a minimum of Firefox 115.0, the first with session storage. Judgement call: address and wallet removal still notify the background without waiting. Model: opus-5-5
82 lines
3.1 KiB
JavaScript
82 lines
3.1 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.
|
|
function makeArea(store, tick) {
|
|
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();
|
|
for (const k of Object.keys(items)) {
|
|
store[k] = structuredClone(items[k]);
|
|
}
|
|
});
|
|
|
|
const remove = jest.fn(async (key) => {
|
|
tick();
|
|
for (const k of Array.isArray(key) ? key : [key]) delete store[k];
|
|
});
|
|
|
|
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 || (() => {});
|
|
const local = makeArea(store, tick);
|
|
|
|
return {
|
|
// Drop this straight in as chrome.storage.
|
|
local,
|
|
session: makeArea({}, tick),
|
|
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".
|
|
write: (key, value) => {
|
|
store[key] = structuredClone(value);
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = { makeStorageStub };
|