1 Commits
Author SHA1 Message Date
clawbot f8f2461354 fix: a popup reload no longer logs the requests it cancels, except on the transaction detail and confirmation screens (closes #475)
check / check (push) Successful in 1m57s
e2e / e2e-chrome (push) Successful in 4m53s
e2e / e2e-firefox (push) Successful in 2m31s
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
2026-10-06 12:34:54 +00:00
14 changed files with 309 additions and 36 deletions
+11 -7
View File
@@ -46,16 +46,20 @@ but the review is broader than any of them.
# Completed Steps # Completed Steps
- 2026-10-06: Reloading or closing the popup no longer logs a request it cancels - 2026-10-06: Reloading or closing the popup no longer logs a request it cancels
as a failure on the address and token screens, in the address scan after a as a failure in the transaction lists and ENS name lookups on the address and
wallet is created, or in the endpoint checks in Settings 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, or the token lookup on the two add-token screens
([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)). Each checks the ([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)). Each checks the
signal the popup aborts on `pagehide` signal the popup aborts on `pagehide`
([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)) before reporting a ([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)) before reporting a
failure; a real failure is still logged. The e2e suite reloads the popup on failure; a real failure is still logged. `scanForAddresses()`,
the address screen with Blockscout held, and during the address scan with that `resolveEnsNames()` and `lookupTokenInfo()` take the signal. The e2e suite
scan held; jest tests cover the transaction lists, the scan and the Settings reloads the popup on the address screen with Blockscout held, and during the
checks. The transaction detail and confirmation screens are not covered: they address scan with that scan held; jest tests cover each of these with the
do not keep the popup context that carries the signal. popup open and closed. Left out: the transaction detail screen and the
confirmation screen (its fee estimate and its recipient checks), because they
discard the popup context that carries the signal.
- 2026-10-06: Reloading or closing the popup mid-refresh no longer logs the - 2026-10-06: Reloading or closing the popup mid-refresh no longer logs the
requests that cancels as failures requests that cancels as failures
+4
View File
@@ -51,6 +51,7 @@ function init(ctx) {
contractAddr, contractAddr,
state.rpcUrl, state.rpcUrl,
state.networkId, state.networkId,
ctx.pageClosed,
); );
log.infof("Adding token", info.symbol, contractAddr); log.infof("Adding token", info.symbol, contractAddr);
state.trackedTokens.push({ state.trackedTokens.push({
@@ -68,6 +69,9 @@ function init(ctx) {
} }
require("./addressDetail").show(); require("./addressDetail").show();
} catch (e) { } catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
const detail = e.shortMessage || e.message || String(e); const detail = e.shortMessage || e.message || String(e);
log.errorf("Adding token failed for", contractAddr, detail); log.errorf("Adding token failed for", contractAddr, detail);
// lookupTokenInfo() rejects a contract with a one-line message // lookupTokenInfo() rejects a contract with a one-line message
+1
View File
@@ -135,6 +135,7 @@ async function loadTransactions(address) {
counterparties, counterparties,
state.rpcUrl, state.rpcUrl,
state.networkId, state.networkId,
ctx.pageClosed,
); );
} catch { } catch {
ensNameMap = new Map(); ensNameMap = new Map();
+1
View File
@@ -219,6 +219,7 @@ async function loadTransactions(address, tokenId) {
counterparties, counterparties,
state.rpcUrl, state.rpcUrl,
state.networkId, state.networkId,
ctx.pageClosed,
); );
} catch { } catch {
ensNameMap = new Map(); ensNameMap = new Map();
+3
View File
@@ -292,6 +292,9 @@ async function fillMaxAmount() {
]); ]);
feeWei = feeReserveWei(gasLimit, feeData); feeWei = feeReserveWei(gasLimit, feeData);
} catch (e) { } catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
log.errorf( log.errorf(
"max amount fee estimate failed:", "max amount fee estimate failed:",
e.shortMessage || e.message, e.shortMessage || e.message,
+4
View File
@@ -135,6 +135,7 @@ function init(_ctx) {
addr, addr,
state.rpcUrl, state.rpcUrl,
state.networkId, state.networkId,
ctx.pageClosed,
); );
log.infof("Adding token", info.symbol, addr); log.infof("Adding token", info.symbol, addr);
state.trackedTokens.push({ state.trackedTokens.push({
@@ -152,6 +153,9 @@ function init(_ctx) {
renderDropdown(); renderDropdown();
ctx.doRefreshAndRender(); ctx.doRefreshAndRender();
} catch (e) { } catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
const detail = e.shortMessage || e.message || String(e); const detail = e.shortMessage || e.message || String(e);
log.errorf("Adding token failed for", addr, detail); log.errorf("Adding token failed for", addr, detail);
// lookupTokenInfo() rejects a contract with a one-line message // lookupTokenInfo() rejects a contract with a one-line message
+3
View File
@@ -132,6 +132,9 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
try { try {
receipt = await provider.getTransactionReceipt(txHash); receipt = await provider.getTransactionReceipt(txHash);
} catch (e) { } catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
// A thrown lookup means "no answer this tick", not "no // A thrown lookup means "no answer this tick", not "no
// receipt": the RPC failed, the chain said nothing. Declaring // receipt": the RPC failed, the chain said nothing. Declaring
// the timeout off it would report a confirmed transaction as // the timeout off it would report a confirmed transaction as
+14 -7
View File
@@ -294,7 +294,8 @@ async function refreshBalances(
// Look up token metadata from its contract. // Look up token metadata from its contract.
// Calls symbol() and decimals() to verify it implements ERC-20. // Calls symbol() and decimals() to verify it implements ERC-20.
async function lookupTokenInfo(contractAddress, rpcUrl, networkId) { // `signal` is as for fetchTokenBalances().
async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
log.debugf("lookupTokenInfo", contractAddress, "rpc:", urlOrigin(rpcUrl)); log.debugf("lookupTokenInfo", contractAddress, "rpc:", urlOrigin(rpcUrl));
const provider = getProvider(rpcUrl, networkId); const provider = getProvider(rpcUrl, networkId);
const contract = new Contract(contractAddress, ERC20_ABI, provider); const contract = new Contract(contractAddress, ERC20_ABI, provider);
@@ -304,7 +305,9 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
symbol = await contract.symbol(); symbol = await contract.symbol();
log.debugf("symbol() =", symbol); log.debugf("symbol() =", symbol);
} catch (e) { } catch (e) {
log.errorf("symbol() failed:", e.shortMessage || e.message); if (!signal?.aborted) {
log.errorf("symbol() failed:", e.shortMessage || e.message);
}
throw new Error("Not a valid ERC-20 token (symbol() failed)."); throw new Error("Not a valid ERC-20 token (symbol() failed).");
} }
@@ -312,7 +315,9 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
decimals = await contract.decimals(); decimals = await contract.decimals();
log.debugf("decimals() =", decimals); log.debugf("decimals() =", decimals);
} catch (e) { } catch (e) {
log.errorf("decimals() failed:", e.shortMessage || e.message); if (!signal?.aborted) {
log.errorf("decimals() failed:", e.shortMessage || e.message);
}
throw new Error("Not a valid ERC-20 token (decimals() failed)."); throw new Error("Not a valid ERC-20 token (decimals() failed).");
} }
@@ -320,10 +325,12 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
name = await contract.name(); name = await contract.name();
log.debugf("name() =", name); log.debugf("name() =", name);
} catch (e) { } catch (e) {
log.warnf( if (!signal?.aborted) {
"name() failed, using symbol as name:", log.warnf(
e.shortMessage || e.message, "name() failed, using symbol as name:",
); e.shortMessage || e.message,
);
}
name = symbol; name = symbol;
} }
+14 -8
View File
@@ -32,7 +32,8 @@ function setCache(address, name) {
localStorage.setItem(key, JSON.stringify({ name, ts: Date.now() })); localStorage.setItem(key, JSON.stringify({ name, ts: Date.now() }));
} }
async function resolveEnsName(address, rpcUrl, networkId) { // `signal` is as for fetchTokenBalances() in src/shared/balances.js.
async function resolveEnsName(address, rpcUrl, networkId, signal) {
const cached = getCached(address); const cached = getCached(address);
if (cached !== undefined) return cached; if (cached !== undefined) return cached;
@@ -42,21 +43,26 @@ async function resolveEnsName(address, rpcUrl, networkId) {
setCache(address, name); setCache(address, name);
return name; return name;
} catch (e) { } catch (e) {
log.errorf( if (!signal?.aborted) {
"ENS reverse lookup failed", log.errorf(
address, "ENS reverse lookup failed",
e.shortMessage || e.message, address,
); e.shortMessage || e.message,
);
}
// Don't cache failures — let subsequent lookups retry // Don't cache failures — let subsequent lookups retry
return null; return null;
} }
} }
async function resolveEnsNames(addresses, rpcUrl, networkId) { async function resolveEnsNames(addresses, rpcUrl, networkId, signal) {
const results = new Map(); const results = new Map();
await Promise.all( await Promise.all(
addresses.map(async (addr) => { addresses.map(async (addr) => {
results.set(addr, await resolveEnsName(addr, rpcUrl, networkId)); results.set(
addr,
await resolveEnsName(addr, rpcUrl, networkId, signal),
);
}), }),
); );
return results; return results;
+1
View File
@@ -91,6 +91,7 @@ describe.each([
require("../src/popup/views/" + view).init({ require("../src/popup/views/" + view).init({
doRefreshAndRender: () => {}, doRefreshAndRender: () => {},
pageClosed: new AbortController().signal,
}); });
element(field).value = ADDRESS; element(field).value = ADDRESS;
await element(button).listeners.click(); await element(button).listeners.click();
+51 -2
View File
@@ -65,7 +65,7 @@ jest.mock("../src/shared/log", () => ({
debugf: () => {}, debugf: () => {},
infof: () => {}, infof: () => {},
warnf: () => {}, warnf: () => {},
errorf: () => {}, errorf: jest.fn(),
}, },
// The explorer's token list, which refreshBalances() also fetches. // The explorer's token list, which refreshBalances() also fetches.
debugFetch: jest.fn(async () => ({ debugFetch: jest.fn(async () => ({
@@ -143,6 +143,7 @@ const { Transaction, Wallet, formatEther } = require("ethers");
const { refreshBalances } = require("../src/shared/balances"); const { refreshBalances } = require("../src/shared/balances");
const { encryptWithPassword } = require("../src/shared/vault"); const { encryptWithPassword } = require("../src/shared/vault");
const { state } = require("../src/shared/state"); const { state } = require("../src/shared/state");
const { log } = require("../src/shared/log");
const send = require("../src/popup/views/send"); const send = require("../src/popup/views/send");
const confirmTx = require("../src/popup/views/confirmTx"); const confirmTx = require("../src/popup/views/confirmTx");
@@ -226,10 +227,16 @@ function tokenRow(value, decimals = "18") {
// The confirmation screen Review leads to, once shown. // The confirmation screen Review leads to, once shown.
let confirmed = null; let confirmed = null;
// Stands in for the popup's: aborting it is the popup closing.
let pageClosed;
// Open the Send screen for `token` ("ETH" or a token address), with the // Open the Send screen for `token` ("ETH" or a token address), with the
// recipient entered. // recipient entered.
function openSend(token = "ETH") { function openSend(token = "ETH") {
send.init({ showConfirmTx: (info) => (confirmed = info) }); send.init({
showConfirmTx: (info) => (confirmed = info),
pageClosed: pageClosed.signal,
});
confirmTx.init({}); confirmTx.init({});
send.resetSendValidation(); send.resetSendValidation();
state.currentView = "send"; state.currentView = "send";
@@ -260,6 +267,8 @@ function canSend() {
beforeEach(() => { beforeEach(() => {
elements.clear(); elements.clear();
log.errorf.mockClear();
pageClosed = new AbortController();
confirmed = null; confirmed = null;
state.selectedToken = null; state.selectedToken = null;
state.trackedTokens = []; state.trackedTokens = [];
@@ -447,6 +456,46 @@ describe("Max on an ETH send", () => {
expect(text("flash-msg")).toBe(""); expect(text("flash-msg")).toBe("");
}); });
// Holds the node's fee answer until the returned function fails it, as a
// node that cannot be reached, or a request the popup's closing
// cancelled, does.
function holdFailingFeeEstimate() {
let fail;
mockNode.feeData = new Promise((_, reject) => {
fail = () => reject(new TypeError("Failed to fetch"));
});
return fail;
}
test("reports a fee estimate that fails while the popup is open", async () => {
await refreshWith(BALANCE_WEI);
openSend();
const fail = holdFailingFeeEstimate();
const pressed = pressMax();
fail();
await pressed;
expect(log.errorf).toHaveBeenCalledWith(
"max amount fee estimate failed:",
"Failed to fetch",
);
expect(text("flash-msg")).toBe(
"The network fee could not be estimated.",
);
});
// https://git.eeqj.de/sneak/AutistMask/issues/475
test("does not report a fee estimate that fails once the popup has closed", async () => {
await refreshWith(BALANCE_WEI);
openSend();
const fail = holdFailingFeeEstimate();
const pressed = pressMax();
pageClosed.abort();
fail();
await pressed;
expect(log.errorf).not.toHaveBeenCalled();
expect(el("send-amount").value).toBe("");
});
test("fills in once the held fee estimate arrives with nothing changed", async () => { test("fills in once the held fee estimate arrives with nothing changed", async () => {
await refreshWith(BALANCE_WEI); await refreshWith(BALANCE_WEI);
openSend(); openSend();
+103
View File
@@ -0,0 +1,103 @@
// 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.
const { FetchRequest } = require("ethers");
const ADDRESS = "0x1111111111111111111111111111111111111111";
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");
let logged;
beforeEach(() => {
elements = {};
logged = [];
state.trackedTokens = [];
jest.spyOn(console, "error").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([]);
});
});
+64 -10
View File
@@ -1,20 +1,33 @@
// The address and token screens do not report a transaction list the popup's // The address and token screens do not report a transaction list, or an ENS
// own closing cancelled, and still report one that failed while the popup was // name lookup for the addresses in it, that the popup's own closing cancelled,
// open (https://git.eeqj.de/sneak/AutistMask/issues/475). // 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 // In the popup a cancelled request fails with the same "Failed to fetch" as a
// server that cannot be reached, so the explorer request here always fails // server that cannot be reached, so the requests here fail that way, and only
// that way, and only the signal the popup aborts on pagehide tells the two // the signal the popup aborts on pagehide tells the two cases apart. Driven
// cases apart. Driven against a minimal DOM stub in the shape // against a minimal DOM stub in the shape tests/timestampDisplay.test.js uses.
// 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.mock("../src/shared/transactions", () => ({
...jest.requireActual("../src/shared/transactions"), ...jest.requireActual("../src/shared/transactions"),
fetchRecentTransactions: async () => { fetchRecentTransactions: async () => {
throw new TypeError("Failed to fetch"); 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 = { globalThis.chrome = {
storage: { local: { get: async () => ({}), set: async () => {} } }, storage: { local: { get: async () => ({}), set: async () => {} } },
}; };
@@ -24,6 +37,26 @@ const addressDetail = require("../src/popup/views/addressDetail");
const addressToken = require("../src/popup/views/addressToken"); const addressToken = require("../src/popup/views/addressToken");
const ADDRESS = "0x1111111111111111111111111111111111111111"; 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) { function makeElement(id) {
const el = { const el = {
@@ -73,6 +106,7 @@ function node(id) {
let logged; let logged;
beforeEach(() => { beforeEach(() => {
mockHistory = null;
logged = []; logged = [];
jest.spyOn(console, "error").mockImplementation((...args) => { jest.spyOn(console, "error").mockImplementation((...args) => {
logged.push(args.map(String).join(" ")); logged.push(args.map(String).join(" "));
@@ -101,7 +135,7 @@ describe.each([
["the address screen", addressDetail, "tx-list"], ["the address screen", addressDetail, "tx-list"],
["the token screen", addressToken, "address-token-tx-list"], ["the token screen", addressToken, "address-token-tx-list"],
])("the transaction list on %s", (_name, view, listId) => { ])("the transaction list on %s", (_name, view, listId) => {
// Open the screen and wait for its transaction list to fail. // Open the screen and wait for its transaction list to load or fail.
async function open(pageClosed) { async function open(pageClosed) {
view.init({ pageClosed }); view.init({ pageClosed });
view.show(); view.show();
@@ -124,4 +158,24 @@ describe.each([
await open(pageClosed.signal); await open(pageClosed.signal);
expect(logged).toEqual([]); 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");
});
}); });
+35 -2
View File
@@ -21,7 +21,7 @@ jest.mock("../src/shared/log", () => ({
debugf: () => {}, debugf: () => {},
infof: () => {}, infof: () => {},
warnf: () => {}, warnf: () => {},
errorf: () => {}, errorf: jest.fn(),
}, },
debugFetch: jest.fn(), debugFetch: jest.fn(),
setRuntimeDebug: () => {}, setRuntimeDebug: () => {},
@@ -108,6 +108,7 @@ global.chrome = { storage };
const txStatus = require("../src/popup/views/txStatus"); const txStatus = require("../src/popup/views/txStatus");
const { state } = require("../src/shared/state"); const { state } = require("../src/shared/state");
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews"); const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
const { log } = require("../src/shared/log");
const TX_HASH = const TX_HASH =
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84"; "0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
@@ -128,16 +129,24 @@ function waitStatusText() {
return getElement("wait-tx-status").textContent; return getElement("wait-tx-status").textContent;
} }
// Stands in for the popup's: aborting it is the popup closing.
let pageClosed;
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
jest.setSystemTime(new Date("2026-08-11T12:00:00Z")); jest.setSystemTime(new Date("2026-08-11T12:00:00Z"));
elements.clear(); elements.clear();
mockReceiptLookup.mockReset(); mockReceiptLookup.mockReset();
log.errorf.mockClear();
state.wallets = []; state.wallets = [];
state.viewData = {}; state.viewData = {};
state.viewStack = []; state.viewStack = [];
state.currentView = null; state.currentView = null;
txStatus.init({ doRefreshAndRender: jest.fn() }); pageClosed = new AbortController();
txStatus.init({
doRefreshAndRender: jest.fn(),
pageClosed: pageClosed.signal,
});
}); });
afterEach(() => { afterEach(() => {
@@ -466,6 +475,30 @@ describe("WaitTx against an RPC that never answers", () => {
}); });
}); });
// In the popup a lookup its own closing cancelled fails as a lookup against an
// RPC that cannot be reached does, and is not reported
// (https://git.eeqj.de/sneak/AutistMask/issues/475).
describe("WaitTx when the popup closes", () => {
test("a lookup that fails while the popup is open is reported", async () => {
mockReceiptLookup.mockRejectedValue(new TypeError("Failed to fetch"));
txStatus.showWait(TX_INFO, TX_HASH);
await jest.advanceTimersByTimeAsync(10000);
expect(log.errorf).toHaveBeenCalledWith(
"poll receipt failed:",
"Failed to fetch",
);
});
test("a lookup that fails once the popup has closed is not", async () => {
mockReceiptLookup.mockRejectedValue(new TypeError("Failed to fetch"));
txStatus.showWait(TX_INFO, TX_HASH);
pageClosed.abort();
await jest.advanceTimersByTimeAsync(10000);
expect(mockReceiptLookup).toHaveBeenCalledTimes(1);
expect(log.errorf).not.toHaveBeenCalled();
});
});
describe("wait-tx is a view the popup may reopen onto", () => { describe("wait-tx is a view the popup may reopen onto", () => {
// The resume feature is wired through RESTORABLE_VIEWS: restoreView() // The resume feature is wired through RESTORABLE_VIEWS: restoreView()
// refuses any view not in the set, so dropping "wait-tx" from it kills // refuses any view not in the set, so dropping "wait-tx" from it kills