fix: long symbols wrap on TxApproval and TransactionDetail, and SuccessTx keeps its amount and recipient (closes #743) #744
@@ -812,7 +812,9 @@ For a swap whose amount could not be decoded, the amount reads
|
|||||||
Shown when the receipt arrives. The screen holds:
|
Shown when the receipt arrives. The screen holds:
|
||||||
|
|
||||||
- the heading "Transaction Confirmed";
|
- the heading "Transaction Confirmed";
|
||||||
- the amount and the recipient, or the decoded action when there is one;
|
- the decoded action, when there is one;
|
||||||
|
- the amount, unless a line of the decoded action shows it;
|
||||||
|
- the recipient, unless a line of the decoded action shows it;
|
||||||
- the block number, with an explorer link;
|
- the block number, with an explorer link;
|
||||||
- the transaction hash, with an explorer link;
|
- the transaction hash, with an explorer link;
|
||||||
- a "Done" button.
|
- a "Done" button.
|
||||||
|
|||||||
@@ -1646,7 +1646,7 @@
|
|||||||
<div class="text-xs text-muted mb-1">Action</div>
|
<div class="text-xs text-muted mb-1">Action</div>
|
||||||
<div
|
<div
|
||||||
id="tx-detail-calldata-action"
|
id="tx-detail-calldata-action"
|
||||||
class="text-xs font-bold mb-2"
|
class="text-xs font-bold mb-2 break-words"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
id="tx-detail-calldata-details"
|
id="tx-detail-calldata-details"
|
||||||
@@ -1721,7 +1721,7 @@
|
|||||||
<div class="text-xs text-muted mb-1">Action</div>
|
<div class="text-xs text-muted mb-1">Action</div>
|
||||||
<div
|
<div
|
||||||
id="approve-tx-action"
|
id="approve-tx-action"
|
||||||
class="text-xs font-bold mb-2"
|
class="text-xs font-bold mb-2 break-words"
|
||||||
></div>
|
></div>
|
||||||
<div id="approve-tx-decoded-details" class="text-xs"></div>
|
<div id="approve-tx-decoded-details" class="text-xs"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1736,7 +1736,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="text-xs text-muted mb-1">Value</div>
|
<div class="text-xs text-muted mb-1">Value</div>
|
||||||
<div id="approve-tx-value" class="text-xs font-bold"></div>
|
<div
|
||||||
|
id="approve-tx-value"
|
||||||
|
class="text-xs font-bold break-words"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="text-xs text-muted mb-1">Network fee (max)</div>
|
<div class="text-xs text-muted mb-1">Network fee (max)</div>
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ function showTxApproval(details) {
|
|||||||
if (toAddr) {
|
if (toAddr) {
|
||||||
let toHtml = "";
|
let toHtml = "";
|
||||||
if (symbol) {
|
if (symbol) {
|
||||||
toHtml += `<div class="font-bold mb-1">${escapeHtml(symbol)}</div>`;
|
toHtml += `<div class="font-bold mb-1 break-words">${escapeHtml(symbol)}</div>`;
|
||||||
}
|
}
|
||||||
toHtml += approvalAddressHtml(toAddr, approvedTx.chainId);
|
toHtml += approvalAddressHtml(toAddr, approvedTx.chainId);
|
||||||
$("approve-tx-to").innerHTML = toHtml;
|
$("approve-tx-to").innerHTML = toHtml;
|
||||||
@@ -394,14 +394,14 @@ function showTxApproval(details) {
|
|||||||
$("approve-tx-action").textContent = decoded.name;
|
$("approve-tx-action").textContent = decoded.name;
|
||||||
let detailsHtml = "";
|
let detailsHtml = "";
|
||||||
if (decoded.description) {
|
if (decoded.description) {
|
||||||
detailsHtml += `<div class="mb-2">${escapeHtml(decoded.description)}</div>`;
|
detailsHtml += `<div class="mb-2 break-words">${escapeHtml(decoded.description)}</div>`;
|
||||||
}
|
}
|
||||||
for (const d of decoded.details) {
|
for (const d of decoded.details) {
|
||||||
detailsHtml += `<div class="mb-2">`;
|
detailsHtml += `<div class="mb-2">`;
|
||||||
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
|
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
|
||||||
if (d.address) {
|
if (d.address) {
|
||||||
if (d.isToken) {
|
if (d.isToken) {
|
||||||
detailsHtml += `<div class="font-bold">${escapeHtml(tokenLabel(d.address, approvedTx.chainId) || "Unknown token")}</div>`;
|
detailsHtml += `<div class="font-bold break-words">${escapeHtml(tokenLabel(d.address, approvedTx.chainId) || "Unknown token")}</div>`;
|
||||||
detailsHtml += blockscoutOffTokenNoteHtml(
|
detailsHtml += blockscoutOffTokenNoteHtml(
|
||||||
d.address,
|
d.address,
|
||||||
approvedTx.chainId,
|
approvedTx.chainId,
|
||||||
@@ -412,7 +412,7 @@ function showTxApproval(details) {
|
|||||||
approvedTx.chainId,
|
approvedTx.chainId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
detailsHtml += `<div class="font-bold break-words">${escapeHtml(d.value)}</div>`;
|
||||||
}
|
}
|
||||||
detailsHtml += `</div>`;
|
detailsHtml += `</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,13 +126,16 @@ function render() {
|
|||||||
const exactStr = tx.exactValue
|
const exactStr = tx.exactValue
|
||||||
? tx.exactValue + " " + detailSym
|
? tx.exactValue + " " + detailSym
|
||||||
: tx.directionLabel + " " + detailSym;
|
: tx.directionLabel + " " + detailSym;
|
||||||
$("tx-detail-value").innerHTML = copyableHtml(exactStr, "font-bold");
|
$("tx-detail-value").innerHTML = copyableHtml(
|
||||||
|
exactStr,
|
||||||
|
"font-bold break-words",
|
||||||
|
);
|
||||||
|
|
||||||
// Native quantity (raw integer, copyable)
|
// Native quantity (raw integer, copyable)
|
||||||
const nativeEl = $("tx-detail-native");
|
const nativeEl = $("tx-detail-native");
|
||||||
if (tx.rawAmount && tx.rawUnit) {
|
if (tx.rawAmount && tx.rawUnit) {
|
||||||
const nativeStr = tx.rawAmount + " " + tx.rawUnit;
|
const nativeStr = tx.rawAmount + " " + tx.rawUnit;
|
||||||
nativeEl.innerHTML = copyableHtml(nativeStr, "");
|
nativeEl.innerHTML = copyableHtml(nativeStr, "break-words");
|
||||||
nativeEl.parentElement.classList.remove("hidden");
|
nativeEl.parentElement.classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
nativeEl.innerHTML = "";
|
nativeEl.innerHTML = "";
|
||||||
@@ -368,7 +371,7 @@ async function loadFullTxDetails(txHash, chainId) {
|
|||||||
actionEl.textContent = decoded.name;
|
actionEl.textContent = decoded.name;
|
||||||
let detailsHtml = "";
|
let detailsHtml = "";
|
||||||
if (decoded.description) {
|
if (decoded.description) {
|
||||||
detailsHtml += `<div class="mb-2">${escapeHtml(decoded.description)}</div>`;
|
detailsHtml += `<div class="mb-2 break-words">${escapeHtml(decoded.description)}</div>`;
|
||||||
}
|
}
|
||||||
for (const d of decoded.details || []) {
|
for (const d of decoded.details || []) {
|
||||||
detailsHtml += `<div class="mb-2">`;
|
detailsHtml += `<div class="mb-2">`;
|
||||||
@@ -377,13 +380,13 @@ async function loadFullTxDetails(txHash, chainId) {
|
|||||||
// Token entry: show symbol on its own line, then address via shared renderer
|
// Token entry: show symbol on its own line, then address via shared renderer
|
||||||
const tokenSymbol = d.value.match(/^(\S+)\s*\(/)?.[1];
|
const tokenSymbol = d.value.match(/^(\S+)\s*\(/)?.[1];
|
||||||
if (tokenSymbol) {
|
if (tokenSymbol) {
|
||||||
detailsHtml += `<div class="font-bold">${escapeHtml(displaySymbol(tokenSymbol))}</div>`;
|
detailsHtml += `<div class="font-bold break-words">${escapeHtml(displaySymbol(tokenSymbol))}</div>`;
|
||||||
}
|
}
|
||||||
detailsHtml += renderAddressHtml(d.address, { chainId });
|
detailsHtml += renderAddressHtml(d.address, { chainId });
|
||||||
} else if (d.address) {
|
} else if (d.address) {
|
||||||
detailsHtml += renderAddressHtml(d.address, { chainId });
|
detailsHtml += renderAddressHtml(d.address, { chainId });
|
||||||
} else {
|
} else {
|
||||||
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
detailsHtml += `<div class="font-bold break-words">${escapeHtml(d.value)}</div>`;
|
||||||
}
|
}
|
||||||
detailsHtml += `</div>`;
|
detailsHtml += `</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-13
@@ -321,10 +321,14 @@ function showSuccess(txInfo, txHash, blockNumber) {
|
|||||||
ctx.doRefreshAndRender();
|
ctx.doRefreshAndRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readableLines(decoded) {
|
||||||
|
if (!decoded || !Array.isArray(decoded.details)) return [];
|
||||||
|
return decoded.details.filter(isReadableLine);
|
||||||
|
}
|
||||||
|
|
||||||
// The decoded action, or "" when no line of it can be read.
|
// The decoded action, or "" when no line of it can be read.
|
||||||
function decodedDetailsHtml(decoded, chainId) {
|
function decodedDetailsHtml(decoded, chainId) {
|
||||||
if (!decoded || !Array.isArray(decoded.details)) return "";
|
const lines = readableLines(decoded);
|
||||||
const lines = decoded.details.filter(isReadableLine);
|
|
||||||
if (lines.length === 0) return "";
|
if (lines.length === 0) return "";
|
||||||
let html = `<div class="border border-border border-dashed p-2 mb-3">`;
|
let html = `<div class="border border-border border-dashed p-2 mb-3">`;
|
||||||
if (decoded.name && typeof decoded.name === "string") {
|
if (decoded.name && typeof decoded.name === "string") {
|
||||||
@@ -355,20 +359,22 @@ function renderSuccess() {
|
|||||||
|
|
||||||
const decodedHtml = decodedDetailsHtml(d.decoded, d.chainId);
|
const decodedHtml = decodedDetailsHtml(d.decoded, d.chainId);
|
||||||
|
|
||||||
// When decoded details are present, the Amount and To are already
|
// Amount and To are hidden only when a readable decoded line shows the same
|
||||||
// shown inside the decoded well — hide the top-level duplicates.
|
// thing: an `Amount` line, or a line with the address in `to`. Some swaps
|
||||||
const summarySection = $("success-tx-summary").parentElement;
|
// have no `Amount` line, and a saved record can have unreadable lines. An
|
||||||
const toSection = $("success-tx-to").parentElement;
|
// empty `to` is a contract creation, which no decoded line shows.
|
||||||
if (decodedHtml) {
|
const lines = readableLines(d.decoded);
|
||||||
summarySection.classList.add("hidden");
|
$("success-tx-summary").parentElement.classList.toggle(
|
||||||
toSection.classList.add("hidden");
|
"hidden",
|
||||||
} else {
|
lines.some((line) => line.label === "Amount"),
|
||||||
summarySection.classList.remove("hidden");
|
);
|
||||||
toSection.classList.remove("hidden");
|
$("success-tx-to").parentElement.classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
lines.some((line) => line.address && line.address === d.to),
|
||||||
|
);
|
||||||
$("success-tx-summary").textContent =
|
$("success-tx-summary").textContent =
|
||||||
d.amount + (d.symbol ? " " + d.symbol : "");
|
d.amount + (d.symbol ? " " + d.symbol : "");
|
||||||
$("success-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
|
$("success-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
|
||||||
}
|
|
||||||
|
|
||||||
$("success-tx-block").innerHTML = blockNumberHtml(d.blockNumber, d.chainId);
|
$("success-tx-block").innerHTML = blockNumberHtml(d.blockNumber, d.chainId);
|
||||||
$("success-tx-hash").innerHTML = txHashHtml(d.hash, d.chainId);
|
$("success-tx-hash").innerHTML = txHashHtml(d.hash, d.chainId);
|
||||||
|
|||||||
@@ -151,10 +151,10 @@ test("names no token, and states the amount in base units", async () => {
|
|||||||
expect(state.currentView).toBe("approve-tx");
|
expect(state.currentView).toBe("approve-tx");
|
||||||
expect(node("approve-tx-to").innerHTML).not.toContain("USDC");
|
expect(node("approve-tx-to").innerHTML).not.toContain("USDC");
|
||||||
expect(decodedLine("Token")).toContain(
|
expect(decodedLine("Token")).toContain(
|
||||||
'<div class="font-bold">Unknown token</div>',
|
'<div class="font-bold break-words">Unknown token</div>',
|
||||||
);
|
);
|
||||||
expect(decodedLine("Amount")).toContain(
|
expect(decodedLine("Amount")).toContain(
|
||||||
'<div class="font-bold">1234500000 base units (decimals unknown)</div>',
|
'<div class="font-bold break-words">1234500000 base units (decimals unknown)</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,8 +162,10 @@ test("names no token, and states the amount in base units", async () => {
|
|||||||
test("a mainnet transfer of USDC is still named and scaled as USDC", async () => {
|
test("a mainnet transfer of USDC is still named and scaled as USDC", async () => {
|
||||||
await showTransfer(NETWORKS.mainnet.chainId);
|
await showTransfer(NETWORKS.mainnet.chainId);
|
||||||
expect(state.currentView).toBe("approve-tx");
|
expect(state.currentView).toBe("approve-tx");
|
||||||
expect(decodedLine("Token")).toContain('<div class="font-bold">USDC</div>');
|
expect(decodedLine("Token")).toContain(
|
||||||
|
'<div class="font-bold break-words">USDC</div>',
|
||||||
|
);
|
||||||
expect(decodedLine("Amount")).toContain(
|
expect(decodedLine("Amount")).toContain(
|
||||||
'<div class="font-bold">1234.5000 USDC</div>',
|
'<div class="font-bold break-words">1234.5000 USDC</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ globalThis.chrome = {
|
|||||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const { Interface } = require("ethers");
|
const { Interface } = require("ethers");
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
const { NETWORKS } = require("../src/shared/networks");
|
const { NETWORKS } = require("../src/shared/networks");
|
||||||
@@ -193,13 +195,13 @@ describe("a token tracked on Sepolia only", () => {
|
|||||||
test("is named on the Contract line and the decoded Token line, with its amount at its scale", async () => {
|
test("is named on the Contract line and the decoded Token line, with its amount at its scale", async () => {
|
||||||
await showTokenTransfer();
|
await showTokenTransfer();
|
||||||
expect(node("approve-tx-to").innerHTML).toContain(
|
expect(node("approve-tx-to").innerHTML).toContain(
|
||||||
'<div class="font-bold mb-1">TDOL</div>',
|
'<div class="font-bold mb-1 break-words">TDOL</div>',
|
||||||
);
|
);
|
||||||
expect(decodedLine("Token")).toContain(
|
expect(decodedLine("Token")).toContain(
|
||||||
'<div class="font-bold">TDOL</div>',
|
'<div class="font-bold break-words">TDOL</div>',
|
||||||
);
|
);
|
||||||
expect(decodedLine("Amount")).toContain(
|
expect(decodedLine("Amount")).toContain(
|
||||||
'<div class="font-bold">1234.5000 TDOL</div>',
|
'<div class="font-bold break-words">1234.5000 TDOL</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -220,13 +222,51 @@ test("a token tracked and held on mainnet only is not named, and its amount is i
|
|||||||
]);
|
]);
|
||||||
expect(node("approve-tx-to").innerHTML).not.toContain("MDOL");
|
expect(node("approve-tx-to").innerHTML).not.toContain("MDOL");
|
||||||
expect(decodedLine("Token")).toContain(
|
expect(decodedLine("Token")).toContain(
|
||||||
'<div class="font-bold">Unknown token</div>',
|
'<div class="font-bold break-words">Unknown token</div>',
|
||||||
);
|
);
|
||||||
expect(decodedLine("Amount")).toContain(
|
expect(decodedLine("Amount")).toContain(
|
||||||
'<div class="font-bold">1234500000 base units (decimals unknown)</div>',
|
'<div class="font-bold break-words">1234500000 base units (decimals unknown)</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Nothing limits the length of a token's symbol. Without the break-words class
|
||||||
|
// a long one runs past the right edge of the screen and is cut off.
|
||||||
|
describe("a long token symbol", () => {
|
||||||
|
const LONG = "L".repeat(60);
|
||||||
|
|
||||||
|
test("wraps on every decoded line and on the Contract line", async () => {
|
||||||
|
state.networkTrackedTokens = {
|
||||||
|
sepolia: [{ address: TOKEN, symbol: LONG, decimals: 6 }],
|
||||||
|
};
|
||||||
|
await showTokenTransfer();
|
||||||
|
expect(node("approve-tx-to").innerHTML).toContain(
|
||||||
|
`<div class="font-bold mb-1 break-words">${LONG}</div>`,
|
||||||
|
);
|
||||||
|
expect(node("approve-tx-decoded-details").innerHTML).toContain(
|
||||||
|
`<div class="mb-2 break-words">Transfer ${LONG}</div>`,
|
||||||
|
);
|
||||||
|
expect(decodedLine("Token")).toContain(
|
||||||
|
`<div class="font-bold break-words">${LONG}</div>`,
|
||||||
|
);
|
||||||
|
expect(decodedLine("Amount")).toContain(
|
||||||
|
`<div class="font-bold break-words">1234.5000 ${LONG}</div>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("wraps on the Action line, and a long amount wraps on the Value line", () => {
|
||||||
|
const html = fs.readFileSync(
|
||||||
|
path.join(__dirname, "..", "src", "popup", "index.html"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
expect(html).toMatch(
|
||||||
|
/id="approve-tx-action"\s+class="text-xs font-bold mb-2 break-words"/,
|
||||||
|
);
|
||||||
|
expect(html).toMatch(
|
||||||
|
/id="approve-tx-value"\s+class="text-xs font-bold break-words"/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// The From and Contract lines, and the decoded Token and Recipient lines.
|
// The From and Contract lines, and the decoded Token and Recipient lines.
|
||||||
test("every address links to the Sepolia explorer", async () => {
|
test("every address links to the Sepolia explorer", async () => {
|
||||||
await showTokenTransfer();
|
await showTokenTransfer();
|
||||||
|
|||||||
@@ -1103,7 +1103,7 @@ describe("a popup reopened on a SuccessTx with a decoded record in another shape
|
|||||||
expect(page.node("success-tx-to").innerHTML).toContain(ADDRESS);
|
expect(page.node("success-tx-to").innerHTML).toContain(ADDRESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a line it can read is shown", async () => {
|
test("a line it can read is shown, with the amount and the recipient", async () => {
|
||||||
const page = await bootPopup(
|
const page = await bootPopup(
|
||||||
savedSuccess({
|
savedSuccess({
|
||||||
details: [null, { label: "Steps", value: "V3_SWAP_EXACT_IN" }],
|
details: [null, { label: "Steps", value: "V3_SWAP_EXACT_IN" }],
|
||||||
@@ -1114,5 +1114,11 @@ describe("a popup reopened on a SuccessTx with a decoded record in another shape
|
|||||||
expect(page.node("success-tx-decoded").innerHTML).toContain(
|
expect(page.node("success-tx-decoded").innerHTML).toContain(
|
||||||
"V3_SWAP_EXACT_IN",
|
"V3_SWAP_EXACT_IN",
|
||||||
);
|
);
|
||||||
|
const summary = page.node("success-tx-summary");
|
||||||
|
expect(summary.parentElement.classList.contains("hidden")).toBe(false);
|
||||||
|
expect(summary.textContent).toBe("1.0000 ETH");
|
||||||
|
const to = page.node("success-tx-to");
|
||||||
|
expect(to.parentElement.classList.contains("hidden")).toBe(false);
|
||||||
|
expect(to.innerHTML).toContain(ADDRESS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -231,6 +231,15 @@ async function approveSwap(data, response, value = 0n) {
|
|||||||
await click("btn-approve-tx");
|
await click("btn-approve-tx");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A later popup resumes the wait, and the receipt is there.
|
||||||
|
async function receiptArrives() {
|
||||||
|
mockReceipt = { blockNumber: 1 };
|
||||||
|
txStatus.restoreWait();
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
txStatus.endWait();
|
txStatus.endWait();
|
||||||
mockReceipt = null;
|
mockReceipt = null;
|
||||||
@@ -287,20 +296,27 @@ describe.each([
|
|||||||
expect(node("error-tx-summary").textContent).toBe(expected);
|
expect(node("error-tx-summary").textContent).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The success screen shows the approval screen's decoded lines in place
|
// The success screen has an Amount line of its own only when the approval
|
||||||
// of the amount, so the ETH value is not on it either.
|
// screen's decoded lines have none. The ETH value is on neither.
|
||||||
test("the success screen has no amount line of its own", async () => {
|
test("the success screen's amount", async () => {
|
||||||
await approveSwap(v4SwapData(actions), { txHash: TX_HASH });
|
await approveSwap(v4SwapData(actions), { txHash: TX_HASH });
|
||||||
mockReceipt = { blockNumber: 1 };
|
await receiptArrives();
|
||||||
// A later popup resumes the wait, and the receipt is there.
|
const summary = node("success-tx-summary");
|
||||||
txStatus.restoreWait();
|
expect(summary.parentElement.classList.contains("hidden")).toBe(
|
||||||
for (let i = 0; i < 10; i++) {
|
!expected.startsWith("Unknown"),
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
);
|
||||||
}
|
expect(summary.textContent).toBe(expected);
|
||||||
const summary = node("success-tx-summary").parentElement;
|
|
||||||
expect(summary.classList.contains("hidden")).toBe(true);
|
|
||||||
expect(node("success-tx-decoded").innerHTML).not.toContain("0.0000");
|
expect(node("success-tx-decoded").innerHTML).not.toContain("0.0000");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The Protocol line of the decoded lines shows the router.
|
||||||
|
test("the success screen has no To line of its own", async () => {
|
||||||
|
await approveSwap(v4SwapData(actions), { txHash: TX_HASH });
|
||||||
|
await receiptArrives();
|
||||||
|
const to = node("success-tx-to").parentElement;
|
||||||
|
expect(to.classList.contains("hidden")).toBe(true);
|
||||||
|
expect(node("success-tx-decoded").innerHTML).toContain(ROUTER);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// A swap paid in native ETH has a Token In of "ETH (native)" and no Amount
|
// A swap paid in native ETH has a Token In of "ETH (native)" and no Amount
|
||||||
@@ -339,6 +355,14 @@ describe.each([
|
|||||||
);
|
);
|
||||||
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
|
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the success screen shows the ETH the transaction sends", async () => {
|
||||||
|
await approveSwap(v4SwapData(actions), { txHash: TX_HASH }, ONE_ETH);
|
||||||
|
await receiptArrives();
|
||||||
|
const summary = node("success-tx-summary");
|
||||||
|
expect(summary.parentElement.classList.contains("hidden")).toBe(false);
|
||||||
|
expect(summary.textContent).toBe("1.0000 ETH");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/722: a WRAP_ETH of 1 ETH into the
|
// https://git.eeqj.de/sneak/AutistMask/issues/722: a WRAP_ETH of 1 ETH into the
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
// calls. Driven against a minimal DOM stub in the shape
|
// calls. Driven against a minimal DOM stub in the shape
|
||||||
// tests/contractCreation.test.js uses.
|
// tests/contractCreation.test.js uses.
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const { Interface } = require("ethers");
|
const { Interface } = require("ethers");
|
||||||
|
|
||||||
jest.mock("../src/shared/log", () => ({
|
jest.mock("../src/shared/log", () => ({
|
||||||
@@ -51,6 +53,7 @@ globalThis.chrome = {
|
|||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
const { NETWORKS } = require("../src/shared/networks");
|
const { NETWORKS } = require("../src/shared/networks");
|
||||||
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||||
|
const { displaySymbol } = require("../src/shared/symbolDisplay");
|
||||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||||
|
|
||||||
const EXPLORER = "https://explorer.example/api/v2";
|
const EXPLORER = "https://explorer.example/api/v2";
|
||||||
@@ -197,15 +200,63 @@ test("a token transfer's decoded details name the token contract and the amount
|
|||||||
"Token Transfer",
|
"Token Transfer",
|
||||||
);
|
);
|
||||||
const token = decodedLine("Token");
|
const token = decodedLine("Token");
|
||||||
expect(token).toContain('<div class="font-bold">TDOL</div>');
|
expect(token).toContain('<div class="font-bold break-words">TDOL</div>');
|
||||||
expect(token).toContain(TOKEN);
|
expect(token).toContain(TOKEN);
|
||||||
expect(token).not.toContain(RECIPIENT);
|
expect(token).not.toContain(RECIPIENT);
|
||||||
expect(decodedLine("Recipient")).toContain(RECIPIENT);
|
expect(decodedLine("Recipient")).toContain(RECIPIENT);
|
||||||
expect(decodedLine("Amount")).toContain(
|
expect(decodedLine("Amount")).toContain(
|
||||||
'<div class="font-bold">1234.5000 TDOL</div>',
|
'<div class="font-bold break-words">1234.5000 TDOL</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Nothing limits the length of a symbol the explorer reports. Without the
|
||||||
|
// break-words class a long one runs past the right edge of the screen and is
|
||||||
|
// cut off.
|
||||||
|
describe("a long token symbol", () => {
|
||||||
|
const LONG = "L".repeat(60);
|
||||||
|
|
||||||
|
test("wraps on the amount, native quantity and decoded lines", async () => {
|
||||||
|
state.trackedTokens = [{ address: TOKEN, symbol: LONG, decimals: 6 }];
|
||||||
|
const [row] = await fetchRecentTransactions(
|
||||||
|
HOLDER,
|
||||||
|
EXPLORER,
|
||||||
|
NETWORKS.mainnet.chainId,
|
||||||
|
);
|
||||||
|
transactionDetail.show({
|
||||||
|
...row,
|
||||||
|
symbol: LONG,
|
||||||
|
rawUnit: LONG + " base units (10^-6)",
|
||||||
|
});
|
||||||
|
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
expect(node("tx-detail-value").innerHTML).toContain(
|
||||||
|
`font-bold break-words" data-copy="1234.5 ${displaySymbol(LONG)}"`,
|
||||||
|
);
|
||||||
|
expect(node("tx-detail-native").innerHTML).toContain(
|
||||||
|
`break-words" data-copy="1234500000 ${LONG} base units (10^-6)"`,
|
||||||
|
);
|
||||||
|
expect(node("tx-detail-calldata-details").innerHTML).toContain(
|
||||||
|
`<div class="mb-2 break-words">Transfer ${LONG}</div>`,
|
||||||
|
);
|
||||||
|
expect(decodedLine("Token")).toContain(
|
||||||
|
`<div class="font-bold break-words">${displaySymbol(LONG)}</div>`,
|
||||||
|
);
|
||||||
|
expect(decodedLine("Amount")).toContain(
|
||||||
|
`<div class="font-bold break-words">1234.5000 ${LONG}</div>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("wraps on the Action line", () => {
|
||||||
|
const html = fs.readFileSync(
|
||||||
|
path.join(__dirname, "..", "src", "popup", "index.html"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
expect(html).toMatch(
|
||||||
|
/id="tx-detail-calldata-action"\s+class="text-xs font-bold mb-2 break-words"/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Mainnet is active, and its Blockscout knows nothing of the transaction. The
|
// Mainnet is active, and its Blockscout knows nothing of the transaction. The
|
||||||
// row was read from the Blockscout Settings remembers for Sepolia, and the
|
// row was read from the Blockscout Settings remembers for Sepolia, and the
|
||||||
// screen reads the transaction from there too, so every link on it is drawn.
|
// screen reads the transaction from there too, so every link on it is drawn.
|
||||||
@@ -278,9 +329,11 @@ test("with mainnet active, a Sepolia token transfer's decoded details name the t
|
|||||||
};
|
};
|
||||||
await showSepoliaTransferWithMainnetActive();
|
await showSepoliaTransferWithMainnetActive();
|
||||||
|
|
||||||
expect(decodedLine("Token")).toContain('<div class="font-bold">TDOL</div>');
|
expect(decodedLine("Token")).toContain(
|
||||||
|
'<div class="font-bold break-words">TDOL</div>',
|
||||||
|
);
|
||||||
expect(decodedLine("Amount")).toContain(
|
expect(decodedLine("Amount")).toContain(
|
||||||
'<div class="font-bold">1234.5000 TDOL</div>',
|
'<div class="font-bold break-words">1234.5000 TDOL</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,6 +348,6 @@ test("with mainnet active, a Sepolia token transfer's decoded details take nothi
|
|||||||
|
|
||||||
expect(decodedLine("Token")).not.toContain("MDOL");
|
expect(decodedLine("Token")).not.toContain("MDOL");
|
||||||
expect(decodedLine("Amount")).toContain(
|
expect(decodedLine("Amount")).toContain(
|
||||||
'<div class="font-bold">1234500000 base units (decimals unknown)</div>',
|
'<div class="font-bold break-words">1234500000 base units (decimals unknown)</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ const { state } = require("../src/shared/state");
|
|||||||
const { NETWORKS } = require("../src/shared/networks");
|
const { NETWORKS } = require("../src/shared/networks");
|
||||||
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
||||||
const { log } = require("../src/shared/log");
|
const { log } = require("../src/shared/log");
|
||||||
|
const { CONTRACT_CREATION_TEXT } = require("../src/popup/views/helpers");
|
||||||
|
|
||||||
const TX_HASH =
|
const TX_HASH =
|
||||||
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
|
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
|
||||||
@@ -1009,6 +1010,80 @@ describe("a saved wait in another shape when its receipt arrives", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The decoded action stands in for the Amount and To lines only where it shows
|
||||||
|
// the same thing. A saved record with unreadable lines can leave out either.
|
||||||
|
describe("SuccessTx's Amount and To lines beside a decoded action", () => {
|
||||||
|
const OTHER = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";
|
||||||
|
const STEPS = { label: "Steps", value: "V3_SWAP_EXACT_IN" };
|
||||||
|
const AMOUNT = { label: "Amount", value: "0.0050 ETH" };
|
||||||
|
const RECIPIENT_LINE = {
|
||||||
|
label: "Recipient",
|
||||||
|
value: RECIPIENT,
|
||||||
|
address: RECIPIENT,
|
||||||
|
};
|
||||||
|
const SPENDER = { label: "Spender", value: OTHER, address: OTHER };
|
||||||
|
|
||||||
|
function renderSuccessWith(details, to) {
|
||||||
|
state.viewData = {
|
||||||
|
amount: "0.0050",
|
||||||
|
symbol: "ETH",
|
||||||
|
to,
|
||||||
|
hash: TX_HASH,
|
||||||
|
chainId: TX_INFO.chainId,
|
||||||
|
decoded: { details },
|
||||||
|
};
|
||||||
|
txStatus.renderSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineHidden(id) {
|
||||||
|
return getElement(id + "-parent").classList.contains("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["no line for either", [STEPS], false, false],
|
||||||
|
["a line of another address", [AMOUNT, SPENDER], true, false],
|
||||||
|
["an Amount line only", [AMOUNT], true, false],
|
||||||
|
["a Recipient line only", [RECIPIENT_LINE], false, true],
|
||||||
|
[
|
||||||
|
"an unreadable Amount line",
|
||||||
|
[{ label: "Amount" }, RECIPIENT_LINE],
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"an unreadable Recipient line",
|
||||||
|
[AMOUNT, { address: RECIPIENT }],
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
["a line for each", [AMOUNT, RECIPIENT_LINE], true, true],
|
||||||
|
])("%s", (_name, details, amountHidden, toHidden) => {
|
||||||
|
renderSuccessWith(details, RECIPIENT);
|
||||||
|
expect(state.currentView).toBe("success-tx");
|
||||||
|
expect(lineHidden("success-tx-summary")).toBe(amountHidden);
|
||||||
|
expect(lineHidden("success-tx-to")).toBe(toHidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the lines read the amount and the recipient", () => {
|
||||||
|
renderSuccessWith([STEPS], RECIPIENT);
|
||||||
|
expect(getElement("success-tx-summary").textContent).toBe("0.0050 ETH");
|
||||||
|
expect(getElement("success-tx-to").innerHTML).toContain(RECIPIENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
// A contract creation's data can decode as an ERC-20 call, with an empty
|
||||||
|
// token address.
|
||||||
|
test("a contract creation keeps its To line", () => {
|
||||||
|
renderSuccessWith(
|
||||||
|
[{ label: "Token", value: "", address: "", isToken: true }],
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
expect(lineHidden("success-tx-to")).toBe(false);
|
||||||
|
expect(getElement("success-tx-to").innerHTML).toBe(
|
||||||
|
CONTRACT_CREATION_TEXT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user