fix: keep the flash line to its one line at any message length (closes #252)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s

The flash line reserves one line, so a message that wrapped pushed the
screen below it down. #flash-msg no longer wraps: text too long for it is
cut with an ellipsis, and showFlash() puts the whole message in its title.
Every message is also reworded to at most 50 characters so none is cut,
and the add-token screens flash a fixed line for any error other than the
two lookup messages, logging the detail.

A new end-to-end test writes a message several lines long into the line
and fails if the line or the screen below it moves.

Model: opus-5-5
This commit was merged in pull request #420.
This commit is contained in:
2026-10-04 10:22:51 +02:00
parent 4b62e31e80
commit 5bf8b5ff1f
13 changed files with 262 additions and 81 deletions
+3 -7
View File
@@ -19,13 +19,9 @@
// that the user did not type — the same silent substitution the visible
// rejection message exists to end.
// Must render on ONE line of #flash-msg, whose reserved height
// (min-h-[1.25rem]) is exactly one line at text-xs. A string long enough to
// wrap to two lines pushes the settings view down, which the No Layout Shift
// policy forbids. Do not lengthen this without re-running the layout test in
// tests/e2e/run.js, which measures the flash line and goes red on a shift.
const DUST_THRESHOLD_MESSAGE =
"Please enter a whole number of gwei, zero or greater.";
// Must render on ONE line of #flash-msg; see showFlash() in
// src/popup/views/helpers.js for how long that is.
const DUST_THRESHOLD_MESSAGE = "Enter a whole number of gwei, zero or greater.";
// Returns the threshold in gwei, or null if the input is not one.
function parseDustThresholdGwei(raw) {
+1 -1
View File
@@ -33,7 +33,7 @@
<!-- ============ FLASH MESSAGE AREA ============ -->
<div
id="flash-msg"
class="text-xs text-muted min-h-[1.25rem] mb-1"
class="text-xs text-muted min-h-[1.25rem] mb-1 truncate"
></div>
<!-- ============ WELCOME / FIRST USE ============ -->
+10 -5
View File
@@ -28,9 +28,7 @@ function init(ctx) {
$("btn-add-token-confirm").addEventListener("click", async () => {
const contractAddr = $("add-token-address").value.trim();
if (!contractAddr || !contractAddr.startsWith("0x")) {
showFlash(
"Please enter a valid contract address starting with 0x.",
);
showFlash("Enter a valid contract address starting with 0x.");
return;
}
const already = state.trackedTokens.find(
@@ -71,8 +69,15 @@ function init(ctx) {
require("./addressDetail").show();
} catch (e) {
const detail = e.shortMessage || e.message || String(e);
log.errorf("Token lookup failed for", contractAddr, detail);
showFlash(detail);
log.errorf("Adding token failed for", contractAddr, detail);
// lookupTokenInfo() rejects a contract with a one-line message
// starting "Not a valid ERC-20 token". Any other error, such as a
// failed save, can be far longer, so it is only logged.
showFlash(
detail.startsWith("Not a valid ERC-20 token")
? detail
: "Could not add the token.",
);
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
}
+10 -23
View File
@@ -142,15 +142,13 @@ function validatePassword() {
async function importMnemonic(ctx) {
const mnemonic = $("wallet-mnemonic").value.trim();
if (!mnemonic) {
showFlash("Enter a recovery phrase or press the die to generate one.");
showFlash("Enter a recovery phrase, or press the die.");
return;
}
const words = mnemonic.split(/\s+/);
if (words.length !== 12 && words.length !== 24) {
showFlash(
"Recovery phrase must be 12 or 24 words. You entered " +
words.length +
".",
"Recovery phrase must be 12 or 24 words, not " + words.length + ".",
);
return;
}
@@ -163,14 +161,12 @@ async function importMnemonic(ctx) {
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
const xpubDup = findWalletByXpub(xpub);
if (xpubDup) {
showFlash(
"This recovery phrase is already added (" + xpubDup.name + ").",
);
showFlash("This recovery phrase is already added.");
return;
}
const addrDup = findWalletByAddress(firstAddress);
if (addrDup) {
showFlash("Address already exists in wallet (" + addrDup.name + ").");
showFlash("Address already exists in a wallet.");
return;
}
const encrypted = await encryptWithPassword(mnemonic, pw);
@@ -229,9 +225,7 @@ async function importPrivateKey(ctx) {
if (!pw) return;
const duplicate = findWalletByAddress(addr);
if (duplicate) {
showFlash(
"This address already exists in wallet (" + duplicate.name + ").",
);
showFlash("This address already exists in a wallet.");
return;
}
const encrypted = await encryptWithPassword(key, pw);
@@ -258,36 +252,29 @@ async function importXprvKey(ctx) {
return;
}
if (!isValidXprv(xprv)) {
showFlash(
"That extended private key is not valid. Please check it and try again.",
);
showFlash("That extended private key is not valid.");
return;
}
if (!isMasterExtendedKey(xprv)) {
showFlash(
"That is an account-level or child key, which cannot be imported. " +
"Please paste the master extended private key for the wallet.",
);
showFlash("Please paste the master key, not a child key.");
return;
}
let result;
try {
result = hdWalletFromXprv(xprv);
} catch {
showFlash(
"That extended private key is not valid. Please check it and try again.",
);
showFlash("That extended private key is not valid.");
return;
}
const { xpub, firstAddress } = result;
const xpubDup = findWalletByXpub(xpub);
if (xpubDup) {
showFlash("This key is already added (" + xpubDup.name + ").");
showFlash("This key is already added.");
return;
}
const addrDup = findWalletByAddress(firstAddress);
if (addrDup) {
showFlash("Address already exists in wallet (" + addrDup.name + ").");
showFlash("Address already exists in a wallet.");
return;
}
const pw = validatePassword();
+8 -4
View File
@@ -223,15 +223,19 @@ function clearFlash() {
flashTimer = null;
}
$("flash-msg").textContent = "";
$("flash-msg").title = "";
}
// The flash line reserves exactly one line, and a message that wrapped would
// push the screen below it down (README, No Layout Shift). So #flash-msg never
// wraps: text too long for the line is cut with an ellipsis, and the whole
// message is also put in the line's title. Write messages to fit, at most 50
// characters, so none is cut.
function showFlash(msg, duration = 2000) {
clearFlash();
$("flash-msg").textContent = msg;
flashTimer = setTimeout(() => {
$("flash-msg").textContent = "";
flashTimer = null;
}, duration);
$("flash-msg").title = msg;
flashTimer = setTimeout(clearFlash, duration);
}
// A stored token balance as a number, or null when there is no number in it.
+3 -3
View File
@@ -63,13 +63,13 @@ function validateToAddress(value) {
if (checksummed !== v) {
return {
valid: false,
error: "Address checksum is invalid. Please double-check the address.",
error: "Address checksum is invalid. Check the address.",
};
}
} catch {
return {
valid: false,
error: "Address checksum is invalid. Please double-check the address.",
error: "Address checksum is invalid. Check the address.",
};
}
}
@@ -211,7 +211,7 @@ function init(_ctx) {
const provider = getProvider(state.rpcUrl, state.networkId);
const resolved = await provider.resolveName(to);
if (!resolved) {
showFlash("Could not resolve " + to);
showFlash("That ENS name has no address.");
return;
}
resolvedTo = resolved;
+2 -8
View File
@@ -264,18 +264,12 @@ function init(ctx) {
const json = await resp.json();
if (json.error) {
log.errorf("RPC validation error:", json.error);
showFlash("Endpoint returned error: " + json.error.message);
showFlash("Endpoint returned an error.");
return;
}
const net = currentNetwork();
if (json.result !== net.chainId) {
showFlash(
"Wrong network (expected " +
net.name +
", got chain " +
json.result +
").",
);
showFlash("Wrong network: expected " + net.name + ".");
return;
}
} catch (e) {
+10 -5
View File
@@ -115,9 +115,7 @@ function init(_ctx) {
$("btn-settings-addtoken-manual").addEventListener("click", async () => {
const addr = $("settings-addtoken-address").value.trim();
if (!addr || !addr.startsWith("0x")) {
showFlash(
"Please enter a valid contract address starting with 0x.",
);
showFlash("Enter a valid contract address starting with 0x.");
return;
}
if (isTracked(addr)) {
@@ -155,8 +153,15 @@ function init(_ctx) {
ctx.doRefreshAndRender();
} catch (e) {
const detail = e.shortMessage || e.message || String(e);
log.errorf("Token lookup failed for", addr, detail);
showFlash(detail);
log.errorf("Adding token failed for", addr, detail);
// lookupTokenInfo() rejects a contract with a one-line message
// starting "Not a valid ERC-20 token". Any other error, such as a
// failed save, can be far longer, so it is only logged.
showFlash(
detail.startsWith("Not a valid ERC-20 token")
? detail
: "Could not add the token.",
);
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
}