diff --git a/README.md b/README.md
index c2c7bcc..614f3d4 100644
--- a/README.md
+++ b/README.md
@@ -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:
diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js
index 671edbf..fc02656 100644
--- a/src/popup/views/addressDetail.js
+++ b/src/popup/views/addressDetail.js
@@ -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;
diff --git a/src/popup/views/addressToken.js b/src/popup/views/addressToken.js
index 42234a9..1ee9a75 100644
--- a/src/popup/views/addressToken.js
+++ b/src/popup/views/addressToken.js
@@ -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
) {
diff --git a/src/popup/views/home.js b/src/popup/views/home.js
index d9fea96..93680fe 100644
--- a/src/popup/views/home.js
+++ b/src/popup/views/home.js
@@ -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 =
+ '
Loading...
';
+ }
+ }
+
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;
}
diff --git a/tests/historyAfterNetworkSwitch.test.js b/tests/historyAfterNetworkSwitch.test.js
index 8fc11c7..94efba0 100644
--- a/tests/historyAfterNetworkSwitch.test.js
+++ b/tests/historyAfterNetworkSwitch.test.js
@@ -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) => {
diff --git a/tests/popupFollowsNetworkSwitch.test.js b/tests/popupFollowsNetworkSwitch.test.js
index a82bb39..bf1b293 100644
--- a/tests/popupFollowsNetworkSwitch.test.js
+++ b/tests/popupFollowsNetworkSwitch.test.js
@@ -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();