// The two add-token screens do not report a token lookup the popup's own // closing cancelled, and still report one that failed while the popup was open // (https://git.eeqj.de/sneak/AutistMask/issues/475). // // In the popup a cancelled request fails with the same "Failed to fetch" as a // server that cannot be reached, so every RPC request here fails that way, and // only the signal the popup aborts on pagehide tells the two cases apart. The // real lookupTokenInfo() runs, which logs a failed lookup itself before the // screen does. Driven against the fake elements tests/flashLine.test.js uses. // The last tests call lookupTokenInfo() directly with symbol() answered, so // that its later calls are the ones that fail. const { FetchRequest, Interface, toUtf8Bytes, toUtf8String, } = require("ethers"); const { ERC20_ABI } = require("../src/shared/constants"); const ADDRESS = "0x1111111111111111111111111111111111111111"; const RPC_URL = "https://rpc.example.invalid"; let elements; function fakeElement() { return { value: "", textContent: "", style: {}, listeners: {}, addEventListener(event, handler) { this.listeners[event] = handler; }, }; } // Stands in for document.getElementById(): one fake element per id. function element(id) { return (elements[id] ||= fakeElement()); } jest.doMock("../src/popup/views/helpers", () => ({ $: element, showView: () => {}, showFlash: () => {}, escapeHtml: (s) => s, goBack: () => {}, })); // state.js reads chrome.storage.local at load. globalThis.chrome = { storage: { local: { get: async () => ({}), set: async () => {} } }, }; const { state } = require("../src/shared/state"); const { lookupTokenInfo } = require("../src/shared/balances"); let logged; beforeEach(() => { elements = {}; logged = []; state.trackedTokens = []; for (const method of ["warn", "error"]) { jest.spyOn(console, method).mockImplementation((...args) => { logged.push(args.map(String).join(" ")); }); } FetchRequest.registerGetUrl(async () => { throw new TypeError("Failed to fetch"); }); }); afterEach(() => { FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc()); jest.restoreAllMocks(); }); describe.each([ ["addToken", "add-token-address", "btn-add-token-confirm"], [ "settingsAddToken", "settings-addtoken-address", "btn-settings-addtoken-manual", ], ])("looking up a token on %s", (view, field, button) => { // Clicks the screen's add button for ADDRESS and waits for the lookup. async function add(pageClosed) { require("../src/popup/views/" + view).init({ doRefreshAndRender: () => {}, pageClosed, }); element(field).value = ADDRESS; await element(button).listeners.click(); } test("a failure while the popup is open is reported", async () => { await add(new AbortController().signal); expect(logged).toHaveLength(2); expect(logged[0]).toContain("symbol() failed:"); expect(logged[1]).toBe( "[AutistMask] Adding token failed for " + ADDRESS + " Not a valid ERC-20 token (symbol() failed).", ); }); test("a failure once the popup has closed is not", async () => { const pageClosed = new AbortController(); pageClosed.abort(); await add(pageClosed.signal); expect(logged).toEqual([]); }); }); // Answers the contract call for each function named in `answers` with its // value, and fails every other request as above. function answer(answers) { const erc20 = new Interface(ERC20_ABI); FetchRequest.registerGetUrl(async (req) => { const { id, params } = JSON.parse(toUtf8String(req.body)); const { name } = erc20.parseTransaction({ data: params[0].data }); if (!(name in answers)) { throw new TypeError("Failed to fetch"); } const result = erc20.encodeFunctionResult(name, [answers[name]]); return { statusCode: 200, statusMessage: "OK", headers: {}, body: toUtf8Bytes(JSON.stringify({ jsonrpc: "2.0", id, result })), }; }); } describe.each([ ["decimals() fails", { symbol: "TKN" }, "decimals() failed:"], [ "name() fails", { symbol: "TKN", decimals: 18 }, "name() failed, using symbol as name:", ], ])("a token lookup where %s", (_, answers, report) => { async function lookUp(pageClosed) { // A token found is logged at info level, which is not under test. jest.spyOn(console, "log").mockImplementation(() => {}); answer(answers); // A failed decimals() also fails the lookup, which the screens report // as tested above. await lookupTokenInfo(ADDRESS, RPC_URL, "mainnet", pageClosed).catch( () => {}, ); } test("is reported while the popup is open", async () => { await lookUp(new AbortController().signal); expect(logged).toEqual([expect.stringContaining(report)]); }); test("is not once the popup has closed", async () => { const pageClosed = new AbortController(); pageClosed.abort(); await lookUp(pageClosed.signal); expect(logged).toEqual([]); }); });