fix: an address holding only unpriced tokens is no longer totalled at $0.00 (closes #261)
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
This commit was merged in pull request #298.
This commit is contained in:
238
tests/addressValue.test.js
Normal file
238
tests/addressValue.test.js
Normal file
@@ -0,0 +1,238 @@
|
||||
// The USD total of an address that holds something this build cannot price
|
||||
// (issue #261).
|
||||
//
|
||||
// Prices exist for the top 25 tokens only, so an address can hold real assets
|
||||
// with no price attached. Summing what is priced and printing the result as
|
||||
// the total says "$0.00" for an address holding nothing but unpriced tokens —
|
||||
// worth-nothing and worth-an-unknown-amount collapsed into one number, in the
|
||||
// direction that matters. The two are separate facts here, the same way an
|
||||
// absent holders_count is not a count of zero.
|
||||
//
|
||||
// The value and its rendering are asserted directly, and then through the two
|
||||
// call sites that return their markup as a string: the wallet list on Home and
|
||||
// the balance warning on the address-removal confirmation. AddressDetail and
|
||||
// the Home summary line render into the DOM and are covered by tests/e2e.
|
||||
|
||||
// helpers.js pulls in state.js, which reads chrome.storage.local at load.
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
};
|
||||
|
||||
const {
|
||||
prices,
|
||||
clearPrices,
|
||||
getAddressValue,
|
||||
getWalletValue,
|
||||
getTotalValue,
|
||||
formatAddressTotal,
|
||||
} = require("../src/shared/prices");
|
||||
const { state } = require("../src/shared/state");
|
||||
const { walletListHtml } = require("../src/popup/views/home");
|
||||
const { balanceWarningHtml } = require("../src/popup/views/deleteAddress");
|
||||
|
||||
const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
||||
const NOVEL = "0x1111111111111111111111111111111111111111";
|
||||
|
||||
// No ETH, and a token no price is known for. The case the user is told is
|
||||
// worth $0.00 today.
|
||||
const UNPRICED_ONLY = {
|
||||
address: "0x" + "a".repeat(40),
|
||||
balance: "0",
|
||||
tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "5000.0" }],
|
||||
};
|
||||
|
||||
// Nothing at all: the address really is worth zero.
|
||||
const EMPTY = {
|
||||
address: "0x" + "b".repeat(40),
|
||||
balance: "0",
|
||||
tokenBalances: [],
|
||||
};
|
||||
|
||||
// Every holding priced.
|
||||
const FULLY_PRICED = {
|
||||
address: "0x" + "c".repeat(40),
|
||||
balance: "1.5",
|
||||
tokenBalances: [{ address: USDC, symbol: "USDC", balance: "2500.0" }],
|
||||
};
|
||||
|
||||
// Part priced, part not: 1.5 ETH plus a token with no price.
|
||||
const PARTLY_PRICED = {
|
||||
address: "0x" + "d".repeat(40),
|
||||
balance: "1.5",
|
||||
tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "5000.0" }],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
clearPrices();
|
||||
prices.ETH = 2000;
|
||||
prices.USDC = 1;
|
||||
state.wallets = [];
|
||||
state.trackedTokens = [];
|
||||
state.showZeroBalanceTokens = false;
|
||||
state.activeAddress = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearPrices();
|
||||
});
|
||||
|
||||
// The total line only, in each of the two markup-returning call sites. The
|
||||
// ETH balance line above it legitimately reads $0.00 for an address with no
|
||||
// ETH, so the assertions have to name the line under test.
|
||||
function walletListTotal(addr) {
|
||||
state.wallets = [{ name: "Wallet 1", type: "hd", addresses: [addr] }];
|
||||
const match = walletListHtml().match(/min-h-\[1rem\]">([^<]*)</);
|
||||
return match && match[1];
|
||||
}
|
||||
|
||||
function removalWarningTotal(addr) {
|
||||
const match = balanceWarningHtml(addr).match(/mt-1">([^<]*)</);
|
||||
return match && match[1];
|
||||
}
|
||||
|
||||
describe("the value of an address, and whether it is the whole value", () => {
|
||||
test("an address holding only unpriced tokens has an incomplete value", () => {
|
||||
expect(getAddressValue(UNPRICED_ONLY)).toEqual({
|
||||
usd: 0,
|
||||
partial: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("an address holding nothing is complete, and zero", () => {
|
||||
expect(getAddressValue(EMPTY)).toEqual({ usd: 0, partial: false });
|
||||
});
|
||||
|
||||
test("a fully priced address is complete, and unchanged", () => {
|
||||
expect(getAddressValue(FULLY_PRICED)).toEqual({
|
||||
usd: 5500,
|
||||
partial: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("a partly priced address keeps the part it can price", () => {
|
||||
expect(getAddressValue(PARTLY_PRICED)).toEqual({
|
||||
usd: 3000,
|
||||
partial: true,
|
||||
});
|
||||
});
|
||||
|
||||
// A token balance of zero is not a holding, so it cannot make the total
|
||||
// incomplete: an address with a spent-out unpriced token is worth zero.
|
||||
test("a zero balance in an unpriced token leaves the value complete", () => {
|
||||
const addr = {
|
||||
address: "0x1",
|
||||
balance: "0",
|
||||
tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "0" }],
|
||||
};
|
||||
expect(getAddressValue(addr)).toEqual({ usd: 0, partial: false });
|
||||
});
|
||||
|
||||
// Before the first price fetch, and on testnet, nothing is knowable: that
|
||||
// is a third state, and it stays distinct from both of the others.
|
||||
test("no prices at all means no value, not an incomplete one", () => {
|
||||
clearPrices();
|
||||
expect(getAddressValue(FULLY_PRICED)).toEqual({
|
||||
usd: null,
|
||||
partial: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("one unpriced holding makes a wallet and the grand total partial", () => {
|
||||
const wallet = { addresses: [FULLY_PRICED, UNPRICED_ONLY] };
|
||||
expect(getWalletValue(wallet)).toEqual({ usd: 5500, partial: true });
|
||||
expect(getTotalValue([wallet])).toEqual({ usd: 5500, partial: true });
|
||||
});
|
||||
|
||||
test("a wallet of fully priced addresses stays complete", () => {
|
||||
const wallet = { addresses: [FULLY_PRICED, EMPTY] };
|
||||
expect(getWalletValue(wallet)).toEqual({ usd: 5500, partial: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("how that value is written on screen", () => {
|
||||
test("a complete total is the figure", () => {
|
||||
expect(formatAddressTotal(getAddressValue(FULLY_PRICED))).toBe(
|
||||
"Total: $5,500.00",
|
||||
);
|
||||
});
|
||||
|
||||
test("an address worth zero says so", () => {
|
||||
expect(formatAddressTotal(getAddressValue(EMPTY))).toBe("Total: $0.00");
|
||||
});
|
||||
|
||||
// The figure is still worth having — it is the ETH the user does hold —
|
||||
// but on its own it understates the address, so it is named as partial.
|
||||
test("a partly priced total is given, and marked as partial", () => {
|
||||
expect(formatAddressTotal(getAddressValue(PARTLY_PRICED))).toBe(
|
||||
"Total: $3,000.00 plus unpriced tokens",
|
||||
);
|
||||
});
|
||||
|
||||
// Nothing priced is held, so there is no figure to give: printing the
|
||||
// $0.00 sum of an empty set is the bug.
|
||||
test("a total with nothing priced in it gives no figure", () => {
|
||||
const line = formatAddressTotal(getAddressValue(UNPRICED_ONLY));
|
||||
expect(line).toBe("Total: unpriced tokens only");
|
||||
expect(line).not.toContain("$");
|
||||
});
|
||||
|
||||
test("an unknown value is written as nothing at all", () => {
|
||||
clearPrices();
|
||||
expect(formatAddressTotal(getAddressValue(FULLY_PRICED))).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the wallet list on Home", () => {
|
||||
test("an address holding only unpriced tokens is not totalled at $0.00", () => {
|
||||
expect(walletListTotal(UNPRICED_ONLY)).toBe(
|
||||
"Total: unpriced tokens only",
|
||||
);
|
||||
});
|
||||
|
||||
test("an address holding nothing is still totalled at $0.00", () => {
|
||||
expect(walletListTotal(EMPTY)).toBe("Total: $0.00");
|
||||
});
|
||||
|
||||
test("a fully priced address shows its total", () => {
|
||||
expect(walletListTotal(FULLY_PRICED)).toBe("Total: $5,500.00");
|
||||
});
|
||||
|
||||
test("a partly priced address shows the priced part, marked partial", () => {
|
||||
expect(walletListTotal(PARTLY_PRICED)).toBe(
|
||||
"Total: $3,000.00 plus unpriced tokens",
|
||||
);
|
||||
});
|
||||
|
||||
test("an address whose value is unknown keeps its blank line", () => {
|
||||
clearPrices();
|
||||
expect(walletListTotal(FULLY_PRICED)).toBe(" ");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the balance warning on the address-removal confirmation", () => {
|
||||
// "This address holds a balance." followed by "Total: $0.00" is a flat
|
||||
// contradiction, on the one screen whose job is to warn.
|
||||
test("an address holding only unpriced tokens is not totalled at $0.00", () => {
|
||||
expect(balanceWarningHtml(UNPRICED_ONLY)).toContain(
|
||||
"This address holds a balance.",
|
||||
);
|
||||
expect(removalWarningTotal(UNPRICED_ONLY)).toBe(
|
||||
"Total: unpriced tokens only",
|
||||
);
|
||||
});
|
||||
|
||||
test("a fully priced address still shows its total", () => {
|
||||
expect(removalWarningTotal(FULLY_PRICED)).toBe("Total: $5,500.00");
|
||||
});
|
||||
|
||||
test("a partly priced address shows the priced part, marked partial", () => {
|
||||
expect(removalWarningTotal(PARTLY_PRICED)).toBe(
|
||||
"Total: $3,000.00 plus unpriced tokens",
|
||||
);
|
||||
});
|
||||
|
||||
test("no total line is written when the value is unknown", () => {
|
||||
clearPrices();
|
||||
expect(removalWarningTotal(FULLY_PRICED)).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -150,7 +150,7 @@ describe("the balance warning on the removal confirmation", () => {
|
||||
expect(balanceWarningHtml(ETH_ONLY)).toContain("Total: $3,000.00");
|
||||
});
|
||||
|
||||
// getAddressValueUsd() returns null on testnet and before the first
|
||||
// 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:");
|
||||
|
||||
Reference in New Issue
Block a user