// Tests for the token filtering in the Send view's token selector // (src/popup/views/send.js). // // The selector decides which of the user's tokens can be spent at all, so // over-filtering here is worse than in the history list: the asset is not // merely hidden, it becomes unspendable through the UI. Issue #230: an // explorer that omits holders_count was read as "zero holders" and the token // disappeared from this list. // // renderSendTokenSelect only ever touches getElementById, createElement, // innerHTML, value, textContent and appendChild, so a small stub document is // enough to drive it; the real DOM behaviour of the view is covered by // tests/e2e/run.js. globalThis.chrome = { storage: { local: { get: async () => ({}), set: async () => {} } }, }; const { state } = require("../src/shared/state"); const { renderSendTokenSelect } = require("../src/popup/views/send"); const USDC_CONTRACT = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const NOVEL_TOKEN = "0x1111111111111111111111111111111111111111"; let select; function installStubDocument() { select = { innerHTML: "", children: [] }; select.appendChild = (child) => select.children.push(child); globalThis.document = { getElementById: (id) => (id === "send-token" ? select : null), createElement: () => ({ value: "", textContent: "" }), }; } // The symbols offered for sending, excluding the hardcoded ETH option that // renderSendTokenSelect writes straight into innerHTML. function offeredTokens() { return select.children.map((opt) => opt.value.toLowerCase()); } function tokenBalance(overrides) { return { address: NOVEL_TOKEN, symbol: "SPAMTKN", decimals: 18, balance: "12.5", holders: 50000, ...overrides, }; } function render(tokenBalances) { installStubDocument(); renderSendTokenSelect({ address: "0x" + "a".repeat(40), tokenBalances }); } beforeEach(() => { state.fraudContracts = []; state.hideLowHolderTokens = true; }); describe("the low-holder rule in the send token selector", () => { test("ETH is always offered", () => { render([]); expect(select.innerHTML).toBe(''); expect(offeredTokens()).toEqual([]); }); test("a token with plenty of holders is offered", () => { render([tokenBalance()]); expect(offeredTokens()).toEqual([NOVEL_TOKEN]); }); test("a token reporting zero holders is withheld", () => { render([tokenBalance({ holders: 0 })]); expect(offeredTokens()).toEqual([]); }); test("boundary: 999 holders is withheld, 1000 is offered", () => { render([tokenBalance({ holders: 999 })]); expect(offeredTokens()).toEqual([]); render([tokenBalance({ holders: 1000 })]); expect(offeredTokens()).toEqual([NOVEL_TOKEN]); }); // Issue #230: an unknown holder count must not read as zero. A token the // user demonstrably holds — it has a balance — cannot be made unspendable // by a field the block explorer failed to report. test("a token whose holder count is unknown is still offered", () => { render([tokenBalance({ holders: null })]); expect(offeredTokens()).toEqual([NOVEL_TOKEN]); }); test("a token balance carrying no holders field at all is offered", () => { const t = tokenBalance(); delete t.holders; render([t]); expect(offeredTokens()).toEqual([NOVEL_TOKEN]); }); test("the rule is bypassed entirely when the setting is off", () => { state.hideLowHolderTokens = false; render([tokenBalance({ holders: 0 })]); expect(offeredTokens()).toEqual([NOVEL_TOKEN]); }); }); describe("the other send-selector rules are unaffected", () => { test("a token spoofing a known symbol from a wrong address is withheld", () => { render([ tokenBalance({ symbol: "USDC", holders: null }), tokenBalance({ address: USDC_CONTRACT, symbol: "USDC" }), ]); expect(offeredTokens()).toEqual([USDC_CONTRACT.toLowerCase()]); }); test("a blocklisted fraud contract is withheld even with an unknown count", () => { state.fraudContracts = [NOVEL_TOKEN.toUpperCase()]; render([tokenBalance({ holders: null })]); expect(offeredTokens()).toEqual([]); }); });