// While a refresh runs, a request through ctx 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). A tick of the ten-second // timer that finds a refresh running is dropped. Each test boots the real popup // over a stored profile and holds the ten-second refresh open. `readChain()` // sets each address's balance from `CHAIN`, as the real `refreshBalances()` // sets it from the node. 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, // `refreshBalances()` never returns unless the test answers it. So an unwanted // refresh is counted once and then waits, instead of starting more. 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"); } // Imports the private key `key` on AddWallet. async function importKey(page, key) { await page.click("btn-add-wallet-bottom"); await page.click("tab-privkey"); page.node("import-private-key").value = key; page.node("add-wallet-password").value = PASSWORD; page.node("add-wallet-password-confirm").value = PASSWORD; await page.click("btn-add-wallet-confirm"); } test("addresses that an import finds during a refresh 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 requests during a refresh lead to exactly one more refresh", async () => { const { page, refreshBalances } = await boot(); const refresh = await holdRefresh(page); const calls = refreshBalances.mock.calls.length; await importKey(page, "0x" + "11".repeat(32)); await importKey(page, "0x" + "22".repeat(32)); await importPhrase(page); expect(refreshBalances).toHaveBeenCalledTimes(calls); await refresh.finish(); await page.settle(); expect(refreshBalances).toHaveBeenCalledTimes(calls + 1); }); test("a tick of the ten-second timer during a refresh starts no other refresh", async () => { const { page, refreshBalances } = await boot(); const refresh = await holdRefresh(page); const calls = refreshBalances.mock.calls.length; await page.tick(); await refresh.finish(); await page.settle(); expect(refreshBalances).toHaveBeenCalledTimes(calls); });