// The address and token screens do not report a transaction list 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 fetch() fails with the same "Failed to fetch" as a // server that cannot be reached, so the explorer request here always fails // 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. jest.mock("../src/shared/transactions", () => ({ ...jest.requireActual("../src/shared/transactions"), fetchRecentTransactions: 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"; 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(() => { 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 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([]); }); });