// A refresh asked for while another runs is not dropped. One more refresh runs // once the running one ends, however many were asked for in that time // (https://git.eeqj.de/sneak/AutistMask/issues/581). Each test boots the real // popup over a stored profile and holds the ten-second refresh open. The // balance stand-in writes what the chain holds onto every address, as the real // refreshBalances() does. const { bootPopup, cleanupPopup, unversionedValidProfile, ADDRESS, } = require("./support/popupBoot"); const { hdWalletFromMnemonic, deriveAddressFromXpub, } = require("../src/shared/wallet"); const PHRASE = "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about"; const PASSWORD = "correct horse battery staple"; // The first two addresses of PHRASE, both used on the chain. const { xpub, firstAddress } = hdWalletFromMnemonic(PHRASE); const SECOND_ADDRESS = deriveAddressFromXpub(xpub, 1); // The ETH balance the chain holds for each address, in ether. const CHAIN = { [ADDRESS]: "1.5", [firstAddress]: "2.0", [SECOND_ADDRESS]: "3.0", }; afterEach(() => { cleanupPopup(); }); // The balance stand-in for a refresh that returns. async function readChain(wallets) { for (const wallet of wallets) { for (const addr of wallet.addresses) { addr.balance = CHAIN[addr.address]; } } } // Boots the popup over a profile with one wallet. From then on, a refresh the // test does not answer never returns. So a refresh that starts another on its // own is counted and stops, rather than running forever. async function boot() { const page = await bootPopup(unversionedValidProfile()); expect(page.pageErrors).toEqual([]); const { refreshBalances } = require("../src/shared/balances"); refreshBalances.mockImplementation(() => new Promise(() => {})); return { page, refreshBalances }; } // Starts the ten-second refresh and holds it open until finish() is called. // The held refresh then writes the chain's balances. finish() resolves once // the held refresh has saved them and drawn Home. The one refresh after the // held one also returns. async function holdRefresh(page) { const { refreshBalances } = require("../src/shared/balances"); let answer; refreshBalances .mockImplementationOnce(async (wallets) => { await new Promise((resolve) => { answer = resolve; }); await readChain(wallets); }) .mockImplementationOnce(readChain); const ticking = page.tick(); await page.settle(); return { finish: async () => { answer(); await ticking; }, }; } // Imports PHRASE on AddWallet. Its address scan finds the phrase's first two // addresses. async function importPhrase(page) { const { scanForAddresses } = require("../src/shared/balances"); scanForAddresses.mockImplementationOnce(async () => ({ addresses: [ { address: firstAddress, index: 0 }, { address: SECOND_ADDRESS, index: 1 }, ], nextIndex: 2, })); await page.click("btn-add-wallet-bottom"); await page.click("tab-mnemonic"); page.node("wallet-mnemonic").value = PHRASE; page.node("add-wallet-password").value = PASSWORD; page.node("add-wallet-password-confirm").value = PASSWORD; await page.click("btn-add-wallet-confirm"); } test("addresses an import finds while a refresh runs show their balances once one more refresh returns", async () => { const { page } = await boot(); const refresh = await holdRefresh(page); await importPhrase(page); expect(page.visibleViews()).toEqual(["main"]); expect(page.text("flash-msg")).toBe("Found 2 addresses."); await refresh.finish(); await page.settle(); const list = page.node("wallet-list").innerHTML; expect(list).toContain("2.0000"); expect(list).toContain("3.0000"); }); test("several refreshes asked for while one runs lead to exactly one more", async () => { const { page, refreshBalances } = await boot(); const refresh = await holdRefresh(page); const calls = refreshBalances.mock.calls.length; await page.tick(); await page.tick(); await importPhrase(page); expect(refreshBalances).toHaveBeenCalledTimes(calls); await refresh.finish(); await page.settle(); expect(refreshBalances).toHaveBeenCalledTimes(calls + 1); });