// The address and token screens do not report a transaction list, or an ENS // name lookup for the addresses in it, that 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 the requests here fail that way, and only // the signal the popup aborts on pagehide tells the two cases apart. Driven // against a minimal DOM stub in the shape tests/timestampDisplay.test.js uses. // The explorer answers with mockHistory, and fails when it is null. let mockHistory = null; jest.mock("../src/shared/transactions", () => ({ ...jest.requireActual("../src/shared/transactions"), fetchRecentTransactions: async () => { if (mockHistory === null) throw new TypeError("Failed to fetch"); return mockHistory; }, })); // Every ENS name lookup fails. jest.mock("../src/shared/balances", () => ({ ...jest.requireActual("../src/shared/balances"), getProvider: () => ({ lookupAddress: async () => { throw new TypeError("Failed to fetch"); }, }), })); globalThis.chrome = { storage: { local: { get: async () => ({}), set: async () => {} } }, }; const { state } = require("../src/shared/state"); const addressDetail = require("../src/popup/views/addressDetail"); const addressToken = require("../src/popup/views/addressToken"); const ADDRESS = "0x1111111111111111111111111111111111111111"; const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a"; // A transaction ADDRESS sent, as the history lists hold it. function historyTx() { return { hash: "0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84", from: ADDRESS, to: RECIPIENT, value: "0.0000", exactValue: "0.0", rawAmount: "0", rawUnit: "wei", symbol: "ETH", timestamp: 1790000000, isError: false, directionLabel: "Sent", direction: "sent", contractAddress: null, }; } function makeElement(id) { const el = { id, textContent: "", value: "", innerHTML: "", style: {}, dataset: {}, classList: { add: () => {}, remove: () => {}, contains: () => false, toggle: () => false, }, addEventListener: () => {}, querySelectorAll: () => [], appendChild: () => {}, }; // Views reach for .parentElement to hide whole sections. Object.defineProperty(el, "parentElement", { get: () => node(id + "-parent"), }); return el; } function makeDocument() { const els = new Map(); return { getElementById(id) { // The debug banner is created on demand by helpers.js; absent // is the state a non-debug, non-testnet popup is in. if (id === "debug-banner") return null; if (!els.has(id)) els.set(id, makeElement(id)); return els.get(id); }, createElement: () => makeElement("created"), addEventListener: () => {}, body: { prepend: () => {} }, }; } function node(id) { return globalThis.document.getElementById(id); } let logged; beforeEach(() => { mockHistory = null; logged = []; jest.spyOn(console, "error").mockImplementation((...args) => { logged.push(args.map(String).join(" ")); }); globalThis.document = makeDocument(); globalThis.window = { location: { search: "" } }; state.wallets = [ { name: "Main", type: "key", addresses: [{ address: ADDRESS, balance: "0.0000" }], }, ]; state.trackedTokens = []; state.viewStack = []; state.selectedWallet = 0; state.selectedAddress = 0; state.selectedToken = "ETH"; }); afterEach(() => { jest.restoreAllMocks(); }); describe.each([ ["the address screen", addressDetail, "tx-list"], ["the token screen", addressToken, "address-token-tx-list"], ])("the transaction list on %s", (_name, view, listId) => { // Open the screen and wait for its transaction list to load or fail. async function open(pageClosed) { view.init({ pageClosed }); view.show(); await new Promise((resolve) => setTimeout(resolve, 0)); } test("a failure while the popup is open is reported", async () => { await open(new AbortController().signal); expect(logged).toEqual([ "[AutistMask] loadTransactions failed: Failed to fetch", ]); expect(node(listId).innerHTML).toContain( "Failed to load transactions.", ); }); test("a failure once the popup has closed is not", async () => { const pageClosed = new AbortController(); pageClosed.abort(); await open(pageClosed.signal); expect(logged).toEqual([]); }); test("a name lookup that fails while the popup is open is reported", async () => { mockHistory = [historyTx()]; await open(new AbortController().signal); expect(logged).toContain( "[AutistMask] ENS reverse lookup failed " + RECIPIENT + " Failed to fetch", ); expect(node(listId).innerHTML).toContain("tx-row"); }); test("a name lookup that fails once the popup has closed is not", async () => { mockHistory = [historyTx()]; const pageClosed = new AbortController(); pageClosed.abort(); await open(pageClosed.signal); expect(logged).toEqual([]); expect(node(listId).innerHTML).toContain("tx-row"); }); });