A new address, a new wallet and a network switch each leave tracked tokens with no balance entry, so their lines read 0.0000 before any refresh had read them. Each of these now sets `balanceNotFetched` on the tracked tokens, and a refresh already clears it. Their lines read `quantity unknown` until then. Addresses that the wallet scan finds get the same. A refresh that did not read every address the page now holds leaves the field set. Model: opus-5-5
763 lines
26 KiB
JavaScript
763 lines
26 KiB
JavaScript
// After "Add" on AddToken, AddressDetail lists the new token. Its line reads
|
|
// `quantity unknown`, not 0.0000, until a balance refresh has read the token's
|
|
// balance at every address, even if the popup closes and opens again
|
|
// meanwhile. A token added on SettingsAddToken reads the same 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).
|
|
//
|
|
// The tracked token's record carries `balanceNotFetched` until a refresh has
|
|
// read the token's balance. Removing the token removes that record. A network
|
|
// switch keeps each network's records apart. A refresh takes the field off the
|
|
// stored record's tokens and keeps another page's changes to the stored list.
|
|
//
|
|
// Every tracked token carries that field again once an address or a wallet is
|
|
// added, and after a network switch, so it reads the same until a refresh has
|
|
// read it (https://git.eeqj.de/sneak/AutistMask/issues/720).
|
|
//
|
|
// 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 { HDNodeWallet, Mnemonic } = require("ethers");
|
|
|
|
const {
|
|
bootPopup,
|
|
cleanupPopup,
|
|
unversionedValidProfile,
|
|
} = require("./support/popupBoot");
|
|
const { hdWalletFromMnemonic } = require("../src/shared/wallet");
|
|
|
|
const PHRASE =
|
|
"abandon abandon abandon abandon abandon abandon " +
|
|
"abandon abandon abandon abandon abandon about";
|
|
const PASSWORD = "correct horse battery staple";
|
|
|
|
// What AddWallet's tabs take. The first and last scan for more addresses.
|
|
const FROM_PHRASE = {
|
|
tab: "tab-mnemonic",
|
|
field: "wallet-mnemonic",
|
|
secret: PHRASE,
|
|
};
|
|
const FROM_PRIVATE_KEY = {
|
|
tab: "tab-privkey",
|
|
field: "import-private-key",
|
|
secret: "0x" + "11".repeat(32),
|
|
};
|
|
const FROM_XPRV = {
|
|
tab: "tab-xprv",
|
|
field: "import-xprv-key",
|
|
secret: HDNodeWallet.fromSeed(Mnemonic.fromPhrase(PHRASE).computeSeed())
|
|
.extendedKey,
|
|
};
|
|
|
|
// Tracked by nobody until a test adds it.
|
|
const NEW_TOKEN = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
|
|
|
// A holding of a million NEW_TOKEN, as refreshBalances() stores it.
|
|
const MILLION = {
|
|
address: NEW_TOKEN,
|
|
name: "New Token",
|
|
symbol: "NEW",
|
|
decimals: 18,
|
|
balance: "1000000.0",
|
|
holders: 3,
|
|
};
|
|
|
|
// NEW_TOKEN as "Add" on AddToken tracks it, once a refresh has read its
|
|
// balance.
|
|
const NEW_TRACKED = {
|
|
address: NEW_TOKEN,
|
|
symbol: "NEW",
|
|
name: "New Token",
|
|
decimals: 18,
|
|
};
|
|
|
|
// A token that another popup page tracks.
|
|
const OTHER_TOKEN = {
|
|
address: "0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
|
|
symbol: "OTH",
|
|
name: "Other Token",
|
|
decimals: 18,
|
|
};
|
|
|
|
afterEach(() => {
|
|
cleanupPopup();
|
|
});
|
|
|
|
// The popup reopened onto AddressDetail for the one address.
|
|
function bootOnAddress(extra) {
|
|
return bootPopup(
|
|
unversionedValidProfile({
|
|
currentView: "address",
|
|
selectedWallet: 0,
|
|
selectedAddress: 0,
|
|
...extra,
|
|
}),
|
|
);
|
|
}
|
|
|
|
// The popup over one wallet that "+" can derive an address from. Its one
|
|
// address holds a million NEW_TOKEN, and NEW_TOKEN is tracked.
|
|
function bootOnDerivingWallet() {
|
|
const { xpub, firstAddress } = hdWalletFromMnemonic(PHRASE);
|
|
return bootPopup(
|
|
unversionedValidProfile({
|
|
wallets: [
|
|
{
|
|
type: "hd",
|
|
name: "Wallet 1",
|
|
xpub,
|
|
encryptedSecret: "encrypted-secret-1",
|
|
nextIndex: 1,
|
|
addresses: [
|
|
{
|
|
address: firstAddress,
|
|
balance: "1.5",
|
|
tokenBalances: [MILLION],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
activeAddress: firstAddress,
|
|
trackedTokens: [NEW_TRACKED],
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Holds the next balance refresh open. The returned function lets it finish
|
|
// with the address's ETH balance at 2 and `tokenBalances` as its token
|
|
// entries, having read the balance of every token tracked when it started.
|
|
// Given null, it finishes having read no token's balance, as when every
|
|
// balanceOf fails, and leaves the token entries as they were.
|
|
function holdNextRefresh(page) {
|
|
const { refreshBalances } = require("../src/shared/balances");
|
|
let release;
|
|
refreshBalances.mockImplementationOnce(
|
|
async (wallets, rpcUrl, explorerUrl, trackedTokens) => {
|
|
const read = trackedTokens.map((t) => t.address.toLowerCase());
|
|
const tokenBalances = await new Promise((resolve) => {
|
|
release = resolve;
|
|
});
|
|
wallets[0].addresses[0].balance = "2.0";
|
|
if (tokenBalances === null) return [];
|
|
wallets[0].addresses[0].tokenBalances = tokenBalances;
|
|
return read;
|
|
},
|
|
);
|
|
return async (tokenBalances) => {
|
|
release(tokenBalances);
|
|
await page.settle();
|
|
};
|
|
}
|
|
|
|
// Runs the ten-second refresh to the end, finishing it as holdNextRefresh()
|
|
// does with `tokenBalances`.
|
|
async function refreshOnce(page, tokenBalances) {
|
|
const finish = holdNextRefresh(page);
|
|
const ticking = page.tick();
|
|
await page.settle();
|
|
await finish(tokenBalances);
|
|
await ticking;
|
|
}
|
|
|
|
// "+ 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");
|
|
}
|
|
|
|
// Settings' "+ Add token", NEW_TOKEN typed in, and "Add", with the contract
|
|
// lookup answering at once. It ends on SettingsAddToken.
|
|
async function addOnSettingsAddToken(page) {
|
|
const { lookupTokenInfo } = require("../src/shared/balances");
|
|
lookupTokenInfo.mockResolvedValueOnce({
|
|
symbol: "NEW",
|
|
decimals: 18,
|
|
name: "New Token",
|
|
});
|
|
await page.click("btn-settings-add-token");
|
|
page.node("settings-addtoken-address").value = NEW_TOKEN;
|
|
await page.click("btn-settings-addtoken-manual");
|
|
}
|
|
|
|
// The "[x]" button Settings draws beside the first tracked token. The page
|
|
// stub does not turn the markup Settings draws into elements. So the list
|
|
// hands Settings a stand-in for that button each time it is drawn, and
|
|
// `press()` presses the last one drawn. Call it before Settings opens.
|
|
function firstRemoveButton(page) {
|
|
const button = { press: null };
|
|
page.node("settings-tracked-tokens").querySelectorAll = () => [
|
|
{
|
|
dataset: { idx: "0" },
|
|
addEventListener: (name, fn) => {
|
|
button.press = fn;
|
|
},
|
|
},
|
|
];
|
|
return button;
|
|
}
|
|
|
|
// Picks `networkId` in the Settings network selector, as the user does.
|
|
async function chooseNetwork(page, networkId) {
|
|
const select = page.node("settings-network");
|
|
select.value = networkId;
|
|
for (const fn of select.listeners.change) await fn();
|
|
await page.settle();
|
|
}
|
|
|
|
// Another popup page saves `trackedTokens` as the tracked token list. This
|
|
// page is not told of the write.
|
|
function anotherPageTracks(page, trackedTokens) {
|
|
page.storage.write("autistmask", {
|
|
...page.storage.read("autistmask"),
|
|
trackedTokens,
|
|
});
|
|
}
|
|
|
|
// 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() + '"'),
|
|
);
|
|
}
|
|
|
|
// Every address's row for NEW_TOKEN on Home, as markup, in the order Home lists
|
|
// the addresses.
|
|
function newTokenRowsOnHome(page) {
|
|
return page
|
|
.node("wallet-list")
|
|
.innerHTML.split("<div ")
|
|
.filter((row) =>
|
|
row.includes('data-token="' + NEW_TOKEN.toLowerCase() + '"'),
|
|
);
|
|
}
|
|
|
|
// The "+" button Home draws for the first wallet. As in firstRemoveButton(),
|
|
// the list hands Home a stand-in for that button each time it is drawn, and
|
|
// `press()` presses the last one drawn. Call it before Home next draws its list.
|
|
function plusButton(page) {
|
|
const button = { press: null };
|
|
page.node("wallet-list").querySelectorAll = (selector) =>
|
|
selector !== ".btn-add-address"
|
|
? []
|
|
: [
|
|
{
|
|
dataset: { wallet: "0" },
|
|
addEventListener: (name, fn) => {
|
|
button.press = () =>
|
|
fn({ stopPropagation: () => {} });
|
|
},
|
|
},
|
|
];
|
|
return button;
|
|
}
|
|
|
|
// "Add wallet" on Home, filled in from `source`, and "Import".
|
|
async function addWallet(page, { tab, field, secret }) {
|
|
await page.click("btn-add-wallet-bottom");
|
|
await page.click(tab);
|
|
page.node(field).value = secret;
|
|
page.node("add-wallet-password").value = PASSWORD;
|
|
page.node("add-wallet-password-confirm").value = PASSWORD;
|
|
await page.click("btn-add-wallet-confirm");
|
|
}
|
|
|
|
// 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 reading its balance", 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 reads the token's balance, which is zero.
|
|
await refreshOnce(page, []);
|
|
|
|
expect(newTokenRow(page)).toContain("0.0000");
|
|
});
|
|
|
|
test("closed and opened again before any refresh has read its balance, 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 reads no token's balance: the boot's stub
|
|
// of refreshBalances() reads 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");
|
|
|
|
await refreshOnce(page, [MILLION]);
|
|
|
|
expect(newTokenRow(page)).toContain("1000000.0000");
|
|
});
|
|
|
|
test("a refresh that could not read the new token's balance leaves its 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("Send for the new token reads its balance as unknown before any refresh has read it, and Max says so", async () => {
|
|
const page = await bootOnAddress();
|
|
const finish = holdNextRefresh(page);
|
|
await addNewToken(page);
|
|
await openToken(page, NEW_TOKEN.toLowerCase());
|
|
|
|
await page.click("btn-address-token-send");
|
|
|
|
expect(page.visibleViews()).toEqual(["send"]);
|
|
expect(page.text("send-balance")).toBe("Current balance: unknown (NEW)");
|
|
|
|
await page.click("btn-send-max");
|
|
|
|
expect(page.text("flash-msg")).toBe("This token's balance is unknown.");
|
|
await finish([MILLION]);
|
|
});
|
|
|
|
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);
|
|
|
|
await page.click("btn-settings");
|
|
await addOnSettingsAddToken(page);
|
|
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");
|
|
|
|
await refreshOnce(page, null);
|
|
|
|
expect(page.node("address-token-balance").innerHTML).toContain("2.0000");
|
|
});
|
|
|
|
test("a token removed before any refresh has read its balance leaves nothing stored, and adding it again tracks it once", async () => {
|
|
const page = await bootPopup(unversionedValidProfile());
|
|
const removeButton = firstRemoveButton(page);
|
|
const stored = () => page.storage.read("autistmask");
|
|
|
|
await page.click("btn-settings");
|
|
await addOnSettingsAddToken(page);
|
|
// Settings opened again from Home draws its list with the token in it.
|
|
await page.click("btn-settings-addtoken-back");
|
|
await page.click("btn-settings-back");
|
|
await page.click("btn-settings");
|
|
await removeButton.press();
|
|
await page.settle();
|
|
|
|
expect(stored().trackedTokens).toEqual([]);
|
|
// Nothing in the stored record names the token any more.
|
|
expect(JSON.stringify(stored()).toLowerCase()).not.toContain(
|
|
NEW_TOKEN.toLowerCase(),
|
|
);
|
|
|
|
await addOnSettingsAddToken(page);
|
|
// A second add of the same token is refused.
|
|
page.node("settings-addtoken-address").value = NEW_TOKEN;
|
|
await page.click("btn-settings-addtoken-manual");
|
|
|
|
const added = { address: NEW_TOKEN, symbol: "NEW", decimals: 18 };
|
|
expect(stored().trackedTokens).toEqual([
|
|
{ ...added, name: "New Token", balanceNotFetched: true },
|
|
]);
|
|
|
|
await refreshOnce(page, []);
|
|
|
|
expect(stored().trackedTokens).toEqual([{ ...added, name: "New Token" }]);
|
|
});
|
|
|
|
test("a refresh on another network leaves the new token reading quantity unknown on its own", async () => {
|
|
// Sepolia tracks a token at the same contract address, also just added.
|
|
const sepoliaToken = {
|
|
address: NEW_TOKEN,
|
|
symbol: "SEP",
|
|
name: "Sepolia Token",
|
|
decimals: 18,
|
|
};
|
|
const page = await bootOnAddress({
|
|
networkTrackedTokens: {
|
|
sepolia: [{ ...sepoliaToken, balanceNotFetched: true }],
|
|
},
|
|
});
|
|
const finishMainnet = holdNextRefresh(page);
|
|
await addNewToken(page);
|
|
|
|
await page.click("btn-settings");
|
|
await chooseNetwork(page, "sepolia");
|
|
// The refresh Add started returns after the switch and writes nothing.
|
|
await finishMainnet([MILLION]);
|
|
// Sepolia's refresh reads the balance of its own token.
|
|
await refreshOnce(page, []);
|
|
|
|
const stored = page.storage.read("autistmask");
|
|
expect(stored.trackedTokens).toEqual([sepoliaToken]);
|
|
expect(stored.networkTrackedTokens.mainnet[0].balanceNotFetched).toBe(true);
|
|
|
|
await chooseNetwork(page, "mainnet");
|
|
await page.click("btn-settings-back");
|
|
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
expect(newTokenRow(page)).toContain("quantity unknown");
|
|
|
|
await refreshOnce(page, [MILLION]);
|
|
|
|
expect(newTokenRow(page)).toContain("1000000.0000");
|
|
});
|
|
|
|
test("a refresh keeps a token that another page added after this page loaded", async () => {
|
|
const page = await bootOnAddress();
|
|
const finish = holdNextRefresh(page);
|
|
await addNewToken(page);
|
|
const otherAdded = { ...OTHER_TOKEN, balanceNotFetched: true };
|
|
anotherPageTracks(page, [
|
|
{ ...NEW_TRACKED, balanceNotFetched: true },
|
|
otherAdded,
|
|
]);
|
|
|
|
await finish([MILLION]);
|
|
|
|
expect(page.storage.read("autistmask").trackedTokens).toEqual([
|
|
NEW_TRACKED,
|
|
otherAdded,
|
|
]);
|
|
expect(newTokenRow(page)).toContain("1000000.0000");
|
|
});
|
|
|
|
test("a refresh does not bring back a token that another page removed after this page loaded", async () => {
|
|
const page = await bootOnAddress({ trackedTokens: [OTHER_TOKEN] });
|
|
const finish = holdNextRefresh(page);
|
|
await addNewToken(page);
|
|
anotherPageTracks(page, [{ ...NEW_TRACKED, balanceNotFetched: true }]);
|
|
|
|
await finish([MILLION]);
|
|
|
|
expect(page.storage.read("autistmask").trackedTokens).toEqual([
|
|
NEW_TRACKED,
|
|
]);
|
|
});
|
|
|
|
test("an address just added reads quantity unknown for a tracked token until a refresh has read it", async () => {
|
|
const page = await bootOnDerivingWallet();
|
|
const plus = plusButton(page);
|
|
await refreshOnce(page, [MILLION]);
|
|
const finish = holdNextRefresh(page);
|
|
|
|
await plus.press();
|
|
await page.settle();
|
|
|
|
const [held, added] = newTokenRowsOnHome(page);
|
|
expect(held).toContain("1000000.0000");
|
|
expect(added).toContain("quantity unknown");
|
|
expect(added).not.toContain("0.0000");
|
|
expect(
|
|
page.storage.read("autistmask").trackedTokens[0].balanceNotFetched,
|
|
).toBe(true);
|
|
|
|
// It reads the new address's balance, which is zero.
|
|
await finish([MILLION]);
|
|
|
|
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
|
});
|
|
|
|
test('an address "+" adds during a refresh reads quantity unknown when that refresh returns', async () => {
|
|
const page = await bootOnDerivingWallet();
|
|
const plus = plusButton(page);
|
|
await refreshOnce(page, [MILLION]);
|
|
const finishRunning = holdNextRefresh(page);
|
|
const ticking = page.tick();
|
|
await page.settle();
|
|
|
|
await plus.press();
|
|
await page.settle();
|
|
const finishNext = holdNextRefresh(page);
|
|
await finishRunning([MILLION]);
|
|
await ticking;
|
|
|
|
const added = newTokenRowsOnHome(page)[1];
|
|
expect(added).toContain("quantity unknown");
|
|
expect(added).not.toContain("0.0000");
|
|
|
|
// It reads the new address's balance, which is zero.
|
|
await finishNext([MILLION]);
|
|
|
|
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
|
});
|
|
|
|
test.each([FROM_PHRASE, FROM_PRIVATE_KEY, FROM_XPRV])(
|
|
"a wallet just added from $tab reads quantity unknown for a tracked token until a refresh has read it",
|
|
async (source) => {
|
|
const page = await bootPopup(
|
|
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
|
);
|
|
const finish = holdNextRefresh(page);
|
|
|
|
await addWallet(page, source);
|
|
|
|
const added = newTokenRowsOnHome(page)[1];
|
|
expect(added).toContain("quantity unknown");
|
|
expect(added).not.toContain("0.0000");
|
|
|
|
// It reads the new address's balance, which is zero.
|
|
await finish([]);
|
|
|
|
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
|
},
|
|
);
|
|
|
|
test("a wallet from a private key added during a refresh reads quantity unknown when that refresh returns", async () => {
|
|
const page = await bootPopup(
|
|
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
|
);
|
|
const finishRunning = holdNextRefresh(page);
|
|
const ticking = page.tick();
|
|
await page.settle();
|
|
|
|
await addWallet(page, FROM_PRIVATE_KEY);
|
|
const finishNext = holdNextRefresh(page);
|
|
await finishRunning([]);
|
|
await ticking;
|
|
|
|
const added = newTokenRowsOnHome(page)[1];
|
|
expect(added).toContain("quantity unknown");
|
|
expect(added).not.toContain("0.0000");
|
|
|
|
// It reads the new address's balance, which is zero.
|
|
await finishNext([]);
|
|
|
|
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
|
});
|
|
|
|
test.each([FROM_PHRASE, FROM_XPRV])(
|
|
"addresses the scan finds for a wallet from $tab read quantity unknown, even after a refresh returned during the scan",
|
|
async (source) => {
|
|
const page = await bootPopup(
|
|
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
|
);
|
|
const { scanForAddresses } = require("../src/shared/balances");
|
|
let finish;
|
|
scanForAddresses.mockImplementationOnce(async () => {
|
|
await refreshOnce(page, []);
|
|
finish = holdNextRefresh(page);
|
|
return {
|
|
addresses: [
|
|
{ address: "0x" + "dd".repeat(20), index: 0 },
|
|
{ address: "0x" + "ee".repeat(20), index: 1 },
|
|
],
|
|
nextIndex: 2,
|
|
};
|
|
});
|
|
|
|
await addWallet(page, source);
|
|
|
|
const [, first, second] = newTokenRowsOnHome(page);
|
|
for (const found of [first, second]) {
|
|
expect(found).toContain("quantity unknown");
|
|
expect(found).not.toContain("0.0000");
|
|
}
|
|
|
|
await finish([]);
|
|
|
|
expect(newTokenRowsOnHome(page)[2]).toContain("0.0000");
|
|
},
|
|
);
|
|
|
|
test.each([FROM_PHRASE, FROM_XPRV])(
|
|
"addresses the scan finds for a wallet from $tab read quantity unknown when a refresh running as the scan ends returns",
|
|
async (source) => {
|
|
const page = await bootPopup(
|
|
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
|
);
|
|
const { scanForAddresses } = require("../src/shared/balances");
|
|
let finishRunning;
|
|
let ticking;
|
|
scanForAddresses.mockImplementationOnce(async () => {
|
|
finishRunning = holdNextRefresh(page);
|
|
ticking = page.tick();
|
|
await page.settle();
|
|
return {
|
|
addresses: [
|
|
{ address: "0x" + "dd".repeat(20), index: 0 },
|
|
{ address: "0x" + "ee".repeat(20), index: 1 },
|
|
],
|
|
nextIndex: 2,
|
|
};
|
|
});
|
|
|
|
await addWallet(page, source);
|
|
const finishNext = holdNextRefresh(page);
|
|
await finishRunning([]);
|
|
await ticking;
|
|
|
|
const [, first, second] = newTokenRowsOnHome(page);
|
|
for (const found of [first, second]) {
|
|
expect(found).toContain("quantity unknown");
|
|
expect(found).not.toContain("0.0000");
|
|
}
|
|
|
|
// It reads the new addresses' balances, which are zero.
|
|
await finishNext([]);
|
|
|
|
expect(newTokenRowsOnHome(page)[2]).toContain("0.0000");
|
|
},
|
|
);
|
|
|
|
test("after a network switch in Settings, a tracked token reads quantity unknown until a refresh has read it", async () => {
|
|
const page = await bootOnAddress({
|
|
trackedTokens: [OTHER_TOKEN],
|
|
networkTrackedTokens: { sepolia: [NEW_TRACKED] },
|
|
});
|
|
|
|
await page.click("btn-settings");
|
|
await chooseNetwork(page, "sepolia");
|
|
await page.click("btn-settings-back");
|
|
|
|
expect(page.visibleViews()).toEqual(["address"]);
|
|
const row = newTokenRow(page);
|
|
expect(row).toContain("quantity unknown");
|
|
expect(row).not.toContain("0.0000");
|
|
|
|
// It reads the balance, which is zero.
|
|
await refreshOnce(page, []);
|
|
|
|
expect(newTokenRow(page)).toContain("0.0000");
|
|
});
|
|
|
|
test("after a switch saved by the background, a tracked token reads quantity unknown until a refresh has read it", async () => {
|
|
const page = await bootOnAddress({
|
|
networkTrackedTokens: { sepolia: [NEW_TRACKED] },
|
|
});
|
|
const {
|
|
applyChainSwitchFields,
|
|
} = require("../src/shared/chainSwitchFields");
|
|
const record = page.storage.read("autistmask");
|
|
applyChainSwitchFields(record, "sepolia");
|
|
page.storage.write("autistmask", record);
|
|
|
|
// The popup takes the switch, and this refresh reads no token's balance.
|
|
await page.tick();
|
|
|
|
const row = newTokenRow(page);
|
|
expect(row).toContain("quantity unknown");
|
|
expect(row).not.toContain("0.0000");
|
|
});
|