The popup's doRefreshAndRender() and the background's backgroundRefresh() each note the network a refresh starts on and write its balances only if that network is still the active one when the requests return. Otherwise nothing is written and lastBalanceRefresh stays as the switch left it. The background checks inside updateState(). The popup refreshes a copy of its wallets and copies the results onto `state` from inside its save, under the lock, only if both its own network and the stored record's still match, so a switch the background makes is caught too. saveState() takes an optional function, run there on the stored record before the page's state is compared, that may change `state` or return false to cancel the write. Model: opus-5-5
161 lines
5.8 KiB
JavaScript
161 lines
5.8 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.
|
|
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);
|
|
});
|