The balance lists and Home's ETH line rounded with toFixed(4), so 0.99996 ETH read 1.0000 there and 0.9999 on Send. Send's current balance and ConfirmTx's balance line kept the nonzero floor, so 0.00004 USDC read 0.0000 in the lists and 0.00004 on Send. All four now cut the stored decimal string with truncateAmount(), as the history rows do. balanceLine() takes the stored balance string instead of a parsed number, and escapes the quantity cell. The amounts, fees and allowances a screen asks the user to approve keep the floor of truncateAmountNeverZero(). Model: opus-5-5
191 lines
7.9 KiB
JavaScript
191 lines
7.9 KiB
JavaScript
// Tests for the copy on the address-removal confirmation (issue #162).
|
|
//
|
|
// The screen's whole job is to warn before a destructive-looking action, so
|
|
// the copy is the substance and is tested as such. Two things it must not
|
|
// get wrong: what it takes to get the address back — the app refuses both
|
|
// obvious routes — and what counts as holding something, which is any
|
|
// ERC-20 as well as ETH, at any size, including a balance below the four
|
|
// decimals the balance lines render. The DOM behaviour
|
|
// around them is driven against the real popup by tests/e2e/run.js.
|
|
|
|
// helpers.js pulls in state.js, which reads chrome.storage.local at load.
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
|
|
const { addressHoldsFunds } = require("../src/popup/views/helpers");
|
|
const {
|
|
recoveryPathText,
|
|
balanceWarningHtml,
|
|
} = require("../src/popup/views/deleteAddress");
|
|
const { prices, clearPrices } = require("../src/shared/prices");
|
|
const { state } = require("../src/shared/state");
|
|
|
|
// The screen prices holdings, and pricing asks which chain it is on. Reading
|
|
// the singleton's network before anything loaded it now throws rather than
|
|
// answering mainnet by default
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/324), so the network this
|
|
// fixture is on is stated instead of assumed.
|
|
state.networkId = "mainnet";
|
|
|
|
const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
|
|
|
const EMPTY = { address: "0x1", balance: "0.0000", tokenBalances: [] };
|
|
const ETH_ONLY = { address: "0x1", balance: "1.5", tokenBalances: [] };
|
|
const DUST = { address: "0x1", balance: "0.00001", tokenBalances: [] };
|
|
const NOT_READ = { address: "0x1", balance: null, tokenBalances: [] };
|
|
const TOKEN_ONLY = {
|
|
address: "0x1",
|
|
balance: "0.0000",
|
|
tokenBalances: [{ address: USDC, symbol: "USDC", balance: "2500.0" }],
|
|
};
|
|
const ZERO_TOKEN = {
|
|
address: "0x1",
|
|
balance: "0",
|
|
tokenBalances: [{ address: USDC, symbol: "USDC", balance: "0" }],
|
|
};
|
|
|
|
afterEach(() => {
|
|
clearPrices();
|
|
});
|
|
|
|
describe("what the screen says it takes to get the address back", () => {
|
|
// The screen used to promise the address "can be brought back at any
|
|
// time by importing this wallet's recovery phrase again". That import is
|
|
// refused as a duplicate for as long as the wallet is present, which it
|
|
// always is here — a wallet never gives up its last address.
|
|
test("it does not promise a re-import while the wallet is here", () => {
|
|
const text = recoveryPathText({ type: "hd" });
|
|
expect(text).not.toMatch(/at any time/);
|
|
expect(text).toContain("is refused while this wallet is still here");
|
|
});
|
|
|
|
test("it names deleting the whole wallet as the route back", () => {
|
|
expect(recoveryPathText({ type: "hd" })).toContain(
|
|
"delete the whole wallet in Settings",
|
|
);
|
|
});
|
|
|
|
// The scan after a re-import finds used addresses only, so an address
|
|
// that never saw a transaction does not come back at all. Saying so is
|
|
// the difference between a warning and a false reassurance.
|
|
test("it states the limit: only on-chain activity is found", () => {
|
|
const text = recoveryPathText({ type: "hd" });
|
|
expect(text).toContain("only finds addresses that have on-chain");
|
|
expect(text).toContain("never been used is not found by it");
|
|
});
|
|
|
|
// The screen is offered on xprv wallets too, and an xprv wallet holds no
|
|
// recovery phrase — telling its owner to import one would send them
|
|
// looking for words that do not exist.
|
|
test("an xprv wallet is told about its extended private key", () => {
|
|
const text = recoveryPathText({ type: "xprv" });
|
|
expect(text).toContain("extended private key");
|
|
expect(text).not.toContain("recovery phrase");
|
|
});
|
|
|
|
test("an HD wallet is told about its recovery phrase", () => {
|
|
const text = recoveryPathText({ type: "hd" });
|
|
expect(text).toContain("recovery phrase");
|
|
expect(text).not.toContain("extended private key");
|
|
});
|
|
});
|
|
|
|
describe("whether an address holds anything", () => {
|
|
test("ETH counts", () => {
|
|
expect(addressHoldsFunds(ETH_ONLY)).toBe(true);
|
|
});
|
|
|
|
// The case that decides the screen: no ETH at all, and $2500 of a
|
|
// stablecoin sitting at the address.
|
|
test("an ERC-20 balance counts even with no ETH", () => {
|
|
expect(addressHoldsFunds(TOKEN_ONLY)).toBe(true);
|
|
});
|
|
|
|
// 0.00001 ETH renders as "0.0000" at four decimals. It is still money.
|
|
test("an ETH balance below the displayed precision counts", () => {
|
|
expect(addressHoldsFunds(DUST)).toBe(true);
|
|
});
|
|
|
|
// No refresh has read it, so the address may hold funds.
|
|
test("an ETH balance no refresh has read counts", () => {
|
|
expect(addressHoldsFunds(NOT_READ)).toBe(true);
|
|
});
|
|
|
|
test("an address holding nothing does not", () => {
|
|
expect(addressHoldsFunds(EMPTY)).toBe(false);
|
|
expect(addressHoldsFunds(ZERO_TOKEN)).toBe(false);
|
|
});
|
|
|
|
test("a missing address does not", () => {
|
|
expect(addressHoldsFunds(undefined)).toBe(false);
|
|
});
|
|
|
|
// It reads as unknown, as a null balance does.
|
|
test("an address with no ETH balance field counts", () => {
|
|
expect(addressHoldsFunds({ address: "0x1" })).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("the balance warning on the removal confirmation", () => {
|
|
test("an address holding nothing gets a blank line, not a warning", () => {
|
|
expect(balanceWarningHtml(EMPTY)).toBe(" ");
|
|
expect(balanceWarningHtml(ZERO_TOKEN)).toBe(" ");
|
|
});
|
|
|
|
test("an ERC-20-only address is warned about, and its token listed", () => {
|
|
const html = balanceWarningHtml(TOKEN_ONLY);
|
|
expect(html).toContain("This address holds a balance.");
|
|
expect(html).toContain("does not move or spend anything");
|
|
expect(html).toContain("USDC");
|
|
expect(html).toContain("2500.0000");
|
|
});
|
|
|
|
// The rendered line says 0.0000 for this address — that is the display
|
|
// format, shared with Home and AddressDetail — and the warning is shown
|
|
// all the same, because the balance is not zero.
|
|
test("an ETH balance that renders as 0.0000 is warned about", () => {
|
|
const html = balanceWarningHtml(DUST);
|
|
expect(html).toContain("This address holds a balance.");
|
|
expect(html).toContain("<span>0.0000</span>");
|
|
});
|
|
|
|
test("an ETH balance no refresh has read is warned about, and reads as unknown", () => {
|
|
const html = balanceWarningHtml(NOT_READ);
|
|
expect(html).toContain("This address may hold a balance.");
|
|
expect(html).not.toContain("This address holds a balance.");
|
|
expect(html).toContain("quantity unknown");
|
|
expect(html).not.toContain("0.0000");
|
|
});
|
|
|
|
test("an address with no ETH balance field reads the same", () => {
|
|
expect(balanceWarningHtml({ address: "0x1" })).toContain(
|
|
"This address may hold a balance.",
|
|
);
|
|
});
|
|
|
|
// The sentence must not assert an amount, because any amount it could
|
|
// assert has been cut to four decimals: "This address holds 0.0000 ETH." is
|
|
// what the cut form produces for an address that holds real money.
|
|
test("the warning sentence asserts no cut amount", () => {
|
|
for (const addr of [DUST, ETH_ONLY, TOKEN_ONLY]) {
|
|
expect(balanceWarningHtml(addr)).not.toMatch(
|
|
/holds [\d.]+ (ETH|USDC)/,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("the USD total is shown when prices are known", () => {
|
|
prices.ETH = 2000;
|
|
prices[USDC] = 1;
|
|
expect(balanceWarningHtml(TOKEN_ONLY)).toContain("Total: $2,500.00");
|
|
expect(balanceWarningHtml(ETH_ONLY)).toContain("Total: $3,000.00");
|
|
});
|
|
|
|
// getAddressValue() reports no value on testnet and before the first
|
|
// price fetch. A "Total: $0.00" there would be a lie about the holdings.
|
|
test("no USD total is shown when prices are not known", () => {
|
|
expect(balanceWarningHtml(TOKEN_ONLY)).not.toContain("Total:");
|
|
});
|
|
});
|