fix: Home's list drops the previous network's rows after a network switch (closes #718)
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:
@@ -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:
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user