fix: an open popup follows a network switch the background saves (closes #650)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

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
This commit is contained in:
2026-10-21 00:26:46 +00:00
parent f53b503b15
commit 482cd42ebb
16 changed files with 555 additions and 91 deletions
+1 -1
View File
@@ -311,7 +311,7 @@ async function bootPopup(stored, options) {
const windowListeners = {};
globalThis.chrome = {
storage: { local: storage.local },
storage: { local: storage.local, onChanged: storage.onChanged },
runtime: {
sendMessage: jest.fn(async () => ({})),
getURL: (p) => "chrome-extension://autistmask/" + p,
+30 -5
View File
@@ -18,7 +18,9 @@
// a storage stub by hand.
// One storage area: get, set and remove over its own store, cloning both ways.
function makeArea(store, tick) {
// `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);
@@ -34,14 +36,25 @@ function makeArea(store, tick) {
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();
for (const k of Array.isArray(key) ? key : [key]) delete store[k];
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 };
@@ -54,12 +67,22 @@ function makeArea(store, tick) {
function makeStorageStub(initial, onOp) {
const store = initial ? structuredClone(initial) : {};
const tick = onOp || (() => {});
const local = makeArea(store, tick);
// 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),
session: makeArea({}, tick, announce("session")),
onChanged: { addListener: (fn) => listeners.push(fn) },
get: local.get,
set: local.set,
remove: local.remove,
@@ -71,7 +94,9 @@ function makeStorageStub(initial, onOp) {
? 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".
// 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);
},