Out of the box the wallet sent the user's addresses to Blockscout and each recipient's address to Etherscan. Two settings, "Use Blockscout" and "Check recipients on Etherscan", now gate every request that sends the user's data to those services. Both start off and are offered unticked on Welcome and in Settings. Each request reads its setting from storage before it asks. An answer that lands after the setting went off is dropped. While a setting is off, the screens that rely on it say so. ENS lookups ask only the RPC endpoint, so a name resolved off-chain does not resolve. RULES.md's External Communication item is now out of date. Its new wording will be proposed on #211 once this lands. Model: opus-5-5 (implementation, summary); fable-5-1 (prose)
184 lines
5.7 KiB
JavaScript
184 lines
5.7 KiB
JavaScript
// The address and token screens do not report a transaction list, or an ENS
|
|
// name lookup for the addresses in it, that the popup's own closing cancelled,
|
|
// and still report one that failed while the popup was open
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/475).
|
|
//
|
|
// In the popup a cancelled request fails with the same "Failed to fetch" as a
|
|
// server that cannot be reached, so the requests here fail that way, and only
|
|
// the signal the popup aborts on pagehide tells the two cases apart. Driven
|
|
// against a minimal DOM stub in the shape tests/timestampDisplay.test.js uses.
|
|
|
|
// The explorer answers with mockHistory, and fails when it is null.
|
|
let mockHistory = null;
|
|
jest.mock("../src/shared/transactions", () => ({
|
|
...jest.requireActual("../src/shared/transactions"),
|
|
fetchRecentTransactions: async () => {
|
|
if (mockHistory === null) throw new TypeError("Failed to fetch");
|
|
return mockHistory;
|
|
},
|
|
}));
|
|
|
|
// Every ENS name lookup fails.
|
|
jest.mock("../src/shared/balances", () => ({
|
|
...jest.requireActual("../src/shared/balances"),
|
|
getProvider: () => ({
|
|
lookupAddress: async () => {
|
|
throw new TypeError("Failed to fetch");
|
|
},
|
|
}),
|
|
}));
|
|
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
|
|
const { state } = require("../src/shared/state");
|
|
const addressDetail = require("../src/popup/views/addressDetail");
|
|
const addressToken = require("../src/popup/views/addressToken");
|
|
|
|
const ADDRESS = "0x1111111111111111111111111111111111111111";
|
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
|
|
|
// A transaction ADDRESS sent, as the history lists hold it.
|
|
function historyTx() {
|
|
return {
|
|
hash: "0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84",
|
|
from: ADDRESS,
|
|
to: RECIPIENT,
|
|
value: "0.0000",
|
|
exactValue: "0.0",
|
|
rawAmount: "0",
|
|
rawUnit: "wei",
|
|
symbol: "ETH",
|
|
timestamp: 1790000000,
|
|
isError: false,
|
|
directionLabel: "Sent",
|
|
direction: "sent",
|
|
contractAddress: null,
|
|
};
|
|
}
|
|
|
|
function makeElement(id) {
|
|
const el = {
|
|
id,
|
|
textContent: "",
|
|
value: "",
|
|
innerHTML: "",
|
|
style: {},
|
|
dataset: {},
|
|
classList: {
|
|
add: () => {},
|
|
remove: () => {},
|
|
contains: () => false,
|
|
toggle: () => false,
|
|
},
|
|
addEventListener: () => {},
|
|
querySelectorAll: () => [],
|
|
appendChild: () => {},
|
|
};
|
|
// Views reach for .parentElement to hide whole sections.
|
|
Object.defineProperty(el, "parentElement", {
|
|
get: () => node(id + "-parent"),
|
|
});
|
|
return el;
|
|
}
|
|
|
|
function makeDocument() {
|
|
const els = new Map();
|
|
return {
|
|
getElementById(id) {
|
|
// The debug banner is created on demand by helpers.js; absent
|
|
// is the state a non-debug, non-testnet popup is in.
|
|
if (id === "debug-banner") return null;
|
|
if (!els.has(id)) els.set(id, makeElement(id));
|
|
return els.get(id);
|
|
},
|
|
createElement: () => makeElement("created"),
|
|
addEventListener: () => {},
|
|
body: { prepend: () => {} },
|
|
};
|
|
}
|
|
|
|
function node(id) {
|
|
return globalThis.document.getElementById(id);
|
|
}
|
|
|
|
let logged;
|
|
|
|
beforeEach(() => {
|
|
mockHistory = null;
|
|
logged = [];
|
|
jest.spyOn(console, "error").mockImplementation((...args) => {
|
|
logged.push(args.map(String).join(" "));
|
|
});
|
|
globalThis.document = makeDocument();
|
|
globalThis.window = { location: { search: "" } };
|
|
state.wallets = [
|
|
{
|
|
name: "Main",
|
|
type: "key",
|
|
addresses: [{ address: ADDRESS, balance: "0.0000" }],
|
|
},
|
|
];
|
|
state.trackedTokens = [];
|
|
state.viewStack = [];
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
state.selectedToken = "ETH";
|
|
// The history is Blockscout's.
|
|
state.useBlockscout = true;
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|