fix: a saved wait in another shape reaches SuccessTx when its receipt arrives (closes #734)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

A saved wait whose decoded record is in another shape threw when its
receipt arrived. WaitTx then stayed on screen with the counter stopped.
decodedDetailsHtml() now shows only the lines whose label and value are
text. SuccessTx shows the amount and recipient when no line can be read.

WaitTx and ErrorTx now show each allowance's Allowance token line. Long
amounts use break-words, so words stay whole. ErrorTx sets up copying
when it has no hash, so the token's address can be copied.

Model: opus-5-5
This commit was merged in pull request #739.
This commit is contained in:
2026-11-03 10:14:05 +01:00
parent 49713e1cdb
commit 448afdb5ce
5 changed files with 222 additions and 44 deletions
+158 -10
View File
@@ -617,8 +617,8 @@ describe("SuccessTx and ErrorTx for a Sepolia transaction with mainnet active",
const decoded = {
name: "Transfer",
details: [
{ label: "Token", address: TOKEN, isToken: true },
{ label: "Recipient", address: RECIPIENT },
{ label: "Token", value: TOKEN, address: TOKEN, isToken: true },
{ label: "Recipient", value: RECIPIENT, address: RECIPIENT },
],
};
mockReceiptLookup.mockResolvedValue({ blockNumber: 21000000 });
@@ -644,7 +644,9 @@ describe("SuccessTx and ErrorTx for a Sepolia transaction with mainnet active",
};
const decoded = {
name: "Transfer",
details: [{ label: "Token", address: TOKEN, isToken: true }],
details: [
{ label: "Token", value: TOKEN, address: TOKEN, isToken: true },
],
};
mockReceiptLookup.mockResolvedValue({ blockNumber: 21000000 });
txStatus.showWait({ ...sepoliaTx, decoded }, TX_HASH);
@@ -747,6 +749,21 @@ describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () =
{ details: [moved(7)] },
{ details: [moved({ toString: 1 })] },
{ details: [{ label: "Permit2 allowance", value: 7 }] },
{
details: [
{ label: "Permit2 allowance", value: "text", address: 7 },
],
},
{
details: [
{
label: "Allowance token",
value: "text",
address: 7,
isToken: true,
},
],
},
]) {
expect(noteOnReopen(saved)).toBe("");
}
@@ -757,11 +774,22 @@ 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 USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const allowance = (value) => ({ label: "Permit2 allowance", value });
const decoded = {
details: [
allowance("Unlimited"),
{ label: "Allowance token", value: "USDC" },
{
label: "Allowance token",
value: "USDC (" + USDC + ")",
address: USDC,
isToken: true,
},
{
label: "Allowance spender",
value: RECIPIENT,
address: RECIPIENT,
},
allowance("<b>400.0000</b> DAI"),
],
};
@@ -771,9 +799,53 @@ describe("the Permit2 allowance lines of a swap on WaitTx and ErrorTx", () => {
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");
expect(html).not.toContain("Allowance spender");
expect(html).not.toContain(RECIPIENT);
}
// `Unlimited` alone does not say which token it covers. The address gets
// the row of its own that every address gets, in the transaction's network.
function expectTokenLine(id) {
const html = getElement(id).innerHTML;
expect(html.indexOf("Unlimited")).toBeLessThan(
html.indexOf("Allowance token"),
);
expect(html).toContain('<div class="font-bold">USDC</div>');
expect(html).toContain(`data-copy="${USDC}"`);
expect(html).toContain(`href="https://etherscan.io/address/${USDC}"`);
expect(html).toMatch(/<div class="text-xs">.*class="am-address"/);
}
test("WaitTx says which token an allowance covers", () => {
mockReceiptLookup.mockResolvedValue(null);
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
expectTokenLine("wait-tx-token-note");
});
test("ErrorTx says which token an allowance covers", () => {
txStatus.showError({ ...TX_INFO, decoded }, TX_HASH, "It failed.");
expectTokenLine("error-tx-token-note");
});
test("the token's address links to the transaction's network", () => {
const sepoliaTx = {
...TX_INFO,
chainId: NETWORKS.sepolia.chainId,
decoded,
};
txStatus.showError(sepoliaTx, TX_HASH, "It failed.");
expect(getElement("error-tx-token-note").innerHTML).toContain(
`href="https://sepolia.etherscan.io/address/${USDC}"`,
);
});
test("the token's address can be copied on an ErrorTx with no hash", () => {
const view = getElement("view-error-tx");
view.querySelectorAll = jest.fn(() => []);
txStatus.showError({ ...TX_INFO, decoded }, null, "It failed.");
expect(view.querySelectorAll).toHaveBeenCalledWith("[data-copy]");
});
test("WaitTx shows each line's amount, escaped", () => {
mockReceiptLookup.mockResolvedValue(null);
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
@@ -795,7 +867,8 @@ describe("the Permit2 allowance lines of a swap on WaitTx and ErrorTx", () => {
});
// 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.
// break-words class its last digits are cut off at the edge. That class keeps
// the words of a line such as `(decimals unknown)` whole.
describe("a long amount on WaitTx and ErrorTx", () => {
test("each amount under the Amount figure wraps", () => {
const decoded = {
@@ -806,8 +879,10 @@ describe("a long amount on WaitTx and ErrorTx", () => {
};
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<');
expect(html).toContain(
'<div class="font-bold break-words">1.0000 ETH<',
);
expect(html).toContain('<div class="font-bold break-words">Unlimited<');
});
test("the Amount figure wraps", () => {
@@ -816,14 +891,87 @@ describe("a long amount on WaitTx and ErrorTx", () => {
"utf8",
);
expect(html).toMatch(
/id="wait-tx-summary"\s+class="font-bold break-all"/,
/id="wait-tx-summary"\s+class="font-bold break-words"/,
);
expect(html).toMatch(
/id="error-tx-summary"\s+class="font-bold break-all"/,
/id="error-tx-summary"\s+class="font-bold break-words"/,
);
});
});
// A saved wait comes back from storage, in whatever shape it is in. A throw when
// its receipt arrives would leave the popup on WaitTx with the counter stopped.
describe("a saved wait in another shape when its receipt arrives", () => {
const LINE = { label: "Steps", value: "V3_SWAP_EXACT_IN" };
async function receiptForSavedWait(decoded) {
mockReceiptLookup.mockResolvedValue({ blockNumber: 21000000 });
state.viewData = {
pendingWait: {
hash: TX_HASH,
txInfo: { ...TX_INFO, decoded },
broadcastTime: Date.now(),
},
};
expect(txStatus.restoreWait()).toBe(true);
await jest.advanceTimersByTimeAsync(0);
}
test.each([
{ details: 7 },
{ details: "text" },
{ details: { label: "Steps", value: "text" } },
{ details: [null] },
{ details: [7] },
{ details: ["text"] },
{ details: [{ value: "text" }] },
{ details: [{ label: 7, value: "text" }] },
{ details: [{ label: { toString: 1 }, value: "text" }] },
{ details: [{ label: "Steps" }] },
{ details: [{ label: "Steps", value: 7 }] },
{ details: [{ label: "Steps", value: { toString: 1 } }] },
{ details: [{ label: "Steps", address: RECIPIENT }] },
{ details: [{ label: "Steps", value: "text", address: 7 }] },
{
details: [
{ label: "Steps", value: "text", address: 7, isToken: true },
],
},
{ name: "Swap", description: "A swap", details: [null] },
])("%j shows SuccessTx with the amount and recipient", async (decoded) => {
await receiptForSavedWait(decoded);
expect(state.currentView).toBe("success-tx");
expect(
getElement("success-tx-decoded").classList.contains("hidden"),
).toBe(true);
expect(
getElement("success-tx-summary-parent").classList.contains(
"hidden",
),
).toBe(false);
expect(getElement("success-tx-summary").textContent).toBe("0.0050 ETH");
expect(
getElement("success-tx-to-parent").classList.contains("hidden"),
).toBe(false);
expect(getElement("success-tx-to").innerHTML).toContain(RECIPIENT);
});
test.each([
{ details: [null, LINE, 7] },
{ name: 7, details: [LINE] },
{ name: { toString: 1 }, details: [LINE] },
{ description: 7, details: [LINE] },
{ description: { toString: 1 }, details: [LINE] },
])("%j shows SuccessTx with the line it can read", async (decoded) => {
await receiptForSavedWait(decoded);
expect(state.currentView).toBe("success-tx");
const html = getElement("success-tx-decoded").innerHTML;
expect(html).toContain("V3_SWAP_EXACT_IN");
expect(html).not.toContain("Action");
expect(html).not.toContain("Description");
});
});
describe("wait-tx is a view the popup may reopen onto", () => {
// The resume feature is wired through RESTORABLE_VIEWS: restoreView()
// refuses any view not in the set, so dropping "wait-tx" from it kills