// Blockscout and Etherscan are sent none of the user's data until the user // turns on a setting for each // (https://git.eeqj.de/sneak/AutistMask/issues/668). A new profile has "Use // Blockscout" and "Check recipients on Etherscan" both off. While a setting is // off, no request sends its service the user's data. // // One request still reaches Blockscout while "Use Blockscout" is off: the check // behind "Save" beside the Blockscout URL in Settings. That check asks the URL // only for `/stats` and sends none of the user's data. // // Before any other request to Blockscout or Etherscan, the wallet reads that // service's setting from storage, not the page's copy. The wallet reads the // setting again when the answer lands. So a change saved by another page // applies at once. An open page also takes such a change and draws its screens // again. // // Only Blockscout reports the tokens the user has not tracked. While "Use // Blockscout" is off, no address keeps them and no screen lists them. // // The functions that make the requests are driven directly, against a fetch() // that records every URL it is asked for. The screens are driven through the // real popup over a stored profile (tests/support/popupBoot.js). The fetch() // stub in popupBoot.js records the same way. "use strict"; const { FetchRequest } = require("ethers"); const { bootPopup, cleanupPopup, unversionedValidProfile, ADDRESS, TOKEN_ADDRESS, } = require("./support/popupBoot"); const { makeStorageStub } = require("./support/storageStub"); const { DEFAULT_STATE, normalizePersisted, } = require("../src/shared/persistedState"); const { fetchTokenBalances, refreshBalances, } = require("../src/shared/balances"); const { fetchRecentTransactions } = require("../src/shared/transactions"); const { getFullWarnings } = require("../src/shared/addressWarnings"); const { checkEtherscanLabel } = require("../src/shared/etherscanLabels"); const { HISTORY_OFF_TEXT, escapeHtml } = require("../src/popup/views/helpers"); const RPC_URL = "https://rpc.example.invalid"; const BLOCKSCOUT = "https://eth.blockscout.com/api/v2"; const RECIPIENT = "0x2222222222222222222222222222222222222222"; const TX_HASH = "0x" + "ab".repeat(32); const ZERO_WORD = "0x" + "0".repeat(64); // USDC, from the bundled list, as Blockscout reports a holding of it. const USDC_ROW = { value: "5000000", token: { type: "ERC-20", address_hash: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", symbol: "USDC", name: "USD Coin", decimals: "6", holders_count: "50000", }, }; // A transfer of 1 ETH from RECIPIENT to ADDRESS, as Blockscout lists it in // ADDRESS's history. const HISTORY_ITEM = { hash: TX_HASH, block_number: 21000000, timestamp: "2026-02-27T12:00:00.000Z", from: { hash: RECIPIENT }, to: { hash: ADDRESS, is_contract: false }, value: "1000000000000000000", method: null, status: "ok", }; // Etherscan's page for RECIPIENT, which labels it a phishing address. const PHISHING_PAGE = "Fake_Phishing1 | Address: " + RECIPIENT + " | Etherscan"; const realFetch = globalThis.fetch; afterEach(() => { cleanupPopup(); jest.restoreAllMocks(); }); describe("a new profile", () => { test("has both settings off", () => { expect(DEFAULT_STATE.useBlockscout).toBe(false); expect(DEFAULT_STATE.checkRecipientsOnEtherscan).toBe(false); const loaded = normalizePersisted(undefined); expect(loaded.useBlockscout).toBe(false); expect(loaded.checkRecipientsOnEtherscan).toBe(false); }); test("keeps either setting the user turned on", () => { const loaded = normalizePersisted({ useBlockscout: true, checkRecipientsOnEtherscan: true, }); expect(loaded.useBlockscout).toBe(true); expect(loaded.checkRecipientsOnEtherscan).toBe(true); }); }); describe("where each request is made", () => { // Every URL that fetch() was asked for, in order. let asked; // What Blockscout and Etherscan answer: USDC among ADDRESS's tokens, // HISTORY_ITEM in its history, and PHISHING_PAGE for RECIPIENT. function answer(url) { return { ok: true, status: 200, json: async () => { if (url.endsWith("/token-balances")) return [USDC_ROW]; if (url.endsWith("/transactions")) { return { items: [HISTORY_ITEM] }; } return { items: [] }; }, text: async () => PHISHING_PAGE, }; } async function recordAndAnswer(url) { asked.push(String(url)); return answer(String(url)); } beforeEach(() => { asked = []; globalThis.fetch = jest.fn(recordAndAnswer); }); afterEach(() => { globalThis.fetch = realFetch; delete globalThis.chrome; }); // Storage holds `profile` as the stored profile, or nothing when it is // undefined. function storing(profile) { globalThis.chrome = { storage: makeStorageStub( profile === undefined ? {} : { autistmask: profile }, ), }; } test("Blockscout's token balances are asked for only while Use Blockscout is on", async () => { storing({ useBlockscout: false }); expect(await fetchTokenBalances(ADDRESS, BLOCKSCOUT, [])).toEqual([]); expect(asked).toEqual([]); storing({ useBlockscout: true }); const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, []); expect(rows.map((row) => row.symbol)).toEqual(["USDC"]); expect(asked).toEqual([ BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances", ]); }); test("the transaction history is asked for only while Use Blockscout is on", async () => { storing({ useBlockscout: false }); expect( await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"), ).toEqual([]); expect(asked).toEqual([]); storing({ useBlockscout: true }); await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"); expect(asked.sort()).toEqual([ BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-transfers?type=ERC-20", BLOCKSCOUT + "/addresses/" + ADDRESS + "/transactions", ]); }); test("the Etherscan label check is made only while the stored profile has Check recipients on Etherscan on", async () => { const provider = { getCode: async () => "0x", getTransactionCount: async () => 1, getBalance: async () => 0n, }; const checkWith = async (profile) => { storing(profile); await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" }); }; await checkWith(undefined); await checkWith({ checkRecipientsOnEtherscan: false }); // Only a stored true is on, as when the profile is loaded. await checkWith({ checkRecipientsOnEtherscan: "true" }); await checkWith({ checkRecipientsOnEtherscan: 42 }); expect(asked).toEqual([]); await checkWith({ checkRecipientsOnEtherscan: true }); expect(asked).toEqual(["https://etherscan.io/address/" + RECIPIENT]); }); // Asks with `setting` on in storage. Once every request is out, storage // holds `after` for the setting, as when the user changes it here or on // another page. Only then does each request get its answer. async function answeredAfter(setting, after, ask) { storing({ [setting]: true }); let release; const held = new Promise((resolve) => { release = resolve; }); let requested; const out = new Promise((resolve) => { requested = resolve; }); globalThis.fetch = jest.fn(async (url) => { requested(); await held; return recordAndAnswer(url); }); const result = ask(); await out; globalThis.chrome.storage.write("autistmask", { [setting]: after }); release(); return result; } // One row per request, giving: // // - its name // - the setting that gates it // - how to make it // - its answer while that setting is off test.each([ [ "Blockscout's token balances", "useBlockscout", () => fetchTokenBalances(ADDRESS, BLOCKSCOUT, []), [], ], [ "the transaction history", "useBlockscout", () => fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"), [], ], [ "the Etherscan label check", "checkRecipientsOnEtherscan", () => checkEtherscanLabel(RECIPIENT, "0x1"), null, ], ])( "%s keeps an answer that lands while its setting is on, and drops one that lands after it went off", async (_what, setting, ask, dropped) => { expect(await answeredAfter(setting, true, ask)).not.toEqual( dropped, ); expect(await answeredAfter(setting, false, ask)).toEqual(dropped); }, ); describe("a balance refresh", () => { // The RPC endpoint answers every balance with zero. The ENS reverse // lookup makes the only other calls, and it finds no resolver. beforeEach(() => { FetchRequest.registerGetUrl(async (req) => { const body = JSON.parse(Buffer.from(req.body).toString("utf8")); const reply = (r) => ({ jsonrpc: "2.0", id: r.id, result: r.method === "eth_getBalance" ? "0x0" : ZERO_WORD, }); const answer = Array.isArray(body) ? body.map(reply) : reply(body); return { statusCode: 200, statusMessage: "OK", headers: { "content-type": "application/json" }, body: Buffer.from(JSON.stringify(answer)), }; }); }); afterEach(() => { FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc()); }); // An address refreshed while "Use Blockscout" was on. It holds the USDC // that Blockscout reported then. function addressWithUsdc() { return { address: ADDRESS, tokenBalances: [ { address: USDC_ROW.token.address_hash, name: "USD Coin", symbol: "USDC", decimals: 6, balance: "5.0", holders: 50000, }, ], }; } async function refresh(addr, useBlockscout) { storing({ useBlockscout }); await refreshBalances( [{ addresses: [addr] }], RPC_URL, BLOCKSCOUT, [], "mainnet", ); } test("asks Blockscout nothing while Use Blockscout is off, and then lists no token the user has not tracked", async () => { const addr = addressWithUsdc(); await refresh(addr, false); expect(asked).toEqual([]); expect(addr.tokenBalances).toEqual([]); }); test("asks Blockscout for the token balances while Use Blockscout is on", async () => { const addr = addressWithUsdc(); await refresh(addr, true); expect(asked).toEqual([ BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances", ]); expect(addr.tokenBalances.map((row) => row.symbol)).toEqual([ "USDC", ]); }); }); }); describe("the popup", () => { // Every URL the booted page handed to fetch(). function fetched() { return globalThis.fetch.mock.calls.map((call) => String(call[0])); } function blockscoutOrEtherscan(url) { return url.includes("blockscout") || url.includes("etherscan"); } beforeEach(() => { // The screens below log their failed RPC reads; the stub node in // popupBoot.js answers none. jest.spyOn(console, "error").mockImplementation(() => {}); }); test("Home says why there is no history and only tracked tokens while Use Blockscout is off", async () => { const page = await bootPopup(unversionedValidProfile()); expect(page.node("home-tx-list").innerHTML).toContain( escapeHtml(HISTORY_OFF_TEXT), ); expect(page.hidden("home-tokens-note")).toBe(false); cleanupPopup(); const on = await bootPopup( unversionedValidProfile({ useBlockscout: true }), ); expect(on.node("home-tx-list").innerHTML).toContain( "No transactions found.", ); expect(on.hidden("home-tokens-note")).toBe(true); }); // The transaction screen, as a reopened popup draws it. function onTransactionScreen(extra) { return unversionedValidProfile({ currentView: "transaction", viewStack: ["main"], selectedWallet: 0, selectedAddress: 0, viewData: { tx: { hash: TX_HASH, from: ADDRESS, to: RECIPIENT, value: "1.0000", exactValue: "1.0", symbol: "ETH", timestamp: 1790000000, isError: false, direction: "sent", directionLabel: "Sent", contractAddress: null, chainId: "0x1", }, }, ...extra, }); } test("TransactionDetail asks Blockscout for the transaction only while Use Blockscout is on", async () => { const page = await bootPopup(onTransactionScreen()); expect(page.visibleViews()).toEqual(["transaction"]); expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]); expect(page.hidden("tx-detail-blockscout-note")).toBe(false); cleanupPopup(); const on = await bootPopup( onTransactionScreen({ useBlockscout: true }), ); expect(fetched().filter(blockscoutOrEtherscan)).toEqual([ BLOCKSCOUT + "/transactions/" + TX_HASH, ]); expect(on.hidden("tx-detail-blockscout-note")).toBe(true); }); // Another page turns "Use Blockscout" off while TransactionDetail's request // to Blockscout is out. This page draws TransactionDetail again with its // line, and only then does Blockscout answer. test("TransactionDetail shows nothing from an answer that lands after another page turned Use Blockscout off", async () => { const page = await bootPopup( onTransactionScreen({ useBlockscout: true }), ); let release; const held = new Promise((resolve) => { release = resolve; }); globalThis.fetch = jest.fn(async () => { await held; return { ok: true, status: 200, json: async () => ({ block_number: 21000000, fee: { value: "21000000000000" }, raw_input: "0x12345678", method: "doSomething", }), }; }); require("../src/popup/views/transactionDetail").render(); await page.settle(); expect(fetched()).toEqual([BLOCKSCOUT + "/transactions/" + TX_HASH]); await anotherPageAnnounces(page, { useBlockscout: false }); release(); await page.settle(); expect(page.visibleViews()).toEqual(["transaction"]); expect(page.hidden("tx-detail-blockscout-note")).toBe(false); for (const id of [ "tx-detail-block-section", "tx-detail-network-section", "tx-detail-calldata-section", "tx-detail-rawdata-section", ]) { expect(page.hidden(id)).toBe(true); } expect(fetched()).toHaveLength(1); }); // The confirmation screen for a send of 0.1 ETH to RECIPIENT, as a // reopened popup draws it. function onConfirmScreen(extra) { return unversionedValidProfile({ currentView: "confirm-tx", viewStack: ["main"], selectedWallet: 0, selectedAddress: 0, viewData: { pendingTx: { token: "ETH", from: ADDRESS, to: RECIPIENT, amount: "0.1", chainId: "0x1", }, }, ...extra, }); } test("ConfirmTx asks Etherscan about the recipient only while Check recipients on Etherscan is on", async () => { const page = await bootPopup(onConfirmScreen()); expect(page.visibleViews()).toEqual(["confirm-tx"]); expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]); expect(page.hidden("confirm-etherscan-note")).toBe(false); expect(page.hidden("confirm-etherscan-warning")).toBe(true); cleanupPopup(); const on = await bootPopup( onConfirmScreen({ checkRecipientsOnEtherscan: true }), ); expect(fetched().filter(blockscoutOrEtherscan)).toEqual([ "https://etherscan.io/address/" + RECIPIENT, ]); expect(on.hidden("confirm-etherscan-note")).toBe(true); expect(on.hidden("confirm-etherscan-warning")).toBe(false); }); // Ticks or unticks a checkbox on the page, as the user does. async function setBox(page, id, checked) { const box = page.node(id); box.checked = checked; for (const fn of box.listeners.change) await fn(); await page.settle(); } test("Welcome offers both settings unticked, and ticking one there stores it", async () => { const page = await bootPopup(undefined); expect(page.visibleViews()).toEqual(["welcome"]); expect(page.node("welcome-use-blockscout").checked).toBe(false); expect(page.node("welcome-check-recipients-on-etherscan").checked).toBe( false, ); await setBox(page, "welcome-use-blockscout", true); expect(page.storage.read("autistmask").useBlockscout).toBe(true); await setBox(page, "welcome-check-recipients-on-etherscan", true); expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe( true, ); }); test("Settings shows both settings unticked, and ticking one there stores it", async () => { const page = await bootPopup(unversionedValidProfile()); await page.click("btn-settings"); expect(page.node("settings-use-blockscout").checked).toBe(false); expect( page.node("settings-check-recipients-on-etherscan").checked, ).toBe(false); await setBox(page, "settings-use-blockscout", true); expect(page.storage.read("autistmask").useBlockscout).toBe(true); await setBox(page, "settings-check-recipients-on-etherscan", true); expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe( true, ); }); // A token the user tracks, and an untracked one that Blockscout reported. // Both are written in lower case, because the balance rows' and the // dropdown's token ids are compared in lower case below. const TRACKED = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; const UNTRACKED = TOKEN_ADDRESS.toLowerCase(); const SECOND_ADDRESS = "0x3333333333333333333333333333333333333333"; function tokenRow(address, symbol) { return { address, name: symbol, symbol, decimals: 18, balance: "5.0", holders: 50000, }; } // AddressDetail of the first of two addresses, as a reopened popup draws // it. Both addresses hold the tracked token. While "Use Blockscout" is on, // both hold the untracked one too. function onAddressDetail(useBlockscout) { const profile = unversionedValidProfile({ useBlockscout, currentView: "address", viewStack: ["main"], selectedWallet: 0, selectedAddress: 0, trackedTokens: [ { address: TRACKED, name: "TRK", symbol: "TRK", decimals: 18 }, ], }); const wallet = profile.wallets[0]; wallet.nextIndex = 2; wallet.addresses.push({ address: SECOND_ADDRESS, balance: "0.0" }); for (const addr of wallet.addresses) { addr.tokenBalances = useBlockscout ? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")] : [tokenRow(TRACKED, "TRK")]; } return profile; } // The token ids of the options created after the first `from` elements. // The page stub keeps no children, so this reads the options for Send's // dropdown from the elements the page created. function optionsSince(page, from) { return page.document.created .slice(from) .filter((el) => el.tagName === "OPTION") .map((el) => el.value.toLowerCase()); } test("unticking Use Blockscout from AddressDetail's gear takes the untracked token off AddressDetail, Send's dropdown and Home on Back", async () => { const page = await bootPopup(onAddressDetail(true)); expect(page.visibleViews()).toEqual(["address"]); expect(page.node("address-balances").innerHTML).toContain(UNTRACKED); await page.click("btn-settings"); await setBox(page, "settings-use-blockscout", false); const from = page.document.created.length; await page.click("btn-settings-back"); expect(page.visibleViews()).toEqual(["address"]); const balances = page.node("address-balances").innerHTML; expect(balances).toContain(TRACKED); expect(balances).not.toContain(UNTRACKED); expect(page.hidden("address-tokens-note")).toBe(false); expect(optionsSince(page, from)).toEqual([TRACKED]); await page.click("btn-send"); expect(page.visibleViews()).toEqual(["send"]); expect(page.hidden("send-token-dropdown")).toBe(false); expect(page.hidden("send-tokens-note")).toBe(false); await page.click("btn-send-back"); await page.click("btn-address-back"); expect(page.visibleViews()).toEqual(["main"]); const home = page.node("wallet-list").innerHTML; expect(home).toContain(TRACKED); expect(home).not.toContain(UNTRACKED); const stored = page.storage.read("autistmask"); for (const addr of stored.wallets[0].addresses) { expect( addr.tokenBalances.map((row) => row.address.toLowerCase()), ).toEqual([TRACKED]); } }); // Send's dropdown was filled under the old setting, and Back cannot draw // Send again. So Back lands on the screen under it. test.each([ ["unticking", true], ["ticking", false], ])( "%s Use Blockscout from Send's gear lands Back on AddressDetail", async (_how, before) => { const page = await bootPopup(onAddressDetail(before)); await page.click("btn-send"); expect(page.visibleViews()).toEqual(["send"]); await page.click("btn-settings"); await setBox(page, "settings-use-blockscout", !before); await page.click("btn-settings-back"); expect(page.visibleViews()).toEqual(["address"]); expect(page.hidden("address-tokens-note")).toBe(!before); }, ); test("Send says under its token dropdown that only tracked tokens are listed while Use Blockscout is off", async () => { const off = await bootPopup(onAddressDetail(false)); await off.click("btn-send"); expect(off.hidden("send-token-dropdown")).toBe(false); expect(off.hidden("send-tokens-note")).toBe(false); cleanupPopup(); const on = await bootPopup(onAddressDetail(true)); await on.click("btn-send"); expect(on.hidden("send-token-dropdown")).toBe(false); expect(on.hidden("send-tokens-note")).toBe(true); }); test("Send opened from AddressToken shows neither the dropdown nor its line", async () => { const page = await bootPopup({ ...onAddressDetail(false), currentView: "address-token", viewStack: ["main", "address"], selectedToken: "ETH", }); expect(page.visibleViews()).toEqual(["address-token"]); await page.click("btn-address-token-send"); expect(page.visibleViews()).toEqual(["send"]); expect(page.hidden("send-token-dropdown")).toBe(true); expect(page.hidden("send-token-static")).toBe(false); }); test("DeleteAddress says only tracked tokens are listed while Use Blockscout is off", async () => { for (const useBlockscout of [false, true]) { const page = await bootPopup(onAddressDetail(useBlockscout)); require("../src/popup/views/deleteAddress").show(0, 1); expect(page.visibleViews()).toEqual(["delete-address-confirm"]); expect(page.hidden("delete-address-tokens-note")).toBe( useBlockscout, ); cleanupPopup(); } }); // The token ids of the first address's balances in the stored profile. function storedTokens(page) { return page.storage .read("autistmask") .wallets[0].addresses[0].tokenBalances.map((row) => row.address.toLowerCase(), ); } // Turning the setting off dropped the untracked token. A refresh that // started while the setting was on still saves its balances when it // returns, but not that token. The same refresh in the background is in // tests/backgroundStateIsolation.test.js. test("a refresh that returns after Use Blockscout was turned off saves its balances without the untracked token", async () => { const page = await bootPopup(onAddressDetail(true)); const { refreshBalances: held } = require("../src/shared/balances"); let answer; held.mockImplementationOnce(async (wallets) => { await new Promise((resolve) => { answer = resolve; }); wallets[0].addresses[0].balance = "7.0"; wallets[0].addresses[0].tokenBalances = [ tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST"), ]; }); const ticking = page.tick(); await page.settle(); await page.click("btn-settings"); await setBox(page, "settings-use-blockscout", false); answer(); await ticking; expect( page.storage.read("autistmask").wallets[0].addresses[0].balance, ).toBe("7.0"); expect(storedTokens(page)).toEqual([TRACKED]); await page.click("btn-settings-back"); expect(page.node("address-balances").innerHTML).not.toContain( UNTRACKED, ); }); // Another page saves `change` over the stored profile. The browser is not // made to tell this page, so this page sees the change only by reading the // stored profile. function anotherPageSaves(page, change) { page.storage.write("autistmask", { ...page.storage.read("autistmask"), ...change, }); } test.each([ ["off", true, false], ["on", false, true], ])( "after another page turns Use Blockscout %s, this page takes the change and its next refresh is saved", async (_how, before, after) => { const page = await bootPopup(onAddressDetail(before)); // The refresh answers as it does under the stored setting. const { refreshBalances: held } = require("../src/shared/balances"); held.mockImplementationOnce(async (wallets) => { const addr = wallets[0].addresses[0]; addr.balance = "7.0"; addr.tokenBalances = after ? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")] : [tokenRow(TRACKED, "TRK")]; }); anotherPageSaves(page, { useBlockscout: after }); await page.tick(); expect( page.storage.read("autistmask").wallets[0].addresses[0].balance, ).toBe("7.0"); expect(storedTokens(page)).toEqual( after ? [TRACKED, UNTRACKED] : [TRACKED], ); expect(page.hidden("address-tokens-note")).toBe(after); }, ); test.each([ ["off", true, false], ["on", false, true], ])( "after another page turns Check recipients on Etherscan %s, this page's next Etherscan check follows the stored setting", async (_how, before, after) => { const page = await bootPopup( onConfirmScreen({ checkRecipientsOnEtherscan: before }), ); anotherPageSaves(page, { checkRecipientsOnEtherscan: after }); const from = fetched().length; require("../src/popup/views/confirmTx").show({ token: "ETH", from: ADDRESS, to: RECIPIENT, amount: "0.1", chainId: "0x1", }); await page.settle(); // The page also takes the change and draws ConfirmTx again, which // checks once more. So the test counts each URL once, however often // it was asked for. const asked = new Set( fetched().slice(from).filter(blockscoutOrEtherscan), ); expect([...asked]).toEqual( after ? ["https://etherscan.io/address/" + RECIPIENT] : [], ); }, ); // Another page's save of the whole profile with `change`, which the // browser announces to this page as it announces any write. async function anotherPageAnnounces(page, change) { await page.storage.local.set({ autistmask: { ...page.storage.read("autistmask"), ...change }, }); await page.settle(); } test("a page open on AddressDetail draws it again when another page turns Use Blockscout off", async () => { const page = await bootPopup(onAddressDetail(true)); expect(page.hidden("address-tokens-note")).toBe(true); await anotherPageAnnounces(page, { useBlockscout: false }); expect(page.visibleViews()).toEqual(["address"]); expect(page.hidden("address-tokens-note")).toBe(false); expect(page.node("address-balances").innerHTML).not.toContain( UNTRACKED, ); expect(storedTokens(page)).toEqual([TRACKED]); }); test("a page open on ConfirmTx draws it again when another page turns Check recipients on Etherscan off", async () => { const page = await bootPopup( onConfirmScreen({ checkRecipientsOnEtherscan: true }), ); expect(page.hidden("confirm-etherscan-note")).toBe(true); await anotherPageAnnounces(page, { checkRecipientsOnEtherscan: false }); expect(page.visibleViews()).toEqual(["confirm-tx"]); expect(page.hidden("confirm-etherscan-note")).toBe(false); }); });