// 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); });