The transaction lists and ENS name lookups on the address and token screens, the address scan after a wallet is created, the endpoint checks in Settings, the wait screen's receipt check, the Send screen's Max fee estimate and the token lookup on both add-token screens now check the signal the popup aborts on pagehide before reporting a failed request. scanForAddresses(), resolveEnsNames() and lookupTokenInfo() take the signal. End-to-end tests reload the popup on the address screen and during the address scan with their requests held. Jest tests show each of these reports a real failure and stays silent once the popup has closed. The transaction detail and confirmation screens are left out: they discard the popup context that carries the signal. Model: opus-5-5
182 lines
5.6 KiB
JavaScript
182 lines
5.6 KiB
JavaScript
// 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");
|
|
});
|
|
});
|