A balance refresh that returns now draws the balances on AddressDetail or AddressToken again when that screen is up, not only the wallet list; their transaction lists are left alone. Every add path on AddToken and SettingsAddToken tracks a token through one step, trackToken(), which also lists it in the new stored field tokensNotFetched: its line reads `quantity unknown` instead of 0.0000 until a refresh that started with it tracked has fetched every address's token balances, even if the popup closes first. To tell an address whose token balances the explorer did not return, the refresh takes them off its copy of the wallets before fetching. Model: opus-5-5
134 lines
4.1 KiB
JavaScript
134 lines
4.1 KiB
JavaScript
// The flash line (#252). #flash-msg reserves one line and cuts a message too
|
|
// long for it with an ellipsis; that is measured in a real browser by
|
|
// tests/e2e/run.js. Here: showFlash() keeps the whole message readable in the
|
|
// line's title, and the two add-token screens flash a fixed line, not the text
|
|
// of whatever error adding the token threw.
|
|
|
|
const ADDRESS = "0x1111111111111111111111111111111111111111";
|
|
|
|
let elements;
|
|
|
|
function fakeElement() {
|
|
return {
|
|
value: "",
|
|
textContent: "",
|
|
title: "",
|
|
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());
|
|
}
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
elements = {};
|
|
globalThis.document = { getElementById: element };
|
|
// state.js reads chrome.storage.local at load.
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.dontMock("../src/popup/views/helpers");
|
|
jest.dontMock("../src/shared/state");
|
|
jest.dontMock("../src/shared/balances");
|
|
jest.restoreAllMocks();
|
|
jest.useRealTimers();
|
|
delete globalThis.document;
|
|
delete globalThis.chrome;
|
|
});
|
|
|
|
test("showFlash() puts the whole message in the title, and clears both", () => {
|
|
jest.useFakeTimers();
|
|
const { showFlash } = require("../src/popup/views/helpers");
|
|
|
|
showFlash("Saved.");
|
|
expect(element("flash-msg").textContent).toBe("Saved.");
|
|
expect(element("flash-msg").title).toBe("Saved.");
|
|
|
|
jest.advanceTimersByTime(2000);
|
|
expect(element("flash-msg").textContent).toBe("");
|
|
expect(element("flash-msg").title).toBe("");
|
|
});
|
|
|
|
describe.each([
|
|
["addToken", "add-token-address", "btn-add-token-confirm"],
|
|
[
|
|
"settingsAddToken",
|
|
"settings-addtoken-address",
|
|
"btn-settings-addtoken-manual",
|
|
],
|
|
])("adding a token on %s", (view, field, button) => {
|
|
let flashes;
|
|
let errors;
|
|
|
|
// Clicks the screen's add button with lookupTokenInfo() and saveState()
|
|
// replaced by the given functions.
|
|
async function add(lookupTokenInfo, saveState) {
|
|
flashes = [];
|
|
errors = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
jest.spyOn(console, "log").mockImplementation(() => {});
|
|
jest.doMock("../src/shared/balances", () => ({ lookupTokenInfo }));
|
|
jest.doMock("../src/shared/state", () => ({
|
|
state: { trackedTokens: [] },
|
|
saveState,
|
|
currentNetwork: () => ({ chainId: 1 }),
|
|
}));
|
|
jest.doMock("../src/popup/views/helpers", () => ({
|
|
$: element,
|
|
showView: () => {},
|
|
showFlash: (msg) => flashes.push(msg),
|
|
escapeHtml: (s) => s,
|
|
goBack: () => {},
|
|
trackToken: () => {},
|
|
}));
|
|
|
|
require("../src/popup/views/" + view).init({
|
|
doRefreshAndRender: () => {},
|
|
pageClosed: new AbortController().signal,
|
|
});
|
|
element(field).value = ADDRESS;
|
|
await element(button).listeners.click();
|
|
}
|
|
|
|
test("a failed save flashes a fixed line and logs the error", async () => {
|
|
const detail = "A sentence about the stored record. ".repeat(4);
|
|
|
|
await add(
|
|
async () => ({ symbol: "TKN", decimals: 18, name: "Token" }),
|
|
async () => {
|
|
throw new Error(detail);
|
|
},
|
|
);
|
|
|
|
expect(flashes).toEqual(["Could not add the token."]);
|
|
expect(errors).toHaveBeenCalledWith(
|
|
"[AutistMask]",
|
|
"Adding token failed for",
|
|
ADDRESS,
|
|
detail,
|
|
);
|
|
});
|
|
|
|
test("a contract that is not a token flashes the lookup message", async () => {
|
|
const detail = "Not a valid ERC-20 token (symbol() failed).";
|
|
|
|
await add(
|
|
async () => {
|
|
throw new Error(detail);
|
|
},
|
|
async () => {},
|
|
);
|
|
|
|
expect(flashes).toEqual([detail]);
|
|
});
|
|
});
|