fix: a popup that opened on Welcome keeps refreshing once the first wallet is added (closes #530) #573

Merged
clawbot merged 1 commits from issue-530-refresh-after-first-wallet into next 2026-10-19 03:19:53 +02:00
5 changed files with 161 additions and 9 deletions
+17 -4
View File
@@ -64,8 +64,8 @@ window.addEventListener("pagehide", () => pageClosed.abort());
let refreshInFlight = false; let refreshInFlight = false;
// The ten-second refresh init() starts, stopped when the popup moves to the // The ten-second refresh startRefresh() starts, stopped when the popup moves to
// recovery screen: there is no profile left to refresh. // the recovery screen: there is no profile left to refresh.
let refreshTimer = null; let refreshTimer = null;
async function doRefreshAndRender() { 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 = { const ctx = {
renderWalletList, renderWalletList,
doRefreshAndRender, doRefreshAndRender,
startRefresh,
pageClosed: pageClosed.signal, pageClosed: pageClosed.signal,
showAddWalletView: () => { showAddWalletView: () => {
pushCurrentView(); pushCurrentView();
@@ -318,8 +332,7 @@ async function init() {
} else { } else {
renderWalletList(); renderWalletList();
restoreView(); restoreView();
doRefreshAndRender(); startRefresh();
refreshTimer = setInterval(doRefreshAndRender, 10000);
} }
} }
+3 -3
View File
@@ -234,7 +234,7 @@ async function importMnemonic(ctx) {
showFlash("Ready.", 1000); showFlash("Ready.", 1000);
} }
ctx.doRefreshAndRender(); ctx.startRefresh();
} }
async function importPrivateKey(ctx) { async function importPrivateKey(ctx) {
@@ -274,7 +274,7 @@ async function importPrivateKey(ctx) {
ctx.renderWalletList(); ctx.renderWalletList();
showView("main"); showView("main");
ctx.doRefreshAndRender(); ctx.startRefresh();
} }
async function importXprvKey(ctx) { async function importXprvKey(ctx) {
@@ -368,7 +368,7 @@ async function importXprvKey(ctx) {
showFlash("Ready.", 1000); showFlash("Ready.", 1000);
} }
ctx.doRefreshAndRender(); ctx.startRefresh();
} }
function init(ctx) { function init(ctx) {
+1 -1
View File
@@ -230,7 +230,7 @@ beforeEach(() => {
addWallet.init({ addWallet.init({
renderWalletList: () => {}, renderWalletList: () => {},
doRefreshAndRender: () => {}, startRefresh: () => {},
pageClosed: new AbortController().signal, pageClosed: new AbortController().signal,
}); });
confirmTx.init({}); confirmTx.init({});
+1 -1
View File
@@ -94,7 +94,7 @@ describe.each([
async function create(pageClosed) { async function create(pageClosed) {
addWallet.init({ addWallet.init({
renderWalletList: () => {}, renderWalletList: () => {},
doRefreshAndRender: () => {}, startRefresh: () => {},
pageClosed, pageClosed,
}); });
element("tab-" + mode).listeners.click(); element("tab-" + mode).listeners.click();
+139
View File
@@ -0,0 +1,139 @@
// 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");
}
// Fake timers hold the timer of the flash message an import ends with, which
// would otherwise fire after cleanupPopup() has removed the page and throw.
beforeEach(() => {
jest.useFakeTimers();
chainBalance = "0.0";
});
afterEach(() => {
cleanupPopup();
jest.useRealTimers();
});
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);
});
});