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
162 lines
5.9 KiB
JavaScript
162 lines
5.9 KiB
JavaScript
// A balance refresh the popup started on one network, and that returns after
|
|
// the network was switched to another, writes nothing: the switch cleared every
|
|
// address's balances, and the previous network's must not come back over them
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/577). The switch is made either
|
|
// by the user in this popup's Settings, or by the background after the user
|
|
// approved a site's request, which changes only the stored record. Driven
|
|
// through the real popup over a stored profile on Ethereum Mainnet. The same
|
|
// refresh in the background is in tests/backgroundStateIsolation.test.js.
|
|
|
|
const {
|
|
bootPopup,
|
|
cleanupPopup,
|
|
unversionedValidProfile,
|
|
TOKEN_ADDRESS,
|
|
} = require("./support/popupBoot");
|
|
const { applyChainSwitchFields } = require("../src/shared/chainSwitchFields");
|
|
|
|
afterEach(() => {
|
|
cleanupPopup();
|
|
});
|
|
|
|
// Starts the ten-second refresh and holds it open. The balance stand-in writes
|
|
// Ethereum Mainnet's balances in place, on the records handed in, as the real
|
|
// refreshBalances() does, once the returned function is called. Resolves with
|
|
// that function and the refresh still running.
|
|
async function holdRefresh(page) {
|
|
const { refreshBalances } = require("../src/shared/balances");
|
|
let answer;
|
|
refreshBalances.mockImplementationOnce(async (wallets) => {
|
|
await new Promise((resolve) => {
|
|
answer = resolve;
|
|
});
|
|
wallets[0].addresses[0].balance = "7.0";
|
|
wallets[0].addresses[0].tokenBalances = [
|
|
{
|
|
address: TOKEN_ADDRESS,
|
|
name: "Test Token",
|
|
symbol: "TST",
|
|
decimals: 18,
|
|
balance: "5.0",
|
|
holders: null,
|
|
},
|
|
];
|
|
});
|
|
const ticking = page.tick();
|
|
await page.settle();
|
|
return {
|
|
finish: async () => {
|
|
answer();
|
|
await ticking;
|
|
},
|
|
};
|
|
}
|
|
|
|
// Picks `networkId` in the Settings network selector, as the user does.
|
|
async function chooseNetwork(page, networkId) {
|
|
const select = page.node("settings-network");
|
|
select.value = networkId;
|
|
for (const fn of select.listeners.change) await fn();
|
|
await page.settle();
|
|
}
|
|
|
|
// The stored record holds none of Ethereum Mainnet's balances, and no refresh
|
|
// recorded as done.
|
|
function expectNothingStored(page) {
|
|
const stored = page.storage.read("autistmask");
|
|
expect(stored.networkId).toBe("sepolia");
|
|
expect(stored.wallets[0].addresses[0].balance).toBe("0");
|
|
expect(stored.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
|
expect(stored.lastBalanceRefresh).toBe(0);
|
|
}
|
|
|
|
test("a refresh that returns after a switch in Settings writes nothing, and the next one runs on the new network", async () => {
|
|
const page = await bootPopup(unversionedValidProfile());
|
|
expect(page.pageErrors).toEqual([]);
|
|
const { refreshBalances } = require("../src/shared/balances");
|
|
const { state } = require("../src/shared/state");
|
|
|
|
const refresh = await holdRefresh(page);
|
|
await page.click("btn-settings");
|
|
await chooseNetwork(page, "sepolia");
|
|
await refresh.finish();
|
|
|
|
expect(state.wallets[0].addresses[0].balance).toBe("0");
|
|
expect(state.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
|
expectNothingStored(page);
|
|
|
|
await page.click("btn-settings-back");
|
|
expect(page.visibleViews()).toEqual(["main"]);
|
|
expect(page.text("total-value")).toBe("0.0000 SepoliaETH");
|
|
|
|
await page.tick();
|
|
|
|
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
|
expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan(
|
|
0,
|
|
);
|
|
});
|
|
|
|
// The switch is made only once the refresh's save, holding the lock, has
|
|
// written Ethereum Mainnet's balances, and the switch then clears them.
|
|
test("a refresh whose save is reading the stored record when the user switches in Settings leaves none of its balances stored", async () => {
|
|
const page = await bootPopup(unversionedValidProfile());
|
|
expect(page.pageErrors).toEqual([]);
|
|
const { state } = require("../src/shared/state");
|
|
|
|
const refresh = await holdRefresh(page);
|
|
await page.click("btn-settings");
|
|
|
|
// The next read of the stored record, the refresh's save's, is held.
|
|
const get = page.storage.local.get;
|
|
const read = get.getMockImplementation();
|
|
let releaseRead;
|
|
get.mockImplementationOnce(async (key) => {
|
|
await new Promise((resolve) => {
|
|
releaseRead = resolve;
|
|
});
|
|
return read(key);
|
|
});
|
|
const finishing = refresh.finish();
|
|
await page.settle();
|
|
|
|
// The user picks Sepolia meanwhile; the switch's own save waits for the
|
|
// refresh's save to let the lock go.
|
|
const select = page.node("settings-network");
|
|
select.value = "sepolia";
|
|
const switching = Promise.all(select.listeners.change.map((fn) => fn()));
|
|
await page.settle();
|
|
|
|
releaseRead();
|
|
await finishing;
|
|
await switching;
|
|
await page.settle();
|
|
|
|
expect(state.networkId).toBe("sepolia");
|
|
expect(state.wallets[0].addresses[0].balance).toBe("0");
|
|
expect(state.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
|
expectNothingStored(page);
|
|
});
|
|
|
|
test("a refresh that returns after the background switched the network writes nothing", async () => {
|
|
const page = await bootPopup(unversionedValidProfile());
|
|
expect(page.pageErrors).toEqual([]);
|
|
const { saveState } = require("../src/shared/state");
|
|
|
|
const refresh = await holdRefresh(page);
|
|
// The background writes the switch to Sepolia, as its updateState() does
|
|
// once the user approves a site's request. This page is not told of it
|
|
// yet.
|
|
const record = page.storage.read("autistmask");
|
|
applyChainSwitchFields(record, "sepolia");
|
|
page.storage.write("autistmask", record);
|
|
await refresh.finish();
|
|
|
|
expectNothingStored(page);
|
|
|
|
// Nor does a later save by this page, such as every navigation makes.
|
|
await saveState();
|
|
|
|
expectNothingStored(page);
|
|
});
|