Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
978e8e49d8 |
@@ -54,22 +54,6 @@ but the review is broader than any of them.
|
|||||||
`leavePrivkeyScreen()` in `tests/e2e/run.js` expects the address screen after
|
`leavePrivkeyScreen()` in `tests/e2e/run.js` expects the address screen after
|
||||||
Settings.
|
Settings.
|
||||||
|
|
||||||
- 2026-10-06: Reloading or closing the popup no longer logs a request it cancels
|
|
||||||
as a failure in 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, or the token lookup on the two add-token screens
|
|
||||||
([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)). Each checks the
|
|
||||||
signal the popup aborts on `pagehide`
|
|
||||||
([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)) before reporting a
|
|
||||||
failure; a real failure is still logged. `scanForAddresses()`,
|
|
||||||
`resolveEnsNames()` and `lookupTokenInfo()` take the signal. The e2e suite
|
|
||||||
reloads the popup on the address screen with Blockscout held, and during the
|
|
||||||
address scan with that scan held; jest tests cover each of these with the
|
|
||||||
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: The private key export screen opens again in the same popup
|
- 2026-10-06: The private key export screen opens again in the same popup
|
||||||
session ([#460](https://git.eeqj.de/sneak/AutistMask/issues/460)). `show()`
|
session ([#460](https://git.eeqj.de/sneak/AutistMask/issues/460)). `show()`
|
||||||
found the address line through the element inside it, which its own rendering
|
found the address line through the element inside it, which its own rendering
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ 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({
|
||||||
@@ -69,9 +68,6 @@ 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
|
||||||
|
|||||||
@@ -190,12 +190,7 @@ async function importMnemonic(ctx) {
|
|||||||
|
|
||||||
// Scan for used HD addresses beyond index 0.
|
// Scan for used HD addresses beyond index 0.
|
||||||
showFlash("Scanning for addresses...", 30000);
|
showFlash("Scanning for addresses...", 30000);
|
||||||
const scan = await scanForAddresses(
|
const scan = await scanForAddresses(xpub, state.rpcUrl, state.networkId);
|
||||||
xpub,
|
|
||||||
state.rpcUrl,
|
|
||||||
state.networkId,
|
|
||||||
ctx.pageClosed,
|
|
||||||
);
|
|
||||||
if (scan.addresses.length > 1) {
|
if (scan.addresses.length > 1) {
|
||||||
wallet.addresses = scan.addresses.map((a) => ({
|
wallet.addresses = scan.addresses.map((a) => ({
|
||||||
address: a.address,
|
address: a.address,
|
||||||
@@ -305,12 +300,7 @@ async function importXprvKey(ctx) {
|
|||||||
|
|
||||||
// Scan for used HD addresses beyond index 0.
|
// Scan for used HD addresses beyond index 0.
|
||||||
showFlash("Scanning for addresses...", 30000);
|
showFlash("Scanning for addresses...", 30000);
|
||||||
const scan = await scanForAddresses(
|
const scan = await scanForAddresses(xpub, state.rpcUrl, state.networkId);
|
||||||
xpub,
|
|
||||||
state.rpcUrl,
|
|
||||||
state.networkId,
|
|
||||||
ctx.pageClosed,
|
|
||||||
);
|
|
||||||
if (scan.addresses.length > 1) {
|
if (scan.addresses.length > 1) {
|
||||||
wallet.addresses = scan.addresses.map((a) => ({
|
wallet.addresses = scan.addresses.map((a) => ({
|
||||||
address: a.address,
|
address: a.address,
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ async function loadTransactions(address) {
|
|||||||
counterparties,
|
counterparties,
|
||||||
state.rpcUrl,
|
state.rpcUrl,
|
||||||
state.networkId,
|
state.networkId,
|
||||||
ctx.pageClosed,
|
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
ensNameMap = new Map();
|
ensNameMap = new Map();
|
||||||
@@ -144,9 +143,6 @@ async function loadTransactions(address) {
|
|||||||
|
|
||||||
renderTransactions(txs);
|
renderTransactions(txs);
|
||||||
} 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("loadTransactions failed:", e.message);
|
log.errorf("loadTransactions failed:", e.message);
|
||||||
$("tx-list").innerHTML =
|
$("tx-list").innerHTML =
|
||||||
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ 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();
|
||||||
@@ -228,9 +227,6 @@ async function loadTransactions(address, tokenId) {
|
|||||||
|
|
||||||
renderTransactions(txs);
|
renderTransactions(txs);
|
||||||
} 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("loadTransactions failed:", e.message);
|
log.errorf("loadTransactions failed:", e.message);
|
||||||
$("address-token-tx-list").innerHTML =
|
$("address-token-tx-list").innerHTML =
|
||||||
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
||||||
|
|||||||
@@ -292,9 +292,6 @@ 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,
|
||||||
|
|||||||
@@ -273,9 +273,6 @@ function init(ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
|
||||||
// src/popup/index.js.
|
|
||||||
if (ctx.pageClosed.aborted) return;
|
|
||||||
// Not the error's message: fetch puts the whole URL, password and
|
// Not the error's message: fetch puts the whole URL, password and
|
||||||
// key included, in the message of the error it throws for a URL
|
// key included, in the message of the error it throws for a URL
|
||||||
// with a user name and password or one it cannot parse.
|
// with a user name and password or one it cannot parse.
|
||||||
@@ -302,7 +299,6 @@ function init(ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (ctx.pageClosed.aborted) return;
|
|
||||||
// Not the error's message, as for the RPC check above.
|
// Not the error's message, as for the RPC check above.
|
||||||
log.errorf("Blockscout validation failed:", urlOrigin(url));
|
log.errorf("Blockscout validation failed:", urlOrigin(url));
|
||||||
showFlash("Could not reach endpoint.");
|
showFlash("Could not reach endpoint.");
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ 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({
|
||||||
@@ -153,9 +152,6 @@ 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,9 +132,6 @@ 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
|
||||||
|
|||||||
+13
-23
@@ -294,8 +294,7 @@ 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.
|
||||||
// `signal` is as for fetchTokenBalances().
|
async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
|
||||||
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);
|
||||||
@@ -305,9 +304,7 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
|||||||
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).");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,9 +312,7 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
|||||||
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).");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,12 +320,10 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,8 +339,7 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
|
|||||||
// Checks gapLimit addresses in parallel per batch. Stops when an entire
|
// Checks gapLimit addresses in parallel per batch. Stops when an entire
|
||||||
// batch has no used addresses (i.e. gapLimit consecutive empty addresses).
|
// batch has no used addresses (i.e. gapLimit consecutive empty addresses).
|
||||||
// Returns { addresses: [{ address, index }], nextIndex }.
|
// Returns { addresses: [{ address, index }], nextIndex }.
|
||||||
// `signal` is as for fetchTokenBalances().
|
async function scanForAddresses(xpub, rpcUrl, networkId, gapLimit = 5) {
|
||||||
async function scanForAddresses(xpub, rpcUrl, networkId, signal, gapLimit = 5) {
|
|
||||||
log.debugf("scanForAddresses start, gapLimit:", gapLimit);
|
log.debugf("scanForAddresses start, gapLimit:", gapLimit);
|
||||||
const provider = getProvider(rpcUrl, networkId);
|
const provider = getProvider(rpcUrl, networkId);
|
||||||
const used = [];
|
const used = [];
|
||||||
@@ -370,13 +362,11 @@ async function scanForAddresses(xpub, rpcUrl, networkId, signal, gapLimit = 5) {
|
|||||||
]);
|
]);
|
||||||
return { addr, index, isUsed: balance > 0n || txCount > 0 };
|
return { addr, index, isUsed: balance > 0n || txCount > 0 };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!signal?.aborted) {
|
log.errorf(
|
||||||
log.errorf(
|
"scanForAddresses check failed",
|
||||||
"scanForAddresses check failed",
|
addr,
|
||||||
addr,
|
e.shortMessage || e.message,
|
||||||
e.shortMessage || e.message,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
return { addr, index, isUsed: false };
|
return { addr, index, isUsed: false };
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
+8
-14
@@ -32,8 +32,7 @@ function setCache(address, name) {
|
|||||||
localStorage.setItem(key, JSON.stringify({ name, ts: Date.now() }));
|
localStorage.setItem(key, JSON.stringify({ name, ts: Date.now() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// `signal` is as for fetchTokenBalances() in src/shared/balances.js.
|
async function resolveEnsName(address, rpcUrl, networkId) {
|
||||||
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;
|
||||||
|
|
||||||
@@ -43,26 +42,21 @@ async function resolveEnsName(address, rpcUrl, networkId, signal) {
|
|||||||
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, signal) {
|
async function resolveEnsNames(addresses, rpcUrl, networkId) {
|
||||||
const results = new Map();
|
const results = new Map();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
addresses.map(async (addr) => {
|
addresses.map(async (addr) => {
|
||||||
results.set(
|
results.set(addr, await resolveEnsName(addr, rpcUrl, networkId));
|
||||||
addr,
|
|
||||||
await resolveEnsName(addr, rpcUrl, networkId, signal),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
// Creating a wallet from a recovery phrase or an extended private key does not
|
|
||||||
// report an address scan request the popup's own closing cancelled, and still
|
|
||||||
// reports 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.
|
|
||||||
// Driven against the fake elements tests/tokenLookupCancelled.test.js uses.
|
|
||||||
|
|
||||||
const { FetchRequest, HDNodeWallet, Mnemonic } = require("ethers");
|
|
||||||
|
|
||||||
const PHRASE =
|
|
||||||
"abandon abandon abandon abandon abandon abandon " +
|
|
||||||
"abandon abandon abandon abandon abandon about";
|
|
||||||
const PASSWORD = "correct horse battery staple";
|
|
||||||
|
|
||||||
let elements;
|
|
||||||
|
|
||||||
function fakeElement() {
|
|
||||||
return {
|
|
||||||
value: "",
|
|
||||||
textContent: "",
|
|
||||||
style: {},
|
|
||||||
classList: { toggle: () => {} },
|
|
||||||
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: () => {},
|
|
||||||
goBack: () => {},
|
|
||||||
clearViewStack: () => {},
|
|
||||||
onViewLeave: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// state.js reads chrome.storage.local at load, and saveState() writes it.
|
|
||||||
globalThis.chrome = {
|
|
||||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
||||||
};
|
|
||||||
|
|
||||||
const { state } = require("../src/shared/state");
|
|
||||||
const addWallet = require("../src/popup/views/addWallet");
|
|
||||||
|
|
||||||
let logged;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
elements = {};
|
|
||||||
logged = [];
|
|
||||||
state.wallets = [];
|
|
||||||
state.rpcUrl = "https://rpc.example.invalid";
|
|
||||||
state.networkId = "mainnet";
|
|
||||||
for (const method of ["warn", "error"]) {
|
|
||||||
jest.spyOn(console, method).mockImplementation((...args) => {
|
|
||||||
logged.push(args.map(String).join(" "));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// What the scan found is logged at info level, which is not under test.
|
|
||||||
jest.spyOn(console, "log").mockImplementation(() => {});
|
|
||||||
FetchRequest.registerGetUrl(async () => {
|
|
||||||
throw new TypeError("Failed to fetch");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each([
|
|
||||||
["a recovery phrase", "mnemonic", "wallet-mnemonic", PHRASE],
|
|
||||||
[
|
|
||||||
"an extended private key",
|
|
||||||
"xprv",
|
|
||||||
"import-xprv-key",
|
|
||||||
HDNodeWallet.fromSeed(Mnemonic.fromPhrase(PHRASE).computeSeed())
|
|
||||||
.extendedKey,
|
|
||||||
],
|
|
||||||
])("creating a wallet from %s", (_name, mode, field, secret) => {
|
|
||||||
// Enters `secret` on the add-wallet screen, presses its button and waits
|
|
||||||
// for the address scan that follows.
|
|
||||||
async function create(pageClosed) {
|
|
||||||
addWallet.init({
|
|
||||||
renderWalletList: () => {},
|
|
||||||
doRefreshAndRender: () => {},
|
|
||||||
pageClosed,
|
|
||||||
});
|
|
||||||
element("tab-" + mode).listeners.click();
|
|
||||||
element(field).value = secret;
|
|
||||||
element("add-wallet-password").value = PASSWORD;
|
|
||||||
element("add-wallet-password-confirm").value = PASSWORD;
|
|
||||||
await element("btn-add-wallet-confirm").listeners.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
test("a scan failure while the popup is open is reported", async () => {
|
|
||||||
await create(new AbortController().signal);
|
|
||||||
expect(state.wallets).toHaveLength(1);
|
|
||||||
expect(logged).not.toEqual([]);
|
|
||||||
for (const line of logged) {
|
|
||||||
expect(line).toContain("scanForAddresses check failed");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a scan failure once the popup has closed is not", async () => {
|
|
||||||
const pageClosed = new AbortController();
|
|
||||||
pageClosed.abort();
|
|
||||||
await create(pageClosed.signal);
|
|
||||||
expect(state.wallets).toHaveLength(1);
|
|
||||||
expect(logged).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
// The balance refresh, and the address scan after a wallet is created, do not
|
// The balance refresh does not report a request the popup's own closing
|
||||||
// report a request the popup's own closing cancelled, and still report one
|
// cancelled, and still reports one that failed while the popup was open
|
||||||
// that failed while the popup was open
|
// (https://git.eeqj.de/sneak/AutistMask/issues/218).
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/218,
|
|
||||||
// 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 fetch() fails with the same "Failed to fetch" as a
|
||||||
// server that cannot be reached, so every request here fails that way, and
|
// server that cannot be reached, so every request here fails that way, and
|
||||||
// only the signal the popup aborts on pagehide tells the two cases apart.
|
// only the signal the popup aborts on pagehide tells the two cases apart.
|
||||||
|
|
||||||
const { FetchRequest } = require("ethers");
|
const { FetchRequest } = require("ethers");
|
||||||
const { refreshBalances, scanForAddresses } = require("../src/shared/balances");
|
const { refreshBalances } = require("../src/shared/balances");
|
||||||
const {
|
|
||||||
generateMnemonic,
|
|
||||||
hdWalletFromMnemonic,
|
|
||||||
} = require("../src/shared/wallet");
|
|
||||||
|
|
||||||
const RPC_URL = "https://rpc.example.invalid";
|
const RPC_URL = "https://rpc.example.invalid";
|
||||||
const EXPLORER_URL = "https://explorer.example.invalid/api/v2";
|
const EXPLORER_URL = "https://explorer.example.invalid/api/v2";
|
||||||
@@ -30,9 +24,8 @@ beforeEach(() => {
|
|||||||
const failedToFetch = async () => {
|
const failedToFetch = async () => {
|
||||||
throw new TypeError("Failed to fetch");
|
throw new TypeError("Failed to fetch");
|
||||||
};
|
};
|
||||||
// The RPC calls (ETH balance, ENS name, and the scan's balance and
|
// The RPC calls (ETH balance, ENS name) and the explorer request (token
|
||||||
// transaction count) and the explorer request (token balances) all fail
|
// balances) all fail the same way.
|
||||||
// the same way.
|
|
||||||
FetchRequest.registerGetUrl(failedToFetch);
|
FetchRequest.registerGetUrl(failedToFetch);
|
||||||
globalThis.fetch = jest.fn(failedToFetch);
|
globalThis.fetch = jest.fn(failedToFetch);
|
||||||
});
|
});
|
||||||
@@ -72,24 +65,3 @@ test("a failure once the popup has closed is not", async () => {
|
|||||||
await refresh(pageClosed.signal);
|
await refresh(pageClosed.signal);
|
||||||
expect(logged).toEqual([]);
|
expect(logged).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
function scan(signal) {
|
|
||||||
// What the scan found is logged at info level, which is not under test.
|
|
||||||
jest.spyOn(console, "log").mockImplementation(() => {});
|
|
||||||
const { xpub } = hdWalletFromMnemonic(generateMnemonic());
|
|
||||||
return scanForAddresses(xpub, RPC_URL, "mainnet", signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("a scan failure while the popup is open is reported", async () => {
|
|
||||||
await scan(new AbortController().signal);
|
|
||||||
expect(
|
|
||||||
logged.some((line) => line.includes("scanForAddresses check failed")),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a scan failure once the popup has closed is not", async () => {
|
|
||||||
const pageClosed = new AbortController();
|
|
||||||
pageClosed.abort();
|
|
||||||
await scan(pageClosed.signal);
|
|
||||||
expect(logged).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|||||||
+6
-11
@@ -420,12 +420,12 @@ const HOLD_MAX_MS = 30000;
|
|||||||
|
|
||||||
// Hold a reply open for as long as the test asks: until opts[name] is false.
|
// Hold a reply open for as long as the test asks: until opts[name] is false.
|
||||||
//
|
//
|
||||||
// The switch (holdGasEstimate, holdTransactionCount or holdBlockscout) is read
|
// The switch (holdGasEstimate or holdBlockscout) is read here rather than
|
||||||
// here rather than captured, so a test flips it on the same options object the
|
// captured, so a test flips it on the same options object the route was
|
||||||
// route was registered with — the same pattern as seedTokenTransfer. This is
|
// registered with — the same pattern as seedTokenTransfer. This is the only way
|
||||||
// the only way to observe a screen while its request is genuinely in flight;
|
// to observe a screen while its request is genuinely in flight; sampling the
|
||||||
// sampling the screen and hoping to win a race against the network would
|
// screen and hoping to win a race against the network would assert nothing on
|
||||||
// assert nothing on a slow machine.
|
// a slow machine.
|
||||||
//
|
//
|
||||||
// It never gives up quietly. A hold that outlives the bound is reported like
|
// It never gives up quietly. A hold that outlives the bound is reported like
|
||||||
// any other harness fault, because a "pending" state that stopped being
|
// any other harness fault, because a "pending" state that stopped being
|
||||||
@@ -560,9 +560,6 @@ async function handleRpc(route, postData, opts, report) {
|
|||||||
if (batch.some((req) => req.method === "eth_estimateGas")) {
|
if (batch.some((req) => req.method === "eth_estimateGas")) {
|
||||||
await awaitRelease(opts, "holdGasEstimate", report);
|
await awaitRelease(opts, "holdGasEstimate", report);
|
||||||
}
|
}
|
||||||
if (batch.some((req) => req.method === "eth_getTransactionCount")) {
|
|
||||||
await awaitRelease(opts, "holdTransactionCount", report);
|
|
||||||
}
|
|
||||||
|
|
||||||
const replies = batch.map((req) => rpcReply(req, opts, report));
|
const replies = batch.map((req) => rpcReply(req, opts, report));
|
||||||
return jsonResponse(route, Array.isArray(payload) ? replies : replies[0]);
|
return jsonResponse(route, Array.isArray(payload) ? replies : replies[0]);
|
||||||
@@ -617,8 +614,6 @@ function traceEnabled(raw) {
|
|||||||
* node-side refusal.
|
* node-side refusal.
|
||||||
* @param {boolean} [opts.holdGasEstimate] hold every batch containing an
|
* @param {boolean} [opts.holdGasEstimate] hold every batch containing an
|
||||||
* eth_estimateGas until this is cleared again.
|
* eth_estimateGas until this is cleared again.
|
||||||
* @param {boolean} [opts.holdTransactionCount] hold every batch containing an
|
|
||||||
* eth_getTransactionCount until this is cleared again.
|
|
||||||
* @param {boolean} [opts.holdBlockscout] hold every Blockscout request until
|
* @param {boolean} [opts.holdBlockscout] hold every Blockscout request until
|
||||||
* this is cleared again.
|
* this is cleared again.
|
||||||
* @param {boolean} [opts.failTransactionList] fail every request for an
|
* @param {boolean} [opts.failTransactionList] fail every request for an
|
||||||
|
|||||||
+8
-47
@@ -249,23 +249,8 @@ test("the exact confirmation phrase erases the record and reloads into Welcome (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// The address scan that follows creating the wallet is held, so the popup
|
test("wallet creation through the UI reaches the main view", async (env) => {
|
||||||
// reloads while it is in flight. As for the reloads mid-refresh further down,
|
env.phrase = await createWallet(env.page);
|
||||||
// the assertion for that is the harness's own: a console.error from the page
|
|
||||||
// being reloaded fails this test (#475).
|
|
||||||
test("wallet creation through the UI reaches the main view, and reloading mid-scan reports no failure (#475)", async (env) => {
|
|
||||||
env.routeOpts.holdTransactionCount = true;
|
|
||||||
try {
|
|
||||||
const scanning = env.page.waitForRequest((r) =>
|
|
||||||
(r.postData() || "").includes("eth_getTransactionCount"),
|
|
||||||
);
|
|
||||||
env.phrase = await createWallet(env.page);
|
|
||||||
await scanning;
|
|
||||||
await env.page.reload();
|
|
||||||
} finally {
|
|
||||||
env.routeOpts.holdTransactionCount = false;
|
|
||||||
}
|
|
||||||
await visible(env.page, "#view-main");
|
|
||||||
assert(
|
assert(
|
||||||
env.phrase.split(/\s+/).length >= 12,
|
env.phrase.split(/\s+/).length >= 12,
|
||||||
"wallet creation did not yield a recovery phrase",
|
"wallet creation did not yield a recovery phrase",
|
||||||
@@ -715,8 +700,9 @@ test("the token contract row links to the explorer's token page (#151)", async (
|
|||||||
//
|
//
|
||||||
// Reloading or closing the popup makes Chrome cancel the requests it still has
|
// Reloading or closing the popup makes Chrome cancel the requests it still has
|
||||||
// open, and in the page a cancelled fetch() fails with the same "Failed to
|
// open, and in the page a cancelled fetch() fails with the same "Failed to
|
||||||
// fetch" as a server that cannot be reached. The reload during the address
|
// fetch" as a server that cannot be reached. Here rather than straight after
|
||||||
// scan that follows creating a wallet is in the wallet creation test (#475).
|
// wallet creation because a reload also cancels the address scan that follows
|
||||||
|
// it, which still logs (#475).
|
||||||
|
|
||||||
// Blockscout is held, so the home screen's transaction list and token balances
|
// Blockscout is held, so the home screen's transaction list and token balances
|
||||||
// cannot have been answered when the popup reloads. The assertion is the
|
// cannot have been answered when the popup reloads. The assertion is the
|
||||||
@@ -766,30 +752,6 @@ test("a transaction list that cannot be fetched is still reported (#218)", async
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// The address screen's transaction list, held the same way. The popup reopens
|
|
||||||
// on the address screen, which asks for its list as it is drawn, so once the
|
|
||||||
// screen is up its request is in flight (#475).
|
|
||||||
test("reloading the popup on the address screen reports no failure (#475)", async (env) => {
|
|
||||||
await openAddressDetail(env.page);
|
|
||||||
await waitForPersisted(
|
|
||||||
env.page,
|
|
||||||
"currentView",
|
|
||||||
"address",
|
|
||||||
"before reloading the popup",
|
|
||||||
);
|
|
||||||
env.routeOpts.holdBlockscout = true;
|
|
||||||
try {
|
|
||||||
await env.page.reload();
|
|
||||||
await visible(env.page, "#view-address");
|
|
||||||
await env.page.reload();
|
|
||||||
} finally {
|
|
||||||
env.routeOpts.holdBlockscout = false;
|
|
||||||
}
|
|
||||||
await visible(env.page, "#view-address");
|
|
||||||
// Home again, where the tests below expect to start.
|
|
||||||
await goHome(env.page);
|
|
||||||
});
|
|
||||||
|
|
||||||
// -------------------------------------------- recovery phrase (#161)
|
// -------------------------------------------- recovery phrase (#161)
|
||||||
|
|
||||||
// The gear toggles, so pressing it while Settings is already up leaves it.
|
// The gear toggles, so pressing it while Settings is already up leaves it.
|
||||||
@@ -2199,7 +2161,9 @@ function quantity(wei) {
|
|||||||
|
|
||||||
// Wait on the main view until a changed balance fixture has been picked up.
|
// Wait on the main view until a changed balance fixture has been picked up.
|
||||||
//
|
//
|
||||||
// Not a reload: the popup re-refreshes on a 10-second timer by itself.
|
// Not a reload: the popup re-refreshes on a 10-second timer by itself, and a
|
||||||
|
// reload on the address screen still logs the transaction list it cancels
|
||||||
|
// (#475).
|
||||||
//
|
//
|
||||||
// It also deliberately settles on MAIN rather than on the address screen.
|
// It also deliberately settles on MAIN rather than on the address screen.
|
||||||
// The address screen builds the send screen's token dropdown once, from the
|
// The address screen builds the send screen's token dropdown once, from the
|
||||||
@@ -4559,9 +4523,6 @@ async function main() {
|
|||||||
ethBalanceWei: null,
|
ethBalanceWei: null,
|
||||||
failGasEstimate: false,
|
failGasEstimate: false,
|
||||||
holdGasEstimate: false,
|
holdGasEstimate: false,
|
||||||
// Whether a JSON-RPC batch asking for a transaction count is held
|
|
||||||
// unanswered: the address scan after creating a wallet (#475).
|
|
||||||
holdTransactionCount: false,
|
|
||||||
// Whether Blockscout requests are held unanswered, and whether an
|
// Whether Blockscout requests are held unanswered, and whether an
|
||||||
// address's transaction list fails as a network error (#218).
|
// address's transaction list fails as a network error (#218).
|
||||||
holdBlockscout: false,
|
holdBlockscout: false,
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ 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();
|
||||||
|
|||||||
+2
-51
@@ -65,7 +65,7 @@ jest.mock("../src/shared/log", () => ({
|
|||||||
debugf: () => {},
|
debugf: () => {},
|
||||||
infof: () => {},
|
infof: () => {},
|
||||||
warnf: () => {},
|
warnf: () => {},
|
||||||
errorf: jest.fn(),
|
errorf: () => {},
|
||||||
},
|
},
|
||||||
// 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,7 +143,6 @@ 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");
|
||||||
|
|
||||||
@@ -227,16 +226,10 @@ 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({
|
send.init({ showConfirmTx: (info) => (confirmed = info) });
|
||||||
showConfirmTx: (info) => (confirmed = info),
|
|
||||||
pageClosed: pageClosed.signal,
|
|
||||||
});
|
|
||||||
confirmTx.init({});
|
confirmTx.init({});
|
||||||
send.resetSendValidation();
|
send.resetSendValidation();
|
||||||
state.currentView = "send";
|
state.currentView = "send";
|
||||||
@@ -267,8 +260,6 @@ 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 = [];
|
||||||
@@ -456,46 +447,6 @@ 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();
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ function element(id) {
|
|||||||
return (elements[id] ||= fakeElement());
|
return (elements[id] ||= fakeElement());
|
||||||
}
|
}
|
||||||
|
|
||||||
// `pageClosed` stands in for the popup's: aborting it is the popup closing.
|
function loadSettingsView() {
|
||||||
function loadSettingsView(pageClosed = new AbortController()) {
|
|
||||||
elements = {};
|
elements = {};
|
||||||
flashes = [];
|
flashes = [];
|
||||||
|
|
||||||
@@ -75,9 +74,7 @@ function loadSettingsView(pageClosed = new AbortController()) {
|
|||||||
state.blockscoutUrl = SAVED_BLOCKSCOUT;
|
state.blockscoutUrl = SAVED_BLOCKSCOUT;
|
||||||
require("../src/shared/log").setRuntimeDebug(true);
|
require("../src/shared/log").setRuntimeDebug(true);
|
||||||
|
|
||||||
require("../src/popup/views/settings").init({
|
require("../src/popup/views/settings").init({});
|
||||||
pageClosed: pageClosed.signal,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(fieldId, buttonId, typed) {
|
async function save(fieldId, buttonId, typed) {
|
||||||
@@ -149,21 +146,3 @@ test("the Blockscout check of a URL with a user name and password", async () =>
|
|||||||
const line = expectFailedWithoutSecrets("Blockscout validation failed");
|
const line = expectFailedWithoutSecrets("Blockscout validation failed");
|
||||||
expect(line).toContain("https://explorer.example.invalid");
|
expect(line).toContain("https://explorer.example.invalid");
|
||||||
});
|
});
|
||||||
|
|
||||||
// In the popup a check its own closing cancelled fails as these do, and is
|
|
||||||
// not reported (https://git.eeqj.de/sneak/AutistMask/issues/475). Nothing is
|
|
||||||
// saved either.
|
|
||||||
test("a check that fails once the popup has closed is not reported", async () => {
|
|
||||||
const pageClosed = new AbortController();
|
|
||||||
pageClosed.abort();
|
|
||||||
loadSettingsView(pageClosed);
|
|
||||||
await save("settings-rpc", "btn-save-rpc", RPC_UNPARSEABLE);
|
|
||||||
await save(
|
|
||||||
"settings-blockscout",
|
|
||||||
"btn-save-blockscout",
|
|
||||||
BLOCKSCOUT_WITH_PASSWORD,
|
|
||||||
);
|
|
||||||
expect(console.error).not.toHaveBeenCalled();
|
|
||||||
expect(state.rpcUrl).toBe(SAVED_RPC);
|
|
||||||
expect(state.blockscoutUrl).toBe(SAVED_BLOCKSCOUT);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// exclusion of the screen from the set of views the popup may reopen onto,
|
// exclusion of the screen from the set of views the popup may reopen onto,
|
||||||
// the absence of any path from this module to the logger, and, against a
|
// the absence of any path from this module to the logger, and, against a
|
||||||
// minimal DOM stub, where Back goes after the screen is left by the settings
|
// minimal DOM stub, where Back goes after the screen is left by the settings
|
||||||
// gear or by its own Back. The rest of the DOM behaviour it guards — nothing rendered before the
|
// gear. The rest of the DOM behaviour it guards — nothing rendered before the
|
||||||
// password is accepted, a wrong password revealing nothing, and the wipe on
|
// password is accepted, a wrong password revealing nothing, and the wipe on
|
||||||
// leaving — is driven against the real popup in a real browser by
|
// leaving — is driven against the real popup in a real browser by
|
||||||
// tests/e2e/run.js, which is where every other view behaviour is tested.
|
// tests/e2e/run.js, which is where every other view behaviour is tested.
|
||||||
@@ -80,13 +80,12 @@ describe("views the popup may reopen onto", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Just enough document for helpers.showView() and this view: every element
|
// Just enough document for helpers.showView() and this view: every element
|
||||||
// is made on first lookup and keeps what the view writes to it, its click
|
// is made on first lookup and keeps what the view writes to it.
|
||||||
// handler included.
|
|
||||||
function makeDocument() {
|
function makeDocument() {
|
||||||
const els = new Map();
|
const els = new Map();
|
||||||
function makeElement() {
|
function makeElement() {
|
||||||
const classes = new Set();
|
const classes = new Set();
|
||||||
const el = {
|
return {
|
||||||
textContent: "",
|
textContent: "",
|
||||||
value: "",
|
value: "",
|
||||||
style: {},
|
style: {},
|
||||||
@@ -97,11 +96,8 @@ function makeDocument() {
|
|||||||
toggle: (name, on) =>
|
toggle: (name, on) =>
|
||||||
on ? classes.add(name) : classes.delete(name),
|
on ? classes.add(name) : classes.delete(name),
|
||||||
},
|
},
|
||||||
addEventListener: (name, fn) => {
|
addEventListener: () => {},
|
||||||
if (name === "click") el.onClick = fn;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return el;
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
getElementById(id) {
|
getElementById(id) {
|
||||||
@@ -115,8 +111,10 @@ function makeDocument() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("Back from Settings after leaving by the settings gear", () => {
|
describe("Back from Settings after leaving by the settings gear", () => {
|
||||||
// On Settings, opened from Home.
|
// https://git.eeqj.de/sneak/AutistMask/issues/461: leaving drops the
|
||||||
function load() {
|
// wallet selection, so Back onto this screen showed a password prompt
|
||||||
|
// that could only answer "No wallet is selected."
|
||||||
|
test("does not land on the recovery phrase screen", () => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
globalThis.document = makeDocument();
|
globalThis.document = makeDocument();
|
||||||
const helpers = loadHelpers();
|
const helpers = loadHelpers();
|
||||||
@@ -126,14 +124,6 @@ describe("Back from Settings after leaving by the settings gear", () => {
|
|||||||
state.wallets = [{ name: "Wallet 1", type: "hd", addresses: [] }];
|
state.wallets = [{ name: "Wallet 1", type: "hd", addresses: [] }];
|
||||||
state.currentView = "settings";
|
state.currentView = "settings";
|
||||||
state.viewStack = ["main"];
|
state.viewStack = ["main"];
|
||||||
return { helpers, state, showPhrase };
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/461: leaving drops the
|
|
||||||
// wallet selection, so Back onto this screen showed a password prompt
|
|
||||||
// that could only answer "No wallet is selected."
|
|
||||||
test("does not land on the recovery phrase screen", () => {
|
|
||||||
const { helpers, state, showPhrase } = load();
|
|
||||||
|
|
||||||
// Opened from the wallet list in Settings, then left by the gear:
|
// Opened from the wallet list in Settings, then left by the gear:
|
||||||
// push the current view, then show Settings.
|
// push the current view, then show Settings.
|
||||||
@@ -145,18 +135,6 @@ describe("Back from Settings after leaving by the settings gear", () => {
|
|||||||
helpers.goBack();
|
helpers.goBack();
|
||||||
expect(state.currentView).not.toBe(SHOW_PHRASE_VIEW);
|
expect(state.currentView).not.toBe(SHOW_PHRASE_VIEW);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This Back takes Settings off the stack before the screen is left, so
|
|
||||||
// the stack's top is then the entry Back from Settings will need.
|
|
||||||
test("its own Back button leaves the rest of the stack alone", () => {
|
|
||||||
const { state, showPhrase } = load();
|
|
||||||
showPhrase.show(0);
|
|
||||||
|
|
||||||
globalThis.document.getElementById("btn-show-phrase-back").onClick();
|
|
||||||
|
|
||||||
expect(state.currentView).toBe("settings");
|
|
||||||
expect(state.viewStack).toEqual(["main"]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("the phrase cannot reach the logger", () => {
|
describe("the phrase cannot reach the logger", () => {
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
// 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.
|
|
||||||
// The last tests call lookupTokenInfo() directly with symbol() answered, so
|
|
||||||
// that its later calls are the ones that fail.
|
|
||||||
|
|
||||||
const {
|
|
||||||
FetchRequest,
|
|
||||||
Interface,
|
|
||||||
toUtf8Bytes,
|
|
||||||
toUtf8String,
|
|
||||||
} = require("ethers");
|
|
||||||
const { ERC20_ABI } = require("../src/shared/constants");
|
|
||||||
|
|
||||||
const ADDRESS = "0x1111111111111111111111111111111111111111";
|
|
||||||
const RPC_URL = "https://rpc.example.invalid";
|
|
||||||
|
|
||||||
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");
|
|
||||||
const { lookupTokenInfo } = require("../src/shared/balances");
|
|
||||||
|
|
||||||
let logged;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
elements = {};
|
|
||||||
logged = [];
|
|
||||||
state.trackedTokens = [];
|
|
||||||
for (const method of ["warn", "error"]) {
|
|
||||||
jest.spyOn(console, method).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([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Answers the contract call for each function named in `answers` with its
|
|
||||||
// value, and fails every other request as above.
|
|
||||||
function answer(answers) {
|
|
||||||
const erc20 = new Interface(ERC20_ABI);
|
|
||||||
FetchRequest.registerGetUrl(async (req) => {
|
|
||||||
const { id, params } = JSON.parse(toUtf8String(req.body));
|
|
||||||
const { name } = erc20.parseTransaction({ data: params[0].data });
|
|
||||||
if (!(name in answers)) {
|
|
||||||
throw new TypeError("Failed to fetch");
|
|
||||||
}
|
|
||||||
const result = erc20.encodeFunctionResult(name, [answers[name]]);
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
statusMessage: "OK",
|
|
||||||
headers: {},
|
|
||||||
body: toUtf8Bytes(JSON.stringify({ jsonrpc: "2.0", id, result })),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe.each([
|
|
||||||
["decimals() fails", { symbol: "TKN" }, "decimals() failed:"],
|
|
||||||
[
|
|
||||||
"name() fails",
|
|
||||||
{ symbol: "TKN", decimals: 18 },
|
|
||||||
"name() failed, using symbol as name:",
|
|
||||||
],
|
|
||||||
])("a token lookup where %s", (_, answers, report) => {
|
|
||||||
async function lookUp(pageClosed) {
|
|
||||||
// A token found is logged at info level, which is not under test.
|
|
||||||
jest.spyOn(console, "log").mockImplementation(() => {});
|
|
||||||
answer(answers);
|
|
||||||
// A failed decimals() also fails the lookup, which the screens report
|
|
||||||
// as tested above.
|
|
||||||
await lookupTokenInfo(ADDRESS, RPC_URL, "mainnet", pageClosed).catch(
|
|
||||||
() => {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("is reported while the popup is open", async () => {
|
|
||||||
await lookUp(new AbortController().signal);
|
|
||||||
expect(logged).toEqual([expect.stringContaining(report)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("is not once the popup has closed", async () => {
|
|
||||||
const pageClosed = new AbortController();
|
|
||||||
pageClosed.abort();
|
|
||||||
await lookUp(pageClosed.signal);
|
|
||||||
expect(logged).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
// 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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+2
-35
@@ -21,7 +21,7 @@ jest.mock("../src/shared/log", () => ({
|
|||||||
debugf: () => {},
|
debugf: () => {},
|
||||||
infof: () => {},
|
infof: () => {},
|
||||||
warnf: () => {},
|
warnf: () => {},
|
||||||
errorf: jest.fn(),
|
errorf: () => {},
|
||||||
},
|
},
|
||||||
debugFetch: jest.fn(),
|
debugFetch: jest.fn(),
|
||||||
setRuntimeDebug: () => {},
|
setRuntimeDebug: () => {},
|
||||||
@@ -108,7 +108,6 @@ 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";
|
||||||
@@ -129,24 +128,16 @@ 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;
|
||||||
pageClosed = new AbortController();
|
txStatus.init({ doRefreshAndRender: jest.fn() });
|
||||||
txStatus.init({
|
|
||||||
doRefreshAndRender: jest.fn(),
|
|
||||||
pageClosed: pageClosed.signal,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -475,30 +466,6 @@ 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