From f38611f1336f6fe6067ca6c3988c26bbb2854fb2 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 18 Oct 2026 23:36:26 +0000 Subject: [PATCH] fix: a popup that opened on Welcome keeps refreshing once the first wallet is added (closes #530) The ten-second refresh was started only by init(), and only when a wallet existed as the popup opened, so a popup that opened on Welcome refreshed the new wallet once and then showed that balance until it was reopened. init() and the three import paths on AddWallet now call startRefresh(), which refreshes at once and starts the ten-second refresh unless it is already running, and does nothing while the recovery screen is up. The two AddWallet tests that build their own context pass it in place of the refresh they passed before. Model: opus-5-5 --- src/popup/index.js | 21 ++++- src/popup/views/addWallet.js | 6 +- tests/addressKey.test.js | 2 +- tests/addressScanCancelled.test.js | 2 +- tests/firstWalletRefresh.test.js | 135 +++++++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 tests/firstWalletRefresh.test.js diff --git a/src/popup/index.js b/src/popup/index.js index 0021a30..8e4ad16 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -64,8 +64,8 @@ window.addEventListener("pagehide", () => pageClosed.abort()); let refreshInFlight = false; -// The ten-second refresh init() starts, stopped when the popup moves to the -// recovery screen: there is no profile left to refresh. +// The ten-second refresh startRefresh() starts, stopped when the popup moves to +// the recovery screen: there is no profile left to refresh. let refreshTimer = null; async function doRefreshAndRender() { @@ -99,9 +99,23 @@ async function doRefreshAndRender() { } } +// Refreshes now and every ten seconds from then on. Called once a wallet +// exists: by init() when the popup opens with one, and by AddWallet when it +// adds one, which in a popup that opened on Welcome is the first +// (https://git.eeqj.de/sneak/AutistMask/issues/530). With the refresh already +// running it starts no second one, and on the recovery screen it does nothing. +function startRefresh() { + if (!$("view-state-recovery").classList.contains("hidden")) return; + doRefreshAndRender(); + if (refreshTimer === null) { + refreshTimer = setInterval(doRefreshAndRender, 10000); + } +} + const ctx = { renderWalletList, doRefreshAndRender, + startRefresh, pageClosed: pageClosed.signal, showAddWalletView: () => { pushCurrentView(); @@ -318,8 +332,7 @@ async function init() { } else { renderWalletList(); restoreView(); - doRefreshAndRender(); - refreshTimer = setInterval(doRefreshAndRender, 10000); + startRefresh(); } } diff --git a/src/popup/views/addWallet.js b/src/popup/views/addWallet.js index 1e5a8e5..a339dae 100644 --- a/src/popup/views/addWallet.js +++ b/src/popup/views/addWallet.js @@ -234,7 +234,7 @@ async function importMnemonic(ctx) { showFlash("Ready.", 1000); } - ctx.doRefreshAndRender(); + ctx.startRefresh(); } async function importPrivateKey(ctx) { @@ -274,7 +274,7 @@ async function importPrivateKey(ctx) { ctx.renderWalletList(); showView("main"); - ctx.doRefreshAndRender(); + ctx.startRefresh(); } async function importXprvKey(ctx) { @@ -368,7 +368,7 @@ async function importXprvKey(ctx) { showFlash("Ready.", 1000); } - ctx.doRefreshAndRender(); + ctx.startRefresh(); } function init(ctx) { diff --git a/tests/addressKey.test.js b/tests/addressKey.test.js index 631cfe5..afdc961 100644 --- a/tests/addressKey.test.js +++ b/tests/addressKey.test.js @@ -230,7 +230,7 @@ beforeEach(() => { addWallet.init({ renderWalletList: () => {}, - doRefreshAndRender: () => {}, + startRefresh: () => {}, pageClosed: new AbortController().signal, }); confirmTx.init({}); diff --git a/tests/addressScanCancelled.test.js b/tests/addressScanCancelled.test.js index febfd63..48815d0 100644 --- a/tests/addressScanCancelled.test.js +++ b/tests/addressScanCancelled.test.js @@ -94,7 +94,7 @@ describe.each([ async function create(pageClosed) { addWallet.init({ renderWalletList: () => {}, - doRefreshAndRender: () => {}, + startRefresh: () => {}, pageClosed, }); element("tab-" + mode).listeners.click(); diff --git a/tests/firstWalletRefresh.test.js b/tests/firstWalletRefresh.test.js new file mode 100644 index 0000000..232cb30 --- /dev/null +++ b/tests/firstWalletRefresh.test.js @@ -0,0 +1,135 @@ +// A popup that opened with no wallet never started the ten-second refresh, so +// after the first wallet was added Home went on showing the balance it had then +// until the popup was reopened +// (https://git.eeqj.de/sneak/AutistMask/issues/530). Each test boots the real +// popup, adds wallets through AddWallet, and runs the intervals the popup set +// with tick(). The balance stand-in writes what the chain holds onto every +// address, as the real refreshBalances() does. + +const { HDNodeWallet, Mnemonic } = require("ethers"); + +const { + bootPopup, + cleanupPopup, + unversionedValidProfile, + ADDRESS, +} = require("./support/popupBoot"); + +const PHRASE = + "abandon abandon abandon abandon abandon abandon " + + "abandon abandon abandon abandon abandon about"; +const PRIVATE_KEY = "0x" + "11".repeat(32); +const XPRV = HDNodeWallet.fromSeed( + Mnemonic.fromPhrase(PHRASE).computeSeed(), +).extendedKey; +const PASSWORD = "correct horse battery staple"; + +const TABS = [ + { tab: "tab-mnemonic", field: "wallet-mnemonic", secret: PHRASE }, + { tab: "tab-privkey", field: "import-private-key", secret: PRIVATE_KEY }, + { tab: "tab-xprv", field: "import-xprv-key", secret: XPRV }, +]; + +// A record this build cannot read: its wallets are a string. +const UNREADABLE = { + hasWallet: true, + wallets: ADDRESS, + activeAddress: ADDRESS, +}; + +// The ETH balance the chain holds for every address, in ether. +let chainBalance = "0.0"; + +// Boots the popup over `stored`, with the balance stand-in reading the chain. +async function boot(stored) { + const page = await bootPopup(stored); + const { refreshBalances } = require("../src/shared/balances"); + refreshBalances.mockImplementation(async (wallets) => { + for (const wallet of wallets) { + for (const addr of wallet.addresses) addr.balance = chainBalance; + } + }); + return { page, refreshBalances }; +} + +// Fills in the open AddWallet screen and presses "Import". +async function importOn(page, { tab, field, secret }) { + await page.click(tab); + page.node(field).value = secret; + page.node("add-wallet-password").value = PASSWORD; + page.node("add-wallet-password-confirm").value = PASSWORD; + await page.click("btn-add-wallet-confirm"); +} + +beforeEach(() => { + chainBalance = "0.0"; +}); + +afterEach(() => { + cleanupPopup(); +}); + +describe("a popup that opened on Welcome", () => { + test.each(TABS)( + "after the first wallet is added from $tab, funds that arrive show on Home", + async (entry) => { + const { page } = await boot(undefined); + expect(page.pageErrors).toEqual([]); + await page.click("btn-welcome-add"); + await importOn(page, entry); + expect(page.visibleViews()).toEqual(["main"]); + expect(page.text("total-value")).toBe("0.0000 ETH"); + + chainBalance = "2.0"; + await page.tick(); + + expect(page.text("total-value")).toBe("2.0000 ETH"); + }, + ); + + test("a second wallet starts no second refresh", async () => { + const { page, refreshBalances } = await boot(undefined); + await page.click("btn-welcome-add"); + await importOn(page, TABS[1]); + await page.click("btn-add-wallet-bottom"); + await importOn(page, TABS[0]); + const calls = refreshBalances.mock.calls.length; + + await page.tick(); + + expect(refreshBalances).toHaveBeenCalledTimes(calls + 1); + }); + + test("a wallet added as the recovery screen goes up starts no refresh", async () => { + const { page, refreshBalances } = await boot(undefined); + // The stored record becomes unreadable during the address scan that + // follows the import, and a save finds it. + const { scanForAddresses } = require("../src/shared/balances"); + const { saveState } = require("../src/shared/state"); + scanForAddresses.mockImplementationOnce(async () => { + page.storage.write("autistmask", UNREADABLE); + await saveState().catch(() => {}); + return { addresses: [], nextIndex: 1 }; + }); + await page.click("btn-welcome-add"); + await importOn(page, TABS[0]); + expect(page.visibleViews()).toEqual(["state-recovery"]); + + await page.tick(); + + expect(refreshBalances).not.toHaveBeenCalled(); + }); +}); + +describe("a popup that opened with a wallet", () => { + test("adding a wallet starts no second refresh", async () => { + const { page, refreshBalances } = await boot(unversionedValidProfile()); + await page.click("btn-add-wallet-bottom"); + await importOn(page, TABS[1]); + const calls = refreshBalances.mock.calls.length; + + await page.tick(); + + expect(refreshBalances).toHaveBeenCalledTimes(calls + 1); + }); +});