Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8f2461354 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
if (!signal?.aborted) {
|
||||||
log.errorf("symbol() failed:", e.shortMessage || e.message);
|
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) {
|
||||||
|
if (!signal?.aborted) {
|
||||||
log.errorf("decimals() failed:", e.shortMessage || e.message);
|
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) {
|
||||||
|
if (!signal?.aborted) {
|
||||||
log.warnf(
|
log.warnf(
|
||||||
"name() failed, using symbol as name:",
|
"name() failed, using symbol as name:",
|
||||||
e.shortMessage || e.message,
|
e.shortMessage || e.message,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
name = symbol;
|
name = symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -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) {
|
||||||
|
if (!signal?.aborted) {
|
||||||
log.errorf(
|
log.errorf(
|
||||||
"ENS reverse lookup failed",
|
"ENS reverse lookup failed",
|
||||||
address,
|
address,
|
||||||
e.shortMessage || e.message,
|
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;
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +1,31 @@
|
|||||||
// 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 () => {
|
||||||
|
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");
|
throw new TypeError("Failed to fetch");
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
globalThis.chrome = {
|
globalThis.chrome = {
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user