fix: a popup reload no longer logs the requests it cancels, except on the transaction detail and confirmation screens (closes #475)
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
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user