Compare commits
1
Commits
next
...
066842bcec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
066842bcec |
@@ -45,6 +45,16 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-05: Escaping in the popup's views follows its own rule with no
|
||||||
|
exceptions ([#329](https://git.eeqj.de/sneak/AutistMask/issues/329)). The
|
||||||
|
decimals and holder count on a token's screen, and every USD figure (the ETH
|
||||||
|
price, each total and each balance row's value), went into `innerHTML`
|
||||||
|
unescaped; they are escaped now. None could carry markup, but `formatUsd()`
|
||||||
|
writes a value under a cent as `< $0.01`. `displaySymbol()` counts a symbol in
|
||||||
|
code points rather than UTF-16 units, so a cut never splits an emoji into a
|
||||||
|
half that renders as U+FFFD. `explorerLink()`, also named in the issue, was
|
||||||
|
already removed by [#168](https://git.eeqj.de/sneak/AutistMask/issues/168).
|
||||||
|
|
||||||
- 2026-10-05: A Chrome end-to-end test that fails no longer takes later tests
|
- 2026-10-05: A Chrome end-to-end test that fails no longer takes later tests
|
||||||
down with it ([#318](https://git.eeqj.de/sneak/AutistMask/issues/318)). Each
|
down with it ([#318](https://git.eeqj.de/sneak/AutistMask/issues/318)). Each
|
||||||
test that turns a fixture switch on for itself alone (a held or failing gas
|
test that turns a fixture switch on for itself alone (a held or failing gas
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function show() {
|
|||||||
$("address-line").dataset.full = addr.address;
|
$("address-line").dataset.full = addr.address;
|
||||||
attachCopyHandlers($("address-line"));
|
attachCopyHandlers($("address-line"));
|
||||||
const usdTotal = formatAddressTotal(getAddressValue(addr));
|
const usdTotal = formatAddressTotal(getAddressValue(addr));
|
||||||
$("address-usd-total").innerHTML = usdTotal || " ";
|
$("address-usd-total").innerHTML = escapeHtml(usdTotal) || " ";
|
||||||
const ensEl = $("address-ens");
|
const ensEl = $("address-ens");
|
||||||
// ENS is now shown inside renderAddressHtml, hide the separate element
|
// ENS is now shown inside renderAddressHtml, hide the separate element
|
||||||
ensEl.classList.add("hidden");
|
ensEl.classList.add("hidden");
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ function show() {
|
|||||||
// USD total for this token only
|
// USD total for this token only
|
||||||
const usdVal = price && amount !== null ? amount * price : null;
|
const usdVal = price && amount !== null ? amount * price : null;
|
||||||
const usdStr = formatUsd(usdVal);
|
const usdStr = formatUsd(usdVal);
|
||||||
$("address-token-usd-total").innerHTML = usdStr || " ";
|
$("address-token-usd-total").innerHTML = escapeHtml(usdStr) || " ";
|
||||||
|
|
||||||
// Single token balance line (no tokenId — not clickable here)
|
// Single token balance line (no tokenId — not clickable here)
|
||||||
$("address-token-balance").innerHTML = balanceLine(symbol, amount, price);
|
$("address-token-balance").innerHTML = balanceLine(symbol, amount, price);
|
||||||
@@ -148,9 +148,9 @@ function show() {
|
|||||||
if (tokenSymbol)
|
if (tokenSymbol)
|
||||||
infoHtml += `<div class="mb-1"><span class="text-muted">Symbol:</span> ${tokenSymbol}</div>`;
|
infoHtml += `<div class="mb-1"><span class="text-muted">Symbol:</span> ${tokenSymbol}</div>`;
|
||||||
if (tokenDecimals != null)
|
if (tokenDecimals != null)
|
||||||
infoHtml += `<div class="mb-1"><span class="text-muted">Decimals:</span> ${tokenDecimals}</div>`;
|
infoHtml += `<div class="mb-1"><span class="text-muted">Decimals:</span> ${escapeHtml(tokenDecimals)}</div>`;
|
||||||
if (tokenHolders != null)
|
if (tokenHolders != null)
|
||||||
infoHtml += `<div class="mb-1"><span class="text-muted">Holders:</span> ${Number(tokenHolders).toLocaleString()}</div>`;
|
infoHtml += `<div class="mb-1"><span class="text-muted">Holders:</span> ${escapeHtml(Number(tokenHolders).toLocaleString())}</div>`;
|
||||||
if (projectUrl)
|
if (projectUrl)
|
||||||
infoHtml += `<div class="mb-1"><span class="text-muted">Website:</span> <a href="${escapeHtml(projectUrl)}" target="_blank" rel="noopener" class="underline decoration-dashed">${escapeHtml(projectUrl)}</a></div>`;
|
infoHtml += `<div class="mb-1"><span class="text-muted">Website:</span> <a href="${escapeHtml(projectUrl)}" target="_blank" rel="noopener" class="underline decoration-dashed">${escapeHtml(projectUrl)}</a></div>`;
|
||||||
contractInfo.innerHTML = infoHtml;
|
contractInfo.innerHTML = infoHtml;
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ function balanceLine(symbol, amount, price, tokenId) {
|
|||||||
const qty = amount === null ? "quantity unknown" : amount.toFixed(4);
|
const qty = amount === null ? "quantity unknown" : amount.toFixed(4);
|
||||||
const usd =
|
const usd =
|
||||||
price && amount !== null
|
price && amount !== null
|
||||||
? formatUsd(amount * price) || " "
|
? escapeHtml(formatUsd(amount * price)) || " "
|
||||||
: " ";
|
: " ";
|
||||||
// tokenId is a contract address out of the same explorer JSON, and it
|
// tokenId is a contract address out of the same explorer JSON, and it
|
||||||
// lands inside a quoted attribute.
|
// lands inside a quoted attribute.
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function renderTotalValue() {
|
|||||||
const ethPrice = getPrice("ETH");
|
const ethPrice = getPrice("ETH");
|
||||||
if (priceEl) {
|
if (priceEl) {
|
||||||
priceEl.innerHTML = ethPrice
|
priceEl.innerHTML = ethPrice
|
||||||
? formatUsd(ethPrice) + " USD/ETH"
|
? escapeHtml(formatUsd(ethPrice) + " USD/ETH")
|
||||||
: " ";
|
: " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,8 @@ function renderTotalValue() {
|
|||||||
el.textContent = ethStr + ethUsd;
|
el.textContent = ethStr + ethUsd;
|
||||||
|
|
||||||
if (subEl) {
|
if (subEl) {
|
||||||
subEl.innerHTML = formatAddressTotal(getAddressValue(addr)) || " ";
|
subEl.innerHTML =
|
||||||
|
escapeHtml(formatAddressTotal(getAddressValue(addr))) || " ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +281,7 @@ function walletListHtml() {
|
|||||||
}
|
}
|
||||||
html += `<div class="am-address text-xs">${escapeHtml(addr.address)}</div>`;
|
html += `<div class="am-address text-xs">${escapeHtml(addr.address)}</div>`;
|
||||||
const addrTotal = formatAddressTotal(getAddressValue(addr));
|
const addrTotal = formatAddressTotal(getAddressValue(addr));
|
||||||
html += `<div class="text-xs text-muted text-right min-h-[1rem]">${addrTotal || " "}</div>`;
|
html += `<div class="text-xs text-muted text-right min-h-[1rem]">${escapeHtml(addrTotal) || " "}</div>`;
|
||||||
html += balanceLinesForAddress(
|
html += balanceLinesForAddress(
|
||||||
addr,
|
addr,
|
||||||
state.trackedTokens,
|
state.trackedTokens,
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
// (MSYRUPUSDP), so nothing the wallet ships as a real token is ever
|
// (MSYRUPUSDP), so nothing the wallet ships as a real token is ever
|
||||||
// truncated. The ellipsis is what tells the user the name they are looking
|
// truncated. The ellipsis is what tells the user the name they are looking
|
||||||
// at is not the whole name — worth knowing before they send to it.
|
// at is not the whole name — worth knowing before they send to it.
|
||||||
|
//
|
||||||
|
// Characters are counted as code points, not UTF-16 units, so an emoji is
|
||||||
|
// one character and the cut never falls between the two halves of one: a
|
||||||
|
// half on its own renders as U+FFFD.
|
||||||
|
|
||||||
const MAX_SYMBOL_LENGTH = 12;
|
const MAX_SYMBOL_LENGTH = 12;
|
||||||
|
|
||||||
@@ -32,8 +36,9 @@ const UNKNOWN_SYMBOL = "???";
|
|||||||
function displaySymbol(symbol) {
|
function displaySymbol(symbol) {
|
||||||
const s = symbol === null || symbol === undefined ? "" : String(symbol);
|
const s = symbol === null || symbol === undefined ? "" : String(symbol);
|
||||||
if (s.length === 0) return UNKNOWN_SYMBOL;
|
if (s.length === 0) return UNKNOWN_SYMBOL;
|
||||||
if (s.length <= MAX_SYMBOL_LENGTH) return s;
|
const chars = Array.from(s);
|
||||||
return s.slice(0, MAX_SYMBOL_LENGTH - 1) + "…";
|
if (chars.length <= MAX_SYMBOL_LENGTH) return s;
|
||||||
|
return chars.slice(0, MAX_SYMBOL_LENGTH - 1).join("") + "…";
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -194,6 +194,14 @@ describe("the wallet list on Home", () => {
|
|||||||
clearPrices();
|
clearPrices();
|
||||||
expect(walletListTotal(FULLY_PRICED)).toBe(" ");
|
expect(walletListTotal(FULLY_PRICED)).toBe(" ");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A total under a cent is written "< $0.01", and the "<" is escaped
|
||||||
|
// here as the removal warning escapes it.
|
||||||
|
test("a total under a cent is escaped, as on the removal warning", () => {
|
||||||
|
const tiny = { ...EMPTY, balance: "0.000001" };
|
||||||
|
expect(walletListTotal(tiny)).toBe("Total: < $0.01");
|
||||||
|
expect(removalWarningTotal(tiny)).toBe("Total: < $0.01");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("the balance warning on the address-removal confirmation", () => {
|
describe("the balance warning on the address-removal confirmation", () => {
|
||||||
|
|||||||
@@ -66,4 +66,11 @@ describe("balanceLine", () => {
|
|||||||
expect(html).toContain("<span>1.5000</span>");
|
expect(html).toContain("<span>1.5000</span>");
|
||||||
expect(html).toContain('data-token="0xabc"');
|
expect(html).toContain('data-token="0xabc"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// formatUsd() writes a value under a cent as "< $0.01".
|
||||||
|
test("escapes the USD value along with the symbol", () => {
|
||||||
|
const html = balanceLine("USDC", 0.001, 1, null);
|
||||||
|
expect(html).toContain("< $0.01");
|
||||||
|
expect(html).not.toContain("< $0.01");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -91,6 +91,17 @@ describe("displaySymbol", () => {
|
|||||||
expect(displaySymbol(exact)).toBe(exact);
|
expect(displaySymbol(exact)).toBe(exact);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// An emoji outside the Basic Multilingual Plane is two UTF-16 units.
|
||||||
|
// Cutting between them leaves half of one, which renders as U+FFFD.
|
||||||
|
test("counts an emoji as one character and never cuts one in half", () => {
|
||||||
|
expect(displaySymbol("🚀".repeat(MAX_SYMBOL_LENGTH))).toBe(
|
||||||
|
"🚀".repeat(MAX_SYMBOL_LENGTH),
|
||||||
|
);
|
||||||
|
expect(displaySymbol("🚀".repeat(20))).toBe(
|
||||||
|
"🚀".repeat(MAX_SYMBOL_LENGTH - 1) + "…",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("substitutes a placeholder for an absent symbol", () => {
|
test("substitutes a placeholder for an absent symbol", () => {
|
||||||
expect(displaySymbol("")).toBe(UNKNOWN_SYMBOL);
|
expect(displaySymbol("")).toBe(UNKNOWN_SYMBOL);
|
||||||
expect(displaySymbol(null)).toBe(UNKNOWN_SYMBOL);
|
expect(displaySymbol(null)).toBe(UNKNOWN_SYMBOL);
|
||||||
|
|||||||
Reference in New Issue
Block a user