fix: Home's list drops the previous network's rows after a network switch (closes #718)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

A network switch left the previous network's rows or failure message in Home's
list until the new network's answer landed. Home now replaces them with
"Loading..." when it first draws on a new network. A redraw on the same network
keeps its rows.

Home, AddressDetail and AddressToken compared the page's network before reading
storage. A switch made in Settings could change the page during that read, and
the answer was still drawn. They now compare after the read, for an answer and
for a failure.

Model: opus-5-5
This commit is contained in:
2026-11-03 07:48:49 +00:00
parent 4531e8bce5
commit dd091a6e1d
6 changed files with 192 additions and 14 deletions
+2 -1
View File
@@ -558,7 +558,8 @@ three lines:
A contract creation has no counterparty. Its last line reads "This transaction
creates a new contract. It has no recipient." While "Use Blockscout" is off, the
list shows only the line "No transaction history is shown, because "Use
Blockscout" is off in Settings."
Blockscout" is off in Settings." After a network switch, the list shows
"Loading..." in place of the previous network's rows or failure message.
Transitions:
+5 -4
View File
@@ -146,11 +146,12 @@ async function loadTransactions(address) {
}
// If the network changed during the lookups above, or the screen now
// shows another address, nothing is saved or drawn. See
// src/shared/storedSettings.js.
// shows another address, nothing is saved or drawn. The page's network
// is compared after the read of storage, because a switch made on this
// page can land during that read. See src/shared/storedSettings.js.
if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId)) ||
state.networkId !== networkId ||
$("address-line").dataset.full !== address
) {
return;
@@ -187,8 +188,8 @@ async function loadTransactions(address) {
// If the network changed, or the screen now shows another address,
// the failure is not shown.
if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId)) ||
state.networkId !== networkId ||
$("address-line").dataset.full !== address
) {
return;
+5 -3
View File
@@ -241,11 +241,13 @@ async function loadTransactions(address, tokenId) {
}
// If the network changed during the lookups above, or the screen now
// shows another address or token, nothing is saved or drawn. See
// shows another address or token, nothing is saved or drawn. The page's
// network is compared after the read of storage, because a switch made
// on this page can land during that read. See
// src/shared/storedSettings.js.
if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId)) ||
state.networkId !== networkId ||
$("address-token-line").dataset.full !== address ||
state.selectedToken !== tokenId
) {
@@ -283,8 +285,8 @@ async function loadTransactions(address, tokenId) {
// If the network changed, or the screen now shows another address or
// token, the failure is not shown.
if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId)) ||
state.networkId !== networkId ||
$("address-token-line").dataset.full !== address ||
state.selectedToken !== tokenId
) {
+23 -5
View File
@@ -100,6 +100,9 @@ function renderActiveAddress() {
let homeTxs = [];
// The network that `home-tx-list` holds rows or a message for.
let listNetworkId = null;
function renderHomeTxList(ctx) {
const list = $("home-tx-list");
if (!list) return;
@@ -186,6 +189,19 @@ async function loadHomeTxs(ctx) {
if (allAddresses.length === 0) return;
const networkId = state.networkId;
// After a network switch the list still holds the previous network's rows
// or failure message. They go here, because every way onto Home draws Home
// first. A redraw on the same network keeps its rows.
if (listNetworkId !== networkId) {
listNetworkId = networkId;
const list = $("home-tx-list");
if (list) {
list.innerHTML =
'<div class="text-muted text-xs py-1">Loading...</div>';
}
}
const filters = {
hideSpoofedSymbols: state.hideSpoofedSymbols,
hideLowHolderTokens: state.hideLowHolderTokens,
@@ -206,10 +222,12 @@ async function loadHomeTxs(ctx) {
const results = await Promise.all(fetches);
// If the network changed during the lookups, nothing is saved or
// drawn. See src/shared/storedSettings.js.
// drawn. The page's network is compared after the read of storage,
// because a switch made on this page can land during that read. See
// src/shared/storedSettings.js.
if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId))
!(await storedNetworkIs(networkId)) ||
state.networkId !== networkId
) {
return;
}
@@ -258,8 +276,8 @@ async function loadHomeTxs(ctx) {
log.errorf("loadHomeTxs failed:", e.message);
// If the network changed, the failure is not shown.
if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId))
!(await storedNetworkIs(networkId)) ||
state.networkId !== networkId
) {
return;
}
+103 -1
View File
@@ -1,6 +1,8 @@
// Home, AddressDetail and AddressToken drop a transaction history answer that
// lands after a network switch. A dropped answer draws no rows and saves no
// fraud contracts (https://git.eeqj.de/sneak/AutistMask/issues/657).
// fraud contracts (https://git.eeqj.de/sneak/AutistMask/issues/657). Home's list
// shows nothing of the previous network once it is drawn after a switch
// (https://git.eeqj.de/sneak/AutistMask/issues/718).
//
// Most tests hold the history request open, switch the network or what the
// screen shows, and then let the answer land. The views run against a minimal
@@ -154,6 +156,21 @@ function holdLock() {
};
}
// Every read of storage waits until the returned function is called. Each read
// then answers with what storage holds at that moment, the oldest read first.
function holdReads() {
const read = storage.local.get;
const waiting = [];
storage.local.get = (key) =>
new Promise((resolve) => {
waiting.push(() => resolve(read(key)));
});
return () => {
storage.local.get = read;
for (const answer of waiting) answer();
};
}
function makeElement(id) {
const el = {
id,
@@ -409,6 +426,91 @@ describe.each([HOME, ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
expect(drawn).toContain("tx-row");
expect(node(screen.listId).innerHTML).toBe(drawn);
});
// The user picks Sepolia in Settings, and the switch reads storage. The
// answer lands during that read, so the screen's own read of storage still
// finds Ethereum Mainnet. The switch changes the page before the screen's
// read ends. The answer is the transfer of ETH alone, so nothing is saved,
// and only a check after the screen's read can drop the answer.
test("an answer that lands during the first read of a switch made in Settings adds nothing", async () => {
const land = holdHistory(history().slice(0, 1));
screen.open();
await settle();
const before = node(screen.listId).innerHTML;
const answerReads = holdReads();
const switching = onChainSwitch("sepolia");
await settle();
land();
await settle();
answerReads();
await switching;
await settle();
expectNothingAdded(screen, before);
});
test("a request that fails during the first read of a switch made in Settings shows no message", async () => {
const fail = holdFailure();
screen.open();
await settle();
const before = node(screen.listId).innerHTML;
const answerReads = holdReads();
const switching = onChainSwitch("sepolia");
await settle();
fail();
await settle();
answerReads();
await switching;
await settle();
expect(node(screen.listId).innerHTML).toBe(before);
});
});
describe("Home's list", () => {
test.each([
{ what: "rows", hold: holdHistory, shown: "tx-row" },
{
what: "failure message",
hold: holdFailure,
shown: "Failed to load transactions.",
},
])(
"shows the previous network's $what neither before nor after the new network's answer lands",
async ({ hold, shown }) => {
const answer = hold();
HOME.open();
answer();
await settle();
expect(node(HOME.listId).innerHTML).toContain(shown);
await onChainSwitch("sepolia");
const land = holdHistory([]);
HOME.open();
await settle();
expect(node(HOME.listId).innerHTML).toContain("Loading...");
land();
await settle();
expect(node(HOME.listId).innerHTML).toContain(
"No transactions found.",
);
},
);
// Each balance refresh draws Home again, so the rows must not blink.
test("keeps its rows while a redraw on the same network waits for the answer", async () => {
fetchRecentTransactions.mockImplementationOnce(async () => history());
HOME.open();
await settle();
const drawn = node(HOME.listId).innerHTML;
holdHistory();
HOME.open();
await settle();
expect(drawn).toContain("tx-row");
expect(node(HOME.listId).innerHTML).toBe(drawn);
});
});
describe.each([ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
+54
View File
@@ -137,6 +137,60 @@ test.each([
expect(page.text("total-value")).toBe("0.0000 SepoliaETH");
});
// A request on Ethereum Mainnet fails, so Home's list holds the failure message.
// The list shows it neither before nor after Sepolia's answer lands.
test.each([
{
how: "saved by the background",
switchToSepolia: (page) => backgroundSwitch(page, "sepolia"),
},
{
how: "made in Settings",
switchToSepolia: async (page) => {
await page.click("btn-settings");
const select = page.node("settings-network");
select.value = "sepolia";
for (const fn of select.listeners.change) await fn();
await page.settle();
await page.click("btn-settings-back");
},
},
])(
"Home's list drops the previous network's message after a switch $how",
async ({ switchToSepolia }) => {
const page = await bootPopup(
unversionedValidProfile({ useBlockscout: true }),
);
const {
fetchRecentTransactions,
} = require("../src/shared/transactions");
fetchRecentTransactions.mockRejectedValueOnce(
new Error("Blockscout is down"),
);
await page.tick();
expect(page.node("home-tx-list").innerHTML).toContain(
"Failed to load transactions.",
);
let land;
fetchRecentTransactions.mockImplementationOnce(
() =>
new Promise((resolve) => {
land = () => resolve([]);
}),
);
await switchToSepolia(page);
expect(page.visibleViews()).toEqual(["main"]);
expect(page.node("home-tx-list").innerHTML).toContain("Loading...");
land();
await page.settle();
expect(page.node("home-tx-list").innerHTML).toContain(
"No transactions found.",
);
},
);
// The report of the switch's own save names the network the page already has.
test("a switch in Settings is not taken a second time when its save is reported", async () => {
const page = await bootOnAddress();