2 Commits
Author SHA1 Message Date
sneak e44d9b3cd8 fix: Home's list drops the previous network's rows after a network switch (closes #718)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
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
2026-11-03 08:31:26 +00:00
clawbot 1ea0730a81 fix: the wait and error screens show a swap's Permit2 allowance amounts (closes #733)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
A swap can give another spender an allowance that its Amount figure leaves
out, and these two screens did not show it. Both now show each Permit2
allowance amount under the figure, with the Moved from the sender amounts.
movedFromSenderHtml() is now movedOrAllowedHtml(), as it shows both.

An amount too long for one line now wraps on these screens, rather than losing
its last digits at the screen's edge.

Model: opus-5-5
2026-11-03 09:14:23 +01:00
10 changed files with 391 additions and 27 deletions
+4 -1
View File
@@ -558,7 +558,8 @@ three lines:
A contract creation has no counterparty. Its last line reads "This transaction 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 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 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: Transitions:
@@ -787,6 +788,7 @@ Shown after a broadcast. The screen holds:
- the heading "Transaction Broadcast", with no "Back" button; - the heading "Transaction Broadcast", with no "Back" button;
- the amount and symbol; - the amount and symbol;
- each `Moved from the sender` amount of a swap; - each `Moved from the sender` amount of a swap;
- each `Permit2 allowance` amount of a swap;
- the recipient; - the recipient;
- the transaction hash, with an explorer link; - the transaction hash, with an explorer link;
- the counter "Waiting for confirmation... Ns". - the counter "Waiting for confirmation... Ns".
@@ -826,6 +828,7 @@ The screen holds:
- the heading "Transaction Failed"; - the heading "Transaction Failed";
- the amount; - the amount;
- each `Moved from the sender` amount of a swap; - each `Moved from the sender` amount of a swap;
- each `Permit2 allowance` amount of a swap;
- the recipient; - the recipient;
- the error message, in a dashed box; - the error message, in a dashed box;
- the transaction hash, when one exists; - the transaction hash, when one exists;
+5 -2
View File
@@ -810,7 +810,7 @@
<h2 class="font-bold mb-2">Transaction Broadcast</h2> <h2 class="font-bold mb-2">Transaction Broadcast</h2>
<div class="mb-3"> <div class="mb-3">
<div class="text-xs text-muted mb-1">Amount</div> <div class="text-xs text-muted mb-1">Amount</div>
<div id="wait-tx-summary" class="font-bold"></div> <div id="wait-tx-summary" class="font-bold break-all"></div>
<div id="wait-tx-token-note"></div> <div id="wait-tx-token-note"></div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -857,7 +857,10 @@
<h2 class="font-bold mb-2">Transaction Failed</h2> <h2 class="font-bold mb-2">Transaction Failed</h2>
<div class="mb-3"> <div class="mb-3">
<div class="text-xs text-muted mb-1">Amount</div> <div class="text-xs text-muted mb-1">Amount</div>
<div id="error-tx-summary" class="font-bold"></div> <div
id="error-tx-summary"
class="font-bold break-all"
></div>
<div id="error-tx-token-note"></div> <div id="error-tx-token-note"></div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
+5 -4
View File
@@ -146,11 +146,12 @@ async function loadTransactions(address) {
} }
// If the network changed during the lookups above, or the screen now // If the network changed during the lookups above, or the screen now
// shows another address, nothing is saved or drawn. See // shows another address, nothing is saved or drawn. The page's network
// src/shared/storedSettings.js. // 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 ( if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId)) || !(await storedNetworkIs(networkId)) ||
state.networkId !== networkId ||
$("address-line").dataset.full !== address $("address-line").dataset.full !== address
) { ) {
return; return;
@@ -187,8 +188,8 @@ async function loadTransactions(address) {
// If the network changed, or the screen now shows another address, // If the network changed, or the screen now shows another address,
// the failure is not shown. // the failure is not shown.
if ( if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId)) || !(await storedNetworkIs(networkId)) ||
state.networkId !== networkId ||
$("address-line").dataset.full !== address $("address-line").dataset.full !== address
) { ) {
return; 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 // 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. // src/shared/storedSettings.js.
if ( if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId)) || !(await storedNetworkIs(networkId)) ||
state.networkId !== networkId ||
$("address-token-line").dataset.full !== address || $("address-token-line").dataset.full !== address ||
state.selectedToken !== tokenId state.selectedToken !== tokenId
) { ) {
@@ -283,8 +285,8 @@ async function loadTransactions(address, tokenId) {
// If the network changed, or the screen now shows another address or // If the network changed, or the screen now shows another address or
// token, the failure is not shown. // token, the failure is not shown.
if ( if (
state.networkId !== networkId ||
!(await storedNetworkIs(networkId)) || !(await storedNetworkIs(networkId)) ||
state.networkId !== networkId ||
$("address-token-line").dataset.full !== address || $("address-token-line").dataset.full !== address ||
state.selectedToken !== tokenId state.selectedToken !== tokenId
) { ) {
+25 -5
View File
@@ -100,6 +100,9 @@ function renderActiveAddress() {
let homeTxs = []; let homeTxs = [];
// The network that `home-tx-list` holds rows or a message for.
let listNetworkId = null;
function renderHomeTxList(ctx) { function renderHomeTxList(ctx) {
const list = $("home-tx-list"); const list = $("home-tx-list");
if (!list) return; if (!list) return;
@@ -186,6 +189,19 @@ async function loadHomeTxs(ctx) {
if (allAddresses.length === 0) return; if (allAddresses.length === 0) return;
const networkId = state.networkId; 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 = { const filters = {
hideSpoofedSymbols: state.hideSpoofedSymbols, hideSpoofedSymbols: state.hideSpoofedSymbols,
hideLowHolderTokens: state.hideLowHolderTokens, hideLowHolderTokens: state.hideLowHolderTokens,
@@ -206,10 +222,12 @@ async function loadHomeTxs(ctx) {
const results = await Promise.all(fetches); const results = await Promise.all(fetches);
// If the network changed during the lookups, nothing is saved or // 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 ( if (
state.networkId !== networkId || !(await storedNetworkIs(networkId)) ||
!(await storedNetworkIs(networkId)) state.networkId !== networkId
) { ) {
return; return;
} }
@@ -250,6 +268,7 @@ async function loadHomeTxs(ctx) {
merged = filtered.transactions; merged = filtered.transactions;
merged.sort((a, b) => b.blockNumber - a.blockNumber); merged.sort((a, b) => b.blockNumber - a.blockNumber);
homeTxs = merged.slice(0, 25); homeTxs = merged.slice(0, 25);
listNetworkId = networkId;
renderHomeTxList(ctx); renderHomeTxList(ctx);
} catch (e) { } catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in // Cancelled by the popup closing, not failed: see pageClosed in
@@ -258,11 +277,12 @@ async function loadHomeTxs(ctx) {
log.errorf("loadHomeTxs failed:", e.message); log.errorf("loadHomeTxs failed:", e.message);
// If the network changed, the failure is not shown. // If the network changed, the failure is not shown.
if ( if (
state.networkId !== networkId || !(await storedNetworkIs(networkId)) ||
!(await storedNetworkIs(networkId)) state.networkId !== networkId
) { ) {
return; return;
} }
listNetworkId = networkId;
const list = $("home-tx-list"); const list = $("home-tx-list");
if (list) { if (list) {
list.innerHTML = list.innerHTML =
+12 -11
View File
@@ -73,24 +73,25 @@ function toAddressHtml(address, chainId) {
return renderAddressHtml(address, { title, chainId }); return renderAddressHtml(address, { title, chainId });
} }
// The amount of each `Moved from the sender` line of a decoded swap, for the // The amount of each `Moved from the sender` and `Permit2 allowance` line of a
// wait and error screens to show under the Amount figure. That figure can leave // decoded swap, for the wait and error screens to show under the Amount figure.
// these amounts out, so alone it can read as less than can have left the // That figure can leave these amounts out, so alone it can understate what can
// wallet. `decoded` can come back from storage in any shape, so this shows only // leave the wallet. `decoded` can come back from storage in any shape, so this
// the lines whose value is text. // shows only the lines whose value is text.
function movedFromSenderHtml(decoded) { function movedOrAllowedHtml(decoded) {
if (!decoded || !Array.isArray(decoded.details)) return ""; if (!decoded || !Array.isArray(decoded.details)) return "";
let html = ""; let html = "";
for (const d of decoded.details) { for (const d of decoded.details) {
if ( if (
!d || !d ||
d.label !== "Moved from the sender" || (d.label !== "Moved from the sender" &&
d.label !== "Permit2 allowance") ||
typeof d.value !== "string" typeof d.value !== "string"
) )
continue; continue;
html += `<div class="mt-2">`; html += `<div class="mt-2">`;
html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`; html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
html += `<div class="font-bold">${escapeHtml(d.value)}</div>`; html += `<div class="font-bold break-all">${escapeHtml(d.value)}</div>`;
html += `</div>`; html += `</div>`;
} }
return html; return html;
@@ -134,7 +135,7 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
txInfo.amount + (symbol ? " " + symbol : ""); txInfo.amount + (symbol ? " " + symbol : "");
$("wait-tx-token-note").innerHTML = $("wait-tx-token-note").innerHTML =
blockscoutOffTokenNoteHtml(txInfo.token, txInfo.chainId) + blockscoutOffTokenNoteHtml(txInfo.token, txInfo.chainId) +
movedFromSenderHtml(txInfo.decoded); movedOrAllowedHtml(txInfo.decoded);
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId); $("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId);
$("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId); $("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId);
attachCopyHandlers("view-wait-tx"); attachCopyHandlers("view-wait-tx");
@@ -245,7 +246,7 @@ function showWait(txInfo, txHash) {
// to name a network. txInfo.token and txInfo.tokenSymbol are not checked here. // to name a network. txInfo.token and txInfo.tokenSymbol are not checked here.
// blockscoutOffTokenNoteHtml() shows no line for a token that is not text. // blockscoutOffTokenNoteHtml() shows no line for a token that is not text.
// Everything else only compares and coalesces them, and tokenSymbol is null for // Everything else only compares and coalesces them, and tokenSymbol is null for
// ETH. txInfo.decoded is not checked either: see movedFromSenderHtml(). // ETH. txInfo.decoded is not checked either: see movedOrAllowedHtml().
function restoreWait() { function restoreWait() {
const d = state.viewData; const d = state.viewData;
if (!d || !d.pendingWait) return false; if (!d || !d.pendingWait) return false;
@@ -391,7 +392,7 @@ function renderError() {
d.amount + (d.symbol ? " " + d.symbol : ""); d.amount + (d.symbol ? " " + d.symbol : "");
$("error-tx-token-note").innerHTML = $("error-tx-token-note").innerHTML =
blockscoutOffTokenNoteHtml(d.token, d.chainId) + blockscoutOffTokenNoteHtml(d.token, d.chainId) +
movedFromSenderHtml(d.decoded); movedOrAllowedHtml(d.decoded);
$("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId); $("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
$("error-tx-message").textContent = d.message; $("error-tx-message").textContent = d.message;
+135 -1
View File
@@ -1,6 +1,8 @@
// Home, AddressDetail and AddressToken drop a transaction history answer that // Home, AddressDetail and AddressToken drop a transaction history answer that
// lands after a network switch. A dropped answer draws no rows and saves no // 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 // 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 // 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) { function makeElement(id) {
const el = { const el = {
id, id,
@@ -409,6 +426,123 @@ describe.each([HOME, ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
expect(drawn).toContain("tx-row"); expect(drawn).toContain("tx-row");
expect(node(screen.listId).innerHTML).toBe(drawn); 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.",
);
},
);
// The user leaves Ethereum Mainnet with a request waiting, comes back, and
// that request lands. The list then holds Ethereum Mainnet's answer, so the
// next draw on Sepolia must replace it.
test.each([
{ what: "rows", hold: holdHistory },
{ what: "failure message", hold: holdFailure },
])(
"shows no $what of an older request that lands after a switch back",
async ({ hold }) => {
const answer = hold();
HOME.open();
await onChainSwitch("sepolia");
holdHistory();
HOME.open();
await onChainSwitch("mainnet");
answer();
await settle();
await onChainSwitch("sepolia");
holdHistory();
HOME.open();
await settle();
const list = node(HOME.listId).innerHTML;
expect(list).toContain("Loading...");
expect(list).not.toContain("tx-row");
expect(list).not.toContain("Failed to load transactions.");
},
);
// 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) => { 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"); 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. // 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 () => { test("a switch in Settings is not taken a second time when its save is reported", async () => {
const page = await bootOnAddress(); const page = await bootOnAddress();
+74
View File
@@ -412,3 +412,77 @@ describe("a swap with a Moved from the sender line of its own", () => {
expectMovedLine("error-tx-token-note"); expectMovedLine("error-tx-token-note");
}); });
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/733: a PERMIT2_PERMIT that gives
// another spender an unlimited USDC allowance, then a V4 swap of 1 ETH. The
// Amount line reads 1.0000 ETH and says nothing of the allowance.
describe("a swap that gives another spender an allowance", () => {
const OTHER_SPENDER = "0x1111111111111111111111111111111111111111";
const MAX_UINT160 = (1n << 160n) - 1n;
const SEND_FAILS = {
error: "The transaction could not be sent.",
retryable: false,
};
const data = routerIface.encodeFunctionData("execute", [
"0x0a10",
[
coder.encode(
[
"tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
"bytes",
],
[
[[USDC, MAX_UINT160, 0, 0], OTHER_SPENDER, 9999999999n],
"0x1234",
],
),
coder.encode(
["bytes", "bytes[]"],
[
new Uint8Array([SWAP_EXACT_IN_SINGLE, SETTLE, TAKE]),
[
coder.encode(
[`tuple(${POOL_KEY},bool,uint128,uint128,bytes)`],
[[ETH_USDC_POOL, true, ONE_ETH, 990000n, "0x"]],
),
settleEth,
takeUsdc,
],
],
),
],
9999999999n,
]);
function expectAllowanceLine(id) {
const html = node(id).innerHTML;
expect(html).toContain("Permit2 allowance");
expect(html).toContain("Unlimited");
}
test("the wait screen shows the allowance under the Amount figure", async () => {
await approveSwap(data, { txHash: TX_HASH }, ONE_ETH);
expect(node("wait-tx-summary").textContent).toBe("1.0000 ETH");
expectAllowanceLine("wait-tx-token-note");
});
test("the error screen shows the allowance under the Amount figure", async () => {
await approveSwap(data, SEND_FAILS, ONE_ETH);
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
expectAllowanceLine("error-tx-token-note");
});
test("a reopened popup's wait screen still shows it", async () => {
await approveSwap(data, { txHash: TX_HASH }, ONE_ETH);
node("wait-tx-token-note").innerHTML = "";
txStatus.restoreWait();
expectAllowanceLine("wait-tx-token-note");
});
test("a reopened popup's error screen still shows it", async () => {
await approveSwap(data, SEND_FAILS, ONE_ETH);
node("error-tx-token-note").innerHTML = "";
txStatus.renderError();
expectAllowanceLine("error-tx-token-note");
});
});
+72
View File
@@ -101,6 +101,9 @@ global.document = {
global.window = { location: { search: "" } }; global.window = { location: { search: "" } };
const fs = require("fs");
const path = require("path");
// Clones in both directions, as the real chrome.storage.local does; the stub // Clones in both directions, as the real chrome.storage.local does; the stub
// here used to hand back the live stored object, so an in-memory mutation // here used to hand back the live stored object, so an in-memory mutation
// looked like a write that had reached storage. See // looked like a write that had reached storage. See
@@ -743,6 +746,7 @@ describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () =
{ details: [{ label: "Moved from the sender" }] }, { details: [{ label: "Moved from the sender" }] },
{ details: [moved(7)] }, { details: [moved(7)] },
{ details: [moved({ toString: 1 })] }, { details: [moved({ toString: 1 })] },
{ details: [{ label: "Permit2 allowance", value: 7 }] },
]) { ]) {
expect(noteOnReopen(saved)).toBe(""); expect(noteOnReopen(saved)).toBe("");
} }
@@ -752,6 +756,74 @@ describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () =
}); });
}); });
describe("the Permit2 allowance lines of a swap on WaitTx and ErrorTx", () => {
const allowance = (value) => ({ label: "Permit2 allowance", value });
const decoded = {
details: [
allowance("Unlimited"),
{ label: "Allowance token", value: "USDC" },
allowance("<b>400.0000</b> DAI"),
],
};
function expectEveryLine(id) {
const html = getElement(id).innerHTML;
expect(html).toContain("Permit2 allowance");
expect(html).toContain("Unlimited");
expect(html).toContain("&lt;b&gt;400.0000&lt;/b&gt; DAI");
expect(html).not.toContain("Allowance token");
}
test("WaitTx shows each line's amount, escaped", () => {
mockReceiptLookup.mockResolvedValue(null);
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
expectEveryLine("wait-tx-token-note");
});
test("ErrorTx shows each line's amount, escaped", () => {
txStatus.showError({ ...TX_INFO, decoded }, TX_HASH, "It failed.");
expectEveryLine("error-tx-token-note");
});
test("a wait that ends in ErrorTx keeps them", async () => {
mockReceiptLookup.mockResolvedValue(null);
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
await jest.advanceTimersByTimeAsync(60000);
expect(state.currentView).toBe("error-tx");
expectEveryLine("error-tx-token-note");
});
});
// An amount of about 47 digits or more is wider than the screen. Without the
// break-all class its last digits are cut off at the edge.
describe("a long amount on WaitTx and ErrorTx", () => {
test("each amount under the Amount figure wraps", () => {
const decoded = {
details: [
{ label: "Moved from the sender", value: "1.0000 ETH" },
{ label: "Permit2 allowance", value: "Unlimited" },
],
};
txStatus.showError({ ...TX_INFO, decoded }, TX_HASH, "It failed.");
const html = getElement("error-tx-token-note").innerHTML;
expect(html).toContain('<div class="font-bold break-all">1.0000 ETH<');
expect(html).toContain('<div class="font-bold break-all">Unlimited<');
});
test("the Amount figure wraps", () => {
const html = fs.readFileSync(
path.join(__dirname, "..", "src", "popup", "index.html"),
"utf8",
);
expect(html).toMatch(
/id="wait-tx-summary"\s+class="font-bold break-all"/,
);
expect(html).toMatch(
/id="error-tx-summary"\s+class="font-bold break-all"/,
);
});
});
describe("wait-tx is a view the popup may reopen onto", () => { describe("wait-tx is a view the popup may reopen onto", () => {
// The resume feature is wired through RESTORABLE_VIEWS: restoreView() // The resume feature is wired through RESTORABLE_VIEWS: restoreView()
// refuses any view not in the set, so dropping "wait-tx" from it kills // refuses any view not in the set, so dropping "wait-tx" from it kills