When the background saves a network switch after the user approves a site's request, or another popup page saves one, an open popup now notices the stored record change and takes the new network from it, holding the lock, inside a save. It then leaves the page as a switch in Settings does: the stack is cut, every screen counts as not yet rendered, and the screen on top is drawn again. The cut and the redraw are now two functions in the Settings view that both paths call. A record already on the popup's network, such as its own Settings switch, changes nothing. 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 stored record still holds Ethereum Mainnet while the refresh's save reads
|
|
// it, so only this page's own network shows the switch.
|
|
test("a refresh whose save is reading the stored record when the user switches in Settings writes nothing", 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);
|
|
});
|