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
274 lines
9.4 KiB
JavaScript
274 lines
9.4 KiB
JavaScript
// After "Add" on AddToken, AddressDetail lists the new token, and its line reads
|
|
// `quantity unknown`, not 0.0000, until a balance refresh that asked the
|
|
// explorer about the token has returned for every address, the popup closing
|
|
// and opening again meanwhile included; so does a token added on
|
|
// SettingsAddToken, wherever it is listed. A refresh that returns draws the
|
|
// balances on AddressDetail or AddressToken again, so the fetched balance shows
|
|
// without the screen being reopened
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/516). Driven through the real
|
|
// popup over a stored profile on Ethereum Mainnet, with "Show tracked tokens
|
|
// with zero balance" on, as it is by default.
|
|
|
|
const {
|
|
bootPopup,
|
|
cleanupPopup,
|
|
unversionedValidProfile,
|
|
} = require("./support/popupBoot");
|
|
|
|
// Tracked by nobody until a test adds it.
|
|
const NEW_TOKEN = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
|
|
|
// The explorer's row for a holding of a million NEW_TOKEN, in the shape
|
|
// fetchTokenBalances() stores it.
|
|
const MILLION = {
|
|
address: NEW_TOKEN,
|
|
name: "New Token",
|
|
symbol: "NEW",
|
|
decimals: 18,
|
|
balance: "1000000.0",
|
|
holders: 3,
|
|
};
|
|
|
|
afterEach(() => {
|
|
cleanupPopup();
|
|
});
|
|
|
|
// The popup reopened onto AddressDetail for the one address.
|
|
function bootOnAddress() {
|
|
return bootPopup(
|
|
unversionedValidProfile({
|
|
currentView: "address",
|
|
selectedWallet: 0,
|
|
selectedAddress: 0,
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Holds the next balance refresh open. The returned function lets it finish
|
|
// with the address's ETH balance at 2 and `tokenBalances` as the explorer's
|
|
// rows for it, or with no token balances at all when given null: an explorer
|
|
// that did not answer, which the real refreshBalances() leaves unwritten.
|
|
function holdNextRefresh(page) {
|
|
const { refreshBalances } = require("../src/shared/balances");
|
|
let release;
|
|
refreshBalances.mockImplementationOnce(async (wallets) => {
|
|
const tokenBalances = await new Promise((resolve) => {
|
|
release = resolve;
|
|
});
|
|
wallets[0].addresses[0].balance = "2.0";
|
|
if (tokenBalances !== null) {
|
|
wallets[0].addresses[0].tokenBalances = tokenBalances;
|
|
}
|
|
});
|
|
return async (tokenBalances) => {
|
|
release(tokenBalances);
|
|
await page.settle();
|
|
};
|
|
}
|
|
|
|
// "+ Token", NEW_TOKEN typed in, and "Add", with the contract lookup
|
|
// answering at once.
|
|
async function addNewToken(page) {
|
|
const { lookupTokenInfo } = require("../src/shared/balances");
|
|
lookupTokenInfo.mockResolvedValueOnce({
|
|
symbol: "NEW",
|
|
decimals: 18,
|
|
name: "New Token",
|
|
});
|
|
await page.click("btn-add-token");
|
|
page.node("add-token-address").value = NEW_TOKEN;
|
|
await page.click("btn-add-token-confirm");
|
|
}
|
|
|
|
// NEW_TOKEN's row in the balance list in element `id`, AddressDetail's unless
|
|
// another is named, as markup.
|
|
function newTokenRow(page, id = "address-balances") {
|
|
return page
|
|
.node(id)
|
|
.innerHTML.split("<div ")
|
|
.find((row) =>
|
|
row.includes('data-token="' + NEW_TOKEN.toLowerCase() + '"'),
|
|
);
|
|
}
|
|
|
|
// AddressDetail for the one address opened from Home, as its `[info]` button
|
|
// does.
|
|
async function openAddress(page) {
|
|
const { state } = require("../src/shared/state");
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
require("../src/popup/views/helpers").pushCurrentView();
|
|
require("../src/popup/views/addressDetail").show();
|
|
await page.settle();
|
|
}
|
|
|
|
// AddressToken for `token` opened from AddressDetail, as tapping the token's
|
|
// row there does.
|
|
async function openToken(page, token) {
|
|
const { state } = require("../src/shared/state");
|
|
state.selectedToken = token;
|
|
require("../src/popup/views/helpers").pushCurrentView();
|
|
require("../src/popup/views/addressToken").show();
|
|
await page.settle();
|
|
}
|
|
|
|
test("until the refresh Add started returns, the new token's line reads quantity unknown", async () => {
|
|
const page = await bootOnAddress();
|
|
expect(page.pageErrors).toEqual([]);
|
|
const finish = holdNextRefresh(page);
|
|
|
|
await addNewToken(page);
|
|
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
const row = newTokenRow(page);
|
|
expect(row).toContain("quantity unknown");
|
|
expect(row).not.toContain("0.0000");
|
|
await finish([MILLION]);
|
|
});
|
|
|
|
test("once that refresh returns, AddressDetail shows the fetched balance without being reopened", async () => {
|
|
const page = await bootOnAddress();
|
|
const finish = holdNextRefresh(page);
|
|
await addNewToken(page);
|
|
// Stands for the transactions on the screen, which only show() replaces.
|
|
page.node("tx-list").innerHTML = "transactions on the screen";
|
|
|
|
await finish([MILLION]);
|
|
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
expect(newTokenRow(page)).toContain("1000000.0000");
|
|
// Only the balances are drawn again, not the transaction list.
|
|
expect(page.node("tx-list").innerHTML).toBe("transactions on the screen");
|
|
});
|
|
|
|
test("a refresh already running when the token is added does not count as fetching it", async () => {
|
|
const page = await bootOnAddress();
|
|
const finishRunning = holdNextRefresh(page);
|
|
const ticking = page.tick();
|
|
await page.settle();
|
|
|
|
await addNewToken(page);
|
|
await finishRunning([]);
|
|
await ticking;
|
|
|
|
expect(newTokenRow(page)).toContain("quantity unknown");
|
|
|
|
// The next one asks the explorer about the token, which lists no holding
|
|
// of it: a fetched zero.
|
|
const finishNext = holdNextRefresh(page);
|
|
const next = page.tick();
|
|
await page.settle();
|
|
await finishNext([]);
|
|
await next;
|
|
|
|
expect(newTokenRow(page)).toContain("0.0000");
|
|
});
|
|
|
|
test("closed and opened again before any refresh has fetched it, the new token's line still reads quantity unknown", async () => {
|
|
const closed = await bootOnAddress();
|
|
holdNextRefresh(closed);
|
|
await addNewToken(closed);
|
|
cleanupPopup();
|
|
|
|
// The refresh this boot starts gets no token balances back: the boot's
|
|
// stub of refreshBalances() writes none.
|
|
const page = await bootPopup(undefined, { storage: closed.storage });
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
const row = newTokenRow(page);
|
|
expect(row).toContain("quantity unknown");
|
|
expect(row).not.toContain("0.0000");
|
|
|
|
const finish = holdNextRefresh(page);
|
|
const ticking = page.tick();
|
|
await page.settle();
|
|
await finish([MILLION]);
|
|
await ticking;
|
|
|
|
expect(newTokenRow(page)).toContain("1000000.0000");
|
|
});
|
|
|
|
test("a refresh in which the explorer did not answer leaves the new token's line quantity unknown", async () => {
|
|
const page = await bootOnAddress();
|
|
const finish = holdNextRefresh(page);
|
|
await addNewToken(page);
|
|
|
|
await finish(null);
|
|
|
|
// Drawn again: the ETH balance it fetched is shown.
|
|
expect(page.node("address-balances").innerHTML).toContain("2.0000");
|
|
expect(newTokenRow(page)).toContain("quantity unknown");
|
|
});
|
|
|
|
test("AddressToken for the new token reads quantity unknown until the refresh returns, then the fetched balance", async () => {
|
|
const page = await bootOnAddress();
|
|
const finish = holdNextRefresh(page);
|
|
await addNewToken(page);
|
|
|
|
await openToken(page, NEW_TOKEN.toLowerCase());
|
|
|
|
expect(page.visibleViews()).toEqual(["address-token"]);
|
|
const before = page.node("address-token-balance").innerHTML;
|
|
expect(before).toContain("quantity unknown");
|
|
expect(before).not.toContain("0.0000");
|
|
|
|
await finish([MILLION]);
|
|
|
|
expect(page.node("address-token-balance").innerHTML).toContain(
|
|
"1000000.0000",
|
|
);
|
|
});
|
|
|
|
test("a token added by contract address on SettingsAddToken reads quantity unknown until the refresh returns", async () => {
|
|
const page = await bootPopup(unversionedValidProfile());
|
|
expect(page.visibleViews()).toEqual(["main"]);
|
|
const finish = holdNextRefresh(page);
|
|
const { lookupTokenInfo } = require("../src/shared/balances");
|
|
lookupTokenInfo.mockResolvedValueOnce({
|
|
symbol: "NEW",
|
|
decimals: 18,
|
|
name: "New Token",
|
|
});
|
|
|
|
await page.click("btn-settings");
|
|
await page.click("btn-settings-add-token");
|
|
page.node("settings-addtoken-address").value = NEW_TOKEN;
|
|
await page.click("btn-settings-addtoken-manual");
|
|
await page.click("btn-settings-addtoken-back");
|
|
await page.click("btn-settings-back");
|
|
expect(page.visibleViews()).toEqual(["main"]);
|
|
const onHome = newTokenRow(page, "wallet-list");
|
|
expect(onHome).toContain("quantity unknown");
|
|
expect(onHome).not.toContain("0.0000");
|
|
|
|
await openAddress(page);
|
|
const onAddress = newTokenRow(page);
|
|
expect(onAddress).toContain("quantity unknown");
|
|
expect(onAddress).not.toContain("0.0000");
|
|
|
|
await finish([MILLION]);
|
|
|
|
expect(newTokenRow(page)).toContain("1000000.0000");
|
|
});
|
|
|
|
test("a refresh that returns while AddressToken is up draws its balance again", async () => {
|
|
const page = await bootPopup(
|
|
unversionedValidProfile({
|
|
currentView: "address-token",
|
|
viewStack: ["main", "address"],
|
|
selectedWallet: 0,
|
|
selectedAddress: 0,
|
|
selectedToken: "ETH",
|
|
}),
|
|
);
|
|
expect(page.visibleViews()).toEqual(["address-token"]);
|
|
expect(page.node("address-token-balance").innerHTML).toContain("1.5000");
|
|
const finish = holdNextRefresh(page);
|
|
const ticking = page.tick();
|
|
await page.settle();
|
|
|
|
await finish(null);
|
|
await ticking;
|
|
|
|
expect(page.node("address-token-balance").innerHTML).toContain("2.0000");
|
|
});
|