// Blockscout and Etherscan are sent none of the user's data until the user
// turns them on (https://git.eeqj.de/sneak/AutistMask/issues/668). A new
// profile has "Use Blockscout" and "Check recipients on Etherscan" both off.
// While a setting is off, the code that would ask its service asks nothing.
//
// The one request made with the setting off is the check behind "Save" beside
// the Blockscout URL in Settings. It asks that URL only for `/stats` and sends
// none of the user's data.
//
// A request that either setting gates reads the stored setting, not the page's
// copy, before it asks and again when the answer lands. So a change another
// page saved applies to it at once. An open page also takes such a change and
// draws its screens again.
//
// Only Blockscout reports the tokens the user has not tracked. While "Use
// Blockscout" is off, no address keeps them and no screen lists them.
//
// The functions that make the requests are driven directly, against a fetch()
// that records every URL it is asked for. The screens are driven through the
// real popup over a stored profile (tests/support/popupBoot.js). Its fetch()
// stub records the same way.
"use strict";
const { FetchRequest } = require("ethers");
const {
bootPopup,
cleanupPopup,
unversionedValidProfile,
ADDRESS,
TOKEN_ADDRESS,
} = require("./support/popupBoot");
const { makeStorageStub } = require("./support/storageStub");
const {
DEFAULT_STATE,
normalizePersisted,
} = require("../src/shared/persistedState");
const {
fetchTokenBalances,
refreshBalances,
} = require("../src/shared/balances");
const { fetchRecentTransactions } = require("../src/shared/transactions");
const { getFullWarnings } = require("../src/shared/addressWarnings");
const { checkEtherscanLabel } = require("../src/shared/etherscanLabels");
const { HISTORY_OFF_TEXT, escapeHtml } = require("../src/popup/views/helpers");
const RPC_URL = "https://rpc.example.invalid";
const BLOCKSCOUT = "https://eth.blockscout.com/api/v2";
const RECIPIENT = "0x2222222222222222222222222222222222222222";
const TX_HASH = "0x" + "ab".repeat(32);
const ZERO_WORD = "0x" + "0".repeat(64);
// USDC, from the bundled list, as Blockscout reports a holding of it.
const USDC_ROW = {
value: "5000000",
token: {
type: "ERC-20",
address_hash: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
symbol: "USDC",
name: "USD Coin",
decimals: "6",
holders_count: "50000",
},
};
// A transfer of 1 ETH from RECIPIENT to ADDRESS, as Blockscout lists it in
// ADDRESS's history.
const HISTORY_ITEM = {
hash: TX_HASH,
block_number: 21000000,
timestamp: "2026-02-27T12:00:00.000Z",
from: { hash: RECIPIENT },
to: { hash: ADDRESS, is_contract: false },
value: "1000000000000000000",
method: null,
status: "ok",
};
// Etherscan's page for RECIPIENT, which labels it a phishing address.
const PHISHING_PAGE =
"
Fake_Phishing1 | Address: " +
RECIPIENT +
" | Etherscan";
const realFetch = globalThis.fetch;
afterEach(() => {
cleanupPopup();
jest.restoreAllMocks();
});
describe("a new profile", () => {
test("has both settings off", () => {
expect(DEFAULT_STATE.useBlockscout).toBe(false);
expect(DEFAULT_STATE.checkRecipientsOnEtherscan).toBe(false);
const loaded = normalizePersisted(undefined);
expect(loaded.useBlockscout).toBe(false);
expect(loaded.checkRecipientsOnEtherscan).toBe(false);
});
test("keeps either setting the user turned on", () => {
const loaded = normalizePersisted({
useBlockscout: true,
checkRecipientsOnEtherscan: true,
});
expect(loaded.useBlockscout).toBe(true);
expect(loaded.checkRecipientsOnEtherscan).toBe(true);
});
});
describe("where each request is made", () => {
// Every URL fetch() was asked for, in order.
let asked;
// What Blockscout and Etherscan answer: USDC among ADDRESS's tokens,
// HISTORY_ITEM in its history, and PHISHING_PAGE for RECIPIENT.
function answer(url) {
return {
ok: true,
status: 200,
json: async () => {
if (url.endsWith("/token-balances")) return [USDC_ROW];
if (url.endsWith("/transactions")) {
return { items: [HISTORY_ITEM] };
}
return { items: [] };
},
text: async () => PHISHING_PAGE,
};
}
async function recordAndAnswer(url) {
asked.push(String(url));
return answer(String(url));
}
beforeEach(() => {
asked = [];
globalThis.fetch = jest.fn(recordAndAnswer);
});
afterEach(() => {
globalThis.fetch = realFetch;
delete globalThis.chrome;
});
// Storage holds `profile` as the stored profile, or nothing when it is
// undefined.
function storing(profile) {
globalThis.chrome = {
storage: makeStorageStub(
profile === undefined ? {} : { autistmask: profile },
),
};
}
test("Blockscout's token balances are asked for only while Use Blockscout is on", async () => {
storing({ useBlockscout: false });
expect(await fetchTokenBalances(ADDRESS, BLOCKSCOUT, [])).toEqual([]);
expect(asked).toEqual([]);
storing({ useBlockscout: true });
const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, []);
expect(rows.map((row) => row.symbol)).toEqual(["USDC"]);
expect(asked).toEqual([
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
]);
});
test("the transaction history is asked for only while Use Blockscout is on", async () => {
storing({ useBlockscout: false });
expect(
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"),
).toEqual([]);
expect(asked).toEqual([]);
storing({ useBlockscout: true });
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1");
expect(asked.sort()).toEqual([
BLOCKSCOUT +
"/addresses/" +
ADDRESS +
"/token-transfers?type=ERC-20",
BLOCKSCOUT + "/addresses/" + ADDRESS + "/transactions",
]);
});
test("the Etherscan label check is made only while the stored profile has Check recipients on Etherscan on", async () => {
const provider = {
getCode: async () => "0x",
getTransactionCount: async () => 1,
getBalance: async () => 0n,
};
const checkWith = async (profile) => {
storing(profile);
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
};
await checkWith(undefined);
await checkWith({ checkRecipientsOnEtherscan: false });
// Only a stored true is on, as when the profile is loaded.
await checkWith({ checkRecipientsOnEtherscan: "true" });
await checkWith({ checkRecipientsOnEtherscan: 42 });
expect(asked).toEqual([]);
await checkWith({ checkRecipientsOnEtherscan: true });
expect(asked).toEqual(["https://etherscan.io/address/" + RECIPIENT]);
});
// Asks with `setting` on in storage. Once every request is out, storage
// holds `after` for it, as when the user changes it here or on another
// page. Only then does each request get its answer.
async function answeredAfter(setting, after, ask) {
storing({ [setting]: true });
let release;
const held = new Promise((resolve) => {
release = resolve;
});
let requested;
const out = new Promise((resolve) => {
requested = resolve;
});
globalThis.fetch = jest.fn(async (url) => {
requested();
await held;
return recordAndAnswer(url);
});
const result = ask();
await out;
globalThis.chrome.storage.write("autistmask", { [setting]: after });
release();
return result;
}
// Each request, the setting that gates it, how to make it, and its answer
// while that setting is off.
test.each([
[
"Blockscout's token balances",
"useBlockscout",
() => fetchTokenBalances(ADDRESS, BLOCKSCOUT, []),
[],
],
[
"the transaction history",
"useBlockscout",
() => fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"),
[],
],
[
"the Etherscan label check",
"checkRecipientsOnEtherscan",
() => checkEtherscanLabel(RECIPIENT, "0x1"),
null,
],
])(
"%s keeps an answer that lands while its setting is on, and drops one that lands after it went off",
async (_what, setting, ask, dropped) => {
expect(await answeredAfter(setting, true, ask)).not.toEqual(
dropped,
);
expect(await answeredAfter(setting, false, ask)).toEqual(dropped);
},
);
describe("a balance refresh", () => {
// The RPC endpoint answers every balance with zero, and every other
// call, the ENS reverse lookup's, with no resolver.
beforeEach(() => {
FetchRequest.registerGetUrl(async (req) => {
const body = JSON.parse(Buffer.from(req.body).toString("utf8"));
const reply = (r) => ({
jsonrpc: "2.0",
id: r.id,
result: r.method === "eth_getBalance" ? "0x0" : ZERO_WORD,
});
const answer = Array.isArray(body)
? body.map(reply)
: reply(body);
return {
statusCode: 200,
statusMessage: "OK",
headers: { "content-type": "application/json" },
body: Buffer.from(JSON.stringify(answer)),
};
});
});
afterEach(() => {
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
});
// An address that was refreshed while Use Blockscout was on, and holds
// the USDC Blockscout reported then.
function addressWithUsdc() {
return {
address: ADDRESS,
tokenBalances: [
{
address: USDC_ROW.token.address_hash,
name: "USD Coin",
symbol: "USDC",
decimals: 6,
balance: "5.0",
holders: 50000,
},
],
};
}
async function refresh(addr, useBlockscout) {
storing({ useBlockscout });
await refreshBalances(
[{ addresses: [addr] }],
RPC_URL,
BLOCKSCOUT,
[],
"mainnet",
);
}
test("asks Blockscout nothing while Use Blockscout is off, and then lists no token the user has not tracked", async () => {
const addr = addressWithUsdc();
await refresh(addr, false);
expect(asked).toEqual([]);
expect(addr.tokenBalances).toEqual([]);
});
test("asks Blockscout for the token balances while Use Blockscout is on", async () => {
const addr = addressWithUsdc();
await refresh(addr, true);
expect(asked).toEqual([
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
]);
expect(addr.tokenBalances.map((row) => row.symbol)).toEqual([
"USDC",
]);
});
});
});
describe("the popup", () => {
// Every URL the booted page handed to fetch().
function fetched() {
return globalThis.fetch.mock.calls.map((call) => String(call[0]));
}
function blockscoutOrEtherscan(url) {
return url.includes("blockscout") || url.includes("etherscan");
}
beforeEach(() => {
// The screens below log their failed RPC reads; the stub node in
// popupBoot.js answers none.
jest.spyOn(console, "error").mockImplementation(() => {});
});
test("Home says why there is no history and only tracked tokens while Use Blockscout is off", async () => {
const page = await bootPopup(unversionedValidProfile());
expect(page.node("home-tx-list").innerHTML).toContain(
escapeHtml(HISTORY_OFF_TEXT),
);
expect(page.hidden("home-tokens-note")).toBe(false);
cleanupPopup();
const on = await bootPopup(
unversionedValidProfile({ useBlockscout: true }),
);
expect(on.node("home-tx-list").innerHTML).toContain(
"No transactions found.",
);
expect(on.hidden("home-tokens-note")).toBe(true);
});
// The transaction screen, as a reopened popup draws it.
function onTransactionScreen(extra) {
return unversionedValidProfile({
currentView: "transaction",
viewStack: ["main"],
selectedWallet: 0,
selectedAddress: 0,
viewData: {
tx: {
hash: TX_HASH,
from: ADDRESS,
to: RECIPIENT,
value: "1.0000",
exactValue: "1.0",
symbol: "ETH",
timestamp: 1790000000,
isError: false,
direction: "sent",
directionLabel: "Sent",
contractAddress: null,
chainId: "0x1",
},
},
...extra,
});
}
test("TransactionDetail asks Blockscout for the transaction only while Use Blockscout is on", async () => {
const page = await bootPopup(onTransactionScreen());
expect(page.visibleViews()).toEqual(["transaction"]);
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]);
expect(page.hidden("tx-detail-blockscout-note")).toBe(false);
cleanupPopup();
const on = await bootPopup(
onTransactionScreen({ useBlockscout: true }),
);
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([
BLOCKSCOUT + "/transactions/" + TX_HASH,
]);
expect(on.hidden("tx-detail-blockscout-note")).toBe(true);
});
// Another page turns "Use Blockscout" off while TransactionDetail's request
// to Blockscout is out. This page draws TransactionDetail again with its
// line, and only then does Blockscout answer.
test("TransactionDetail shows nothing from an answer that lands after another page turned Use Blockscout off", async () => {
const page = await bootPopup(
onTransactionScreen({ useBlockscout: true }),
);
let release;
const held = new Promise((resolve) => {
release = resolve;
});
globalThis.fetch = jest.fn(async () => {
await held;
return {
ok: true,
status: 200,
json: async () => ({
block_number: 21000000,
fee: { value: "21000000000000" },
raw_input: "0x12345678",
method: "doSomething",
}),
};
});
require("../src/popup/views/transactionDetail").render();
await page.settle();
expect(fetched()).toEqual([BLOCKSCOUT + "/transactions/" + TX_HASH]);
await anotherPageAnnounces(page, { useBlockscout: false });
release();
await page.settle();
expect(page.visibleViews()).toEqual(["transaction"]);
expect(page.hidden("tx-detail-blockscout-note")).toBe(false);
for (const id of [
"tx-detail-block-section",
"tx-detail-network-section",
"tx-detail-calldata-section",
"tx-detail-rawdata-section",
]) {
expect(page.hidden(id)).toBe(true);
}
expect(fetched()).toHaveLength(1);
});
// The confirmation screen for a send of 0.1 ETH to RECIPIENT, as a
// reopened popup draws it.
function onConfirmScreen(extra) {
return unversionedValidProfile({
currentView: "confirm-tx",
viewStack: ["main"],
selectedWallet: 0,
selectedAddress: 0,
viewData: {
pendingTx: {
token: "ETH",
from: ADDRESS,
to: RECIPIENT,
amount: "0.1",
chainId: "0x1",
},
},
...extra,
});
}
test("ConfirmTx asks Etherscan about the recipient only while Check recipients on Etherscan is on", async () => {
const page = await bootPopup(onConfirmScreen());
expect(page.visibleViews()).toEqual(["confirm-tx"]);
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]);
expect(page.hidden("confirm-etherscan-note")).toBe(false);
expect(page.hidden("confirm-etherscan-warning")).toBe(true);
cleanupPopup();
const on = await bootPopup(
onConfirmScreen({ checkRecipientsOnEtherscan: true }),
);
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([
"https://etherscan.io/address/" + RECIPIENT,
]);
expect(on.hidden("confirm-etherscan-note")).toBe(true);
expect(on.hidden("confirm-etherscan-warning")).toBe(false);
});
// Ticks or unticks a checkbox on the page, as the user does.
async function setBox(page, id, checked) {
const box = page.node(id);
box.checked = checked;
for (const fn of box.listeners.change) await fn();
await page.settle();
}
test("Welcome offers both settings unticked, and ticking one there stores it", async () => {
const page = await bootPopup(undefined);
expect(page.visibleViews()).toEqual(["welcome"]);
expect(page.node("welcome-use-blockscout").checked).toBe(false);
expect(page.node("welcome-check-recipients-on-etherscan").checked).toBe(
false,
);
await setBox(page, "welcome-use-blockscout", true);
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
await setBox(page, "welcome-check-recipients-on-etherscan", true);
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
true,
);
});
test("Settings shows both settings unticked, and ticking one there stores it", async () => {
const page = await bootPopup(unversionedValidProfile());
await page.click("btn-settings");
expect(page.node("settings-use-blockscout").checked).toBe(false);
expect(
page.node("settings-check-recipients-on-etherscan").checked,
).toBe(false);
await setBox(page, "settings-use-blockscout", true);
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
await setBox(page, "settings-check-recipients-on-etherscan", true);
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
true,
);
});
// A token the user tracks, and one Blockscout reported that the user has
// not tracked. Both are written in lower case, as the balance rows' and
// the dropdown's token ids are compared below.
const TRACKED = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const UNTRACKED = TOKEN_ADDRESS.toLowerCase();
const SECOND_ADDRESS = "0x3333333333333333333333333333333333333333";
function tokenRow(address, symbol) {
return {
address,
name: symbol,
symbol,
decimals: 18,
balance: "5.0",
holders: 50000,
};
}
// AddressDetail of the first of two addresses, as a reopened popup draws
// it. Both addresses hold the tracked token. While "Use Blockscout" is on,
// both hold the untracked one too.
function onAddressDetail(useBlockscout) {
const profile = unversionedValidProfile({
useBlockscout,
currentView: "address",
viewStack: ["main"],
selectedWallet: 0,
selectedAddress: 0,
trackedTokens: [
{ address: TRACKED, name: "TRK", symbol: "TRK", decimals: 18 },
],
});
const wallet = profile.wallets[0];
wallet.nextIndex = 2;
wallet.addresses.push({ address: SECOND_ADDRESS, balance: "0.0" });
for (const addr of wallet.addresses) {
addr.tokenBalances = useBlockscout
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
: [tokenRow(TRACKED, "TRK")];
}
return profile;
}
// The token ids of the options created after the first `from` elements.
// The page stub keeps no children, so this is where the options Send's
// dropdown was filled with are read.
function optionsSince(page, from) {
return page.document.created
.slice(from)
.filter((el) => el.tagName === "OPTION")
.map((el) => el.value.toLowerCase());
}
test("unticking Use Blockscout from AddressDetail's gear takes the untracked token off AddressDetail, Send's dropdown and Home on Back", async () => {
const page = await bootPopup(onAddressDetail(true));
expect(page.visibleViews()).toEqual(["address"]);
expect(page.node("address-balances").innerHTML).toContain(UNTRACKED);
await page.click("btn-settings");
await setBox(page, "settings-use-blockscout", false);
const from = page.document.created.length;
await page.click("btn-settings-back");
expect(page.visibleViews()).toEqual(["address"]);
const balances = page.node("address-balances").innerHTML;
expect(balances).toContain(TRACKED);
expect(balances).not.toContain(UNTRACKED);
expect(page.hidden("address-tokens-note")).toBe(false);
expect(optionsSince(page, from)).toEqual([TRACKED]);
await page.click("btn-send");
expect(page.visibleViews()).toEqual(["send"]);
expect(page.hidden("send-token-dropdown")).toBe(false);
expect(page.hidden("send-tokens-note")).toBe(false);
await page.click("btn-send-back");
await page.click("btn-address-back");
expect(page.visibleViews()).toEqual(["main"]);
const home = page.node("wallet-list").innerHTML;
expect(home).toContain(TRACKED);
expect(home).not.toContain(UNTRACKED);
const stored = page.storage.read("autistmask");
for (const addr of stored.wallets[0].addresses) {
expect(
addr.tokenBalances.map((row) => row.address.toLowerCase()),
).toEqual([TRACKED]);
}
});
// Send's dropdown was filled under the old setting, and Back cannot draw
// Send again. So Back lands on the screen under it.
test.each([
["unticking", true],
["ticking", false],
])(
"%s Use Blockscout from Send's gear lands Back on AddressDetail",
async (_how, before) => {
const page = await bootPopup(onAddressDetail(before));
await page.click("btn-send");
expect(page.visibleViews()).toEqual(["send"]);
await page.click("btn-settings");
await setBox(page, "settings-use-blockscout", !before);
await page.click("btn-settings-back");
expect(page.visibleViews()).toEqual(["address"]);
expect(page.hidden("address-tokens-note")).toBe(!before);
},
);
test("Send says under its token dropdown that only tracked tokens are listed while Use Blockscout is off", async () => {
const off = await bootPopup(onAddressDetail(false));
await off.click("btn-send");
expect(off.hidden("send-token-dropdown")).toBe(false);
expect(off.hidden("send-tokens-note")).toBe(false);
cleanupPopup();
const on = await bootPopup(onAddressDetail(true));
await on.click("btn-send");
expect(on.hidden("send-token-dropdown")).toBe(false);
expect(on.hidden("send-tokens-note")).toBe(true);
});
test("Send opened from AddressToken shows neither the dropdown nor its line", async () => {
const page = await bootPopup({
...onAddressDetail(false),
currentView: "address-token",
viewStack: ["main", "address"],
selectedToken: "ETH",
});
expect(page.visibleViews()).toEqual(["address-token"]);
await page.click("btn-address-token-send");
expect(page.visibleViews()).toEqual(["send"]);
expect(page.hidden("send-token-dropdown")).toBe(true);
expect(page.hidden("send-token-static")).toBe(false);
});
test("DeleteAddress says only tracked tokens are listed while Use Blockscout is off", async () => {
for (const useBlockscout of [false, true]) {
const page = await bootPopup(onAddressDetail(useBlockscout));
require("../src/popup/views/deleteAddress").show(0, 1);
expect(page.visibleViews()).toEqual(["delete-address-confirm"]);
expect(page.hidden("delete-address-tokens-note")).toBe(
useBlockscout,
);
cleanupPopup();
}
});
// The token ids of the first address's balances in the stored profile.
function storedTokens(page) {
return page.storage
.read("autistmask")
.wallets[0].addresses[0].tokenBalances.map((row) =>
row.address.toLowerCase(),
);
}
// Turning the setting off dropped the untracked token. A refresh that
// started while it was on still saves its balances when it returns, but
// not that token. The same refresh in the background is in
// tests/backgroundStateIsolation.test.js.
test("a refresh that returns after Use Blockscout was turned off saves its balances without the untracked token", async () => {
const page = await bootPopup(onAddressDetail(true));
const { refreshBalances: held } = require("../src/shared/balances");
let answer;
held.mockImplementationOnce(async (wallets) => {
await new Promise((resolve) => {
answer = resolve;
});
wallets[0].addresses[0].balance = "7.0";
wallets[0].addresses[0].tokenBalances = [
tokenRow(TRACKED, "TRK"),
tokenRow(UNTRACKED, "TST"),
];
});
const ticking = page.tick();
await page.settle();
await page.click("btn-settings");
await setBox(page, "settings-use-blockscout", false);
answer();
await ticking;
expect(
page.storage.read("autistmask").wallets[0].addresses[0].balance,
).toBe("7.0");
expect(storedTokens(page)).toEqual([TRACKED]);
await page.click("btn-settings-back");
expect(page.node("address-balances").innerHTML).not.toContain(
UNTRACKED,
);
});
// Another page saves `change` over the stored profile. The browser is not
// made to tell this page, so this page sees the change only by reading the
// stored profile.
function anotherPageSaves(page, change) {
page.storage.write("autistmask", {
...page.storage.read("autistmask"),
...change,
});
}
test.each([
["off", true, false],
["on", false, true],
])(
"after another page turns Use Blockscout %s, this page takes the change and its next refresh is saved",
async (_how, before, after) => {
const page = await bootPopup(onAddressDetail(before));
// The refresh answers as it does under the stored setting.
const { refreshBalances: held } = require("../src/shared/balances");
held.mockImplementationOnce(async (wallets) => {
const addr = wallets[0].addresses[0];
addr.balance = "7.0";
addr.tokenBalances = after
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
: [tokenRow(TRACKED, "TRK")];
});
anotherPageSaves(page, { useBlockscout: after });
await page.tick();
expect(
page.storage.read("autistmask").wallets[0].addresses[0].balance,
).toBe("7.0");
expect(storedTokens(page)).toEqual(
after ? [TRACKED, UNTRACKED] : [TRACKED],
);
expect(page.hidden("address-tokens-note")).toBe(after);
},
);
test.each([
["off", true, false],
["on", false, true],
])(
"after another page turns Check recipients on Etherscan %s, this page's next Etherscan check follows the stored setting",
async (_how, before, after) => {
const page = await bootPopup(
onConfirmScreen({ checkRecipientsOnEtherscan: before }),
);
anotherPageSaves(page, { checkRecipientsOnEtherscan: after });
const from = fetched().length;
require("../src/popup/views/confirmTx").show({
token: "ETH",
from: ADDRESS,
to: RECIPIENT,
amount: "0.1",
chainId: "0x1",
});
await page.settle();
// The page also takes the change and draws ConfirmTx again, which
// checks once more. So each URL asked for is counted once.
const asked = new Set(
fetched().slice(from).filter(blockscoutOrEtherscan),
);
expect([...asked]).toEqual(
after ? ["https://etherscan.io/address/" + RECIPIENT] : [],
);
},
);
// Another page's save of the whole profile with `change`, which the
// browser announces to this page as it announces any write.
async function anotherPageAnnounces(page, change) {
await page.storage.local.set({
autistmask: { ...page.storage.read("autistmask"), ...change },
});
await page.settle();
}
test("a page open on AddressDetail draws it again when another page turns Use Blockscout off", async () => {
const page = await bootPopup(onAddressDetail(true));
expect(page.hidden("address-tokens-note")).toBe(true);
await anotherPageAnnounces(page, { useBlockscout: false });
expect(page.visibleViews()).toEqual(["address"]);
expect(page.hidden("address-tokens-note")).toBe(false);
expect(page.node("address-balances").innerHTML).not.toContain(
UNTRACKED,
);
expect(storedTokens(page)).toEqual([TRACKED]);
});
test("a page open on ConfirmTx draws it again when another page turns Check recipients on Etherscan off", async () => {
const page = await bootPopup(
onConfirmScreen({ checkRecipientsOnEtherscan: true }),
);
expect(page.hidden("confirm-etherscan-note")).toBe(true);
await anotherPageAnnounces(page, { checkRecipientsOnEtherscan: false });
expect(page.visibleViews()).toEqual(["confirm-tx"]);
expect(page.hidden("confirm-etherscan-note")).toBe(false);
});
});