// After "Add" on AddToken, AddressDetail lists the new token, and its line reads // `quantity unknown`, not 0.0000, until a balance refresh that asked the // explorer about the token has returned for every address, the popup closing // and opening again meanwhile included; so does a token added on // SettingsAddToken, wherever it is listed. A refresh that returns draws the // balances on AddressDetail or AddressToken again, so the fetched balance shows // without the screen being reopened // (https://git.eeqj.de/sneak/AutistMask/issues/516). Driven through the real // popup over a stored profile on Ethereum Mainnet, with "Show tracked tokens // with zero balance" on, as it is by default. const { bootPopup, cleanupPopup, unversionedValidProfile, } = require("./support/popupBoot"); // Tracked by nobody until a test adds it. const NEW_TOKEN = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; // The explorer's row for a holding of a million NEW_TOKEN, in the shape // fetchTokenBalances() stores it. const MILLION = { address: NEW_TOKEN, name: "New Token", symbol: "NEW", decimals: 18, balance: "1000000.0", holders: 3, }; afterEach(() => { cleanupPopup(); }); // The popup reopened onto AddressDetail for the one address. function bootOnAddress() { return bootPopup( unversionedValidProfile({ currentView: "address", selectedWallet: 0, selectedAddress: 0, }), ); } // Holds the next balance refresh open. The returned function lets it finish // with the address's ETH balance at 2 and `tokenBalances` as the explorer's // rows for it, or with no token balances at all when given null: an explorer // that did not answer, which the real refreshBalances() leaves unwritten. function holdNextRefresh(page) { const { refreshBalances } = require("../src/shared/balances"); let release; refreshBalances.mockImplementationOnce(async (wallets) => { const tokenBalances = await new Promise((resolve) => { release = resolve; }); wallets[0].addresses[0].balance = "2.0"; if (tokenBalances !== null) { wallets[0].addresses[0].tokenBalances = tokenBalances; } }); return async (tokenBalances) => { release(tokenBalances); await page.settle(); }; } // "+ Token", NEW_TOKEN typed in, and "Add", with the contract lookup // answering at once. async function addNewToken(page) { const { lookupTokenInfo } = require("../src/shared/balances"); lookupTokenInfo.mockResolvedValueOnce({ symbol: "NEW", decimals: 18, name: "New Token", }); await page.click("btn-add-token"); page.node("add-token-address").value = NEW_TOKEN; await page.click("btn-add-token-confirm"); } // NEW_TOKEN's row in the balance list in element `id`, AddressDetail's unless // another is named, as markup. function newTokenRow(page, id = "address-balances") { return page .node(id) .innerHTML.split("
row.includes('data-token="' + NEW_TOKEN.toLowerCase() + '"'), ); } // AddressDetail for the one address opened from Home, as its `[info]` button // does. async function openAddress(page) { const { state } = require("../src/shared/state"); state.selectedWallet = 0; state.selectedAddress = 0; require("../src/popup/views/helpers").pushCurrentView(); require("../src/popup/views/addressDetail").show(); await page.settle(); } // AddressToken for `token` opened from AddressDetail, as tapping the token's // row there does. async function openToken(page, token) { const { state } = require("../src/shared/state"); state.selectedToken = token; require("../src/popup/views/helpers").pushCurrentView(); require("../src/popup/views/addressToken").show(); await page.settle(); } test("until the refresh Add started returns, the new token's line reads quantity unknown", async () => { const page = await bootOnAddress(); expect(page.pageErrors).toEqual([]); const finish = holdNextRefresh(page); await addNewToken(page); expect(page.visibleViews()).toEqual(["address"]); const row = newTokenRow(page); expect(row).toContain("quantity unknown"); expect(row).not.toContain("0.0000"); await finish([MILLION]); }); test("once that refresh returns, AddressDetail shows the fetched balance without being reopened", async () => { const page = await bootOnAddress(); const finish = holdNextRefresh(page); await addNewToken(page); // Stands for the transactions on the screen, which only show() replaces. page.node("tx-list").innerHTML = "transactions on the screen"; await finish([MILLION]); expect(page.visibleViews()).toEqual(["address"]); expect(newTokenRow(page)).toContain("1000000.0000"); // Only the balances are drawn again, not the transaction list. expect(page.node("tx-list").innerHTML).toBe("transactions on the screen"); }); test("a refresh already running when the token is added does not count as fetching it", async () => { const page = await bootOnAddress(); const finishRunning = holdNextRefresh(page); const ticking = page.tick(); await page.settle(); await addNewToken(page); await finishRunning([]); await ticking; expect(newTokenRow(page)).toContain("quantity unknown"); // The next one asks the explorer about the token, which lists no holding // of it: a fetched zero. const finishNext = holdNextRefresh(page); const next = page.tick(); await page.settle(); await finishNext([]); await next; expect(newTokenRow(page)).toContain("0.0000"); }); test("closed and opened again before any refresh has fetched it, the new token's line still reads quantity unknown", async () => { const closed = await bootOnAddress(); holdNextRefresh(closed); await addNewToken(closed); cleanupPopup(); // The refresh this boot starts gets no token balances back: the boot's // stub of refreshBalances() writes none. const page = await bootPopup(undefined, { storage: closed.storage }); expect(page.visibleViews()).toEqual(["address"]); const row = newTokenRow(page); expect(row).toContain("quantity unknown"); expect(row).not.toContain("0.0000"); const finish = holdNextRefresh(page); const ticking = page.tick(); await page.settle(); await finish([MILLION]); await ticking; expect(newTokenRow(page)).toContain("1000000.0000"); }); test("a refresh in which the explorer did not answer leaves the new token's line quantity unknown", async () => { const page = await bootOnAddress(); const finish = holdNextRefresh(page); await addNewToken(page); await finish(null); // Drawn again: the ETH balance it fetched is shown. expect(page.node("address-balances").innerHTML).toContain("2.0000"); expect(newTokenRow(page)).toContain("quantity unknown"); }); test("AddressToken for the new token reads quantity unknown until the refresh returns, then the fetched balance", async () => { const page = await bootOnAddress(); const finish = holdNextRefresh(page); await addNewToken(page); await openToken(page, NEW_TOKEN.toLowerCase()); expect(page.visibleViews()).toEqual(["address-token"]); const before = page.node("address-token-balance").innerHTML; expect(before).toContain("quantity unknown"); expect(before).not.toContain("0.0000"); await finish([MILLION]); expect(page.node("address-token-balance").innerHTML).toContain( "1000000.0000", ); }); test("a token added by contract address on SettingsAddToken reads quantity unknown until the refresh returns", async () => { const page = await bootPopup(unversionedValidProfile()); expect(page.visibleViews()).toEqual(["main"]); const finish = holdNextRefresh(page); const { lookupTokenInfo } = require("../src/shared/balances"); lookupTokenInfo.mockResolvedValueOnce({ symbol: "NEW", decimals: 18, name: "New Token", }); await page.click("btn-settings"); await page.click("btn-settings-add-token"); page.node("settings-addtoken-address").value = NEW_TOKEN; await page.click("btn-settings-addtoken-manual"); await page.click("btn-settings-addtoken-back"); await page.click("btn-settings-back"); expect(page.visibleViews()).toEqual(["main"]); const onHome = newTokenRow(page, "wallet-list"); expect(onHome).toContain("quantity unknown"); expect(onHome).not.toContain("0.0000"); await openAddress(page); const onAddress = newTokenRow(page); expect(onAddress).toContain("quantity unknown"); expect(onAddress).not.toContain("0.0000"); await finish([MILLION]); expect(newTokenRow(page)).toContain("1000000.0000"); }); test("a refresh that returns while AddressToken is up draws its balance again", async () => { const page = await bootPopup( unversionedValidProfile({ currentView: "address-token", viewStack: ["main", "address"], selectedWallet: 0, selectedAddress: 0, selectedToken: "ETH", }), ); expect(page.visibleViews()).toEqual(["address-token"]); expect(page.node("address-token-balance").innerHTML).toContain("1.5000"); const finish = holdNextRefresh(page); const ticking = page.tick(); await page.settle(); await finish(null); await ticking; expect(page.node("address-token-balance").innerHTML).toContain("2.0000"); });