fix: keep every flash message on the one line it reserves (closes #252)
The flash line reserves one line of height, so a message that wrapped pushed the screen below it down. Every message is now at most 50 characters, one line of the popup's monospace font: the longer ones are reworded, and the ones that carried a wallet name or text from a server no longer do. The rule is written at showFlash(). A new end-to-end test drives the longest message and fails if the line's rendered height grows. It measures in the monospace font, which Firefox draws and Chromium does not. Model: opus-5-5
This commit is contained in:
@@ -45,6 +45,20 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-04: Every flash message fits on the one line the flash line reserves,
|
||||||
|
and a test measures it
|
||||||
|
([#252](https://git.eeqj.de/sneak/AutistMask/issues/252)). A message that
|
||||||
|
wrapped pushed the whole screen below it down. Rather than reserve a second
|
||||||
|
line on every screen or let the flash cover the screen, every message is now
|
||||||
|
at most 50 characters, one line of the popup's monospace font: the longer ones
|
||||||
|
are reworded, and messages that carried a wallet name or text from a server no
|
||||||
|
longer do, since neither has a length limit. The rule is written at
|
||||||
|
`showFlash()` in `src/popup/views/helpers.js`. A new test in
|
||||||
|
`tests/e2e/run.js` drives the longest message and fails if the line's rendered
|
||||||
|
height grows. It measures in the monospace font, because Chromium draws the
|
||||||
|
popup in its narrower system font and only Firefox shows the wrap. The two
|
||||||
|
approval-screen error boxes are left to
|
||||||
|
[#297](https://git.eeqj.de/sneak/AutistMask/issues/297).
|
||||||
- 2026-10-04: Removing an address or deleting a wallet ends every site
|
- 2026-10-04: Removing an address or deleting a wallet ends every site
|
||||||
connection approved without "Remember" for the addresses removed
|
connection approved without "Remember" for the addresses removed
|
||||||
([#245](https://git.eeqj.de/sneak/AutistMask/issues/245)). Such a connection
|
([#245](https://git.eeqj.de/sneak/AutistMask/issues/245)). Such a connection
|
||||||
|
|||||||
@@ -19,13 +19,9 @@
|
|||||||
// that the user did not type — the same silent substitution the visible
|
// that the user did not type — the same silent substitution the visible
|
||||||
// rejection message exists to end.
|
// rejection message exists to end.
|
||||||
|
|
||||||
// Must render on ONE line of #flash-msg, whose reserved height
|
// Must render on ONE line of #flash-msg; see showFlash() in
|
||||||
// (min-h-[1.25rem]) is exactly one line at text-xs. A string long enough to
|
// src/popup/views/helpers.js for how long that is.
|
||||||
// wrap to two lines pushes the settings view down, which the No Layout Shift
|
const DUST_THRESHOLD_MESSAGE = "Enter a whole number of gwei, zero or greater.";
|
||||||
// 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.";
|
|
||||||
|
|
||||||
// Returns the threshold in gwei, or null if the input is not one.
|
// Returns the threshold in gwei, or null if the input is not one.
|
||||||
function parseDustThresholdGwei(raw) {
|
function parseDustThresholdGwei(raw) {
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ function init(ctx) {
|
|||||||
$("btn-add-token-confirm").addEventListener("click", async () => {
|
$("btn-add-token-confirm").addEventListener("click", async () => {
|
||||||
const contractAddr = $("add-token-address").value.trim();
|
const contractAddr = $("add-token-address").value.trim();
|
||||||
if (!contractAddr || !contractAddr.startsWith("0x")) {
|
if (!contractAddr || !contractAddr.startsWith("0x")) {
|
||||||
showFlash(
|
showFlash("Enter a valid contract address starting with 0x.");
|
||||||
"Please enter a valid contract address starting with 0x.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const already = state.trackedTokens.find(
|
const already = state.trackedTokens.find(
|
||||||
|
|||||||
@@ -142,15 +142,13 @@ function validatePassword() {
|
|||||||
async function importMnemonic(ctx) {
|
async function importMnemonic(ctx) {
|
||||||
const mnemonic = $("wallet-mnemonic").value.trim();
|
const mnemonic = $("wallet-mnemonic").value.trim();
|
||||||
if (!mnemonic) {
|
if (!mnemonic) {
|
||||||
showFlash("Enter a recovery phrase or press the die to generate one.");
|
showFlash("Enter a recovery phrase, or press the die.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const words = mnemonic.split(/\s+/);
|
const words = mnemonic.split(/\s+/);
|
||||||
if (words.length !== 12 && words.length !== 24) {
|
if (words.length !== 12 && words.length !== 24) {
|
||||||
showFlash(
|
showFlash(
|
||||||
"Recovery phrase must be 12 or 24 words. You entered " +
|
"Recovery phrase must be 12 or 24 words, not " + words.length + ".",
|
||||||
words.length +
|
|
||||||
".",
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -163,14 +161,12 @@ async function importMnemonic(ctx) {
|
|||||||
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
||||||
const xpubDup = findWalletByXpub(xpub);
|
const xpubDup = findWalletByXpub(xpub);
|
||||||
if (xpubDup) {
|
if (xpubDup) {
|
||||||
showFlash(
|
showFlash("This recovery phrase is already added.");
|
||||||
"This recovery phrase is already added (" + xpubDup.name + ").",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const addrDup = findWalletByAddress(firstAddress);
|
const addrDup = findWalletByAddress(firstAddress);
|
||||||
if (addrDup) {
|
if (addrDup) {
|
||||||
showFlash("Address already exists in wallet (" + addrDup.name + ").");
|
showFlash("Address already exists in a wallet.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const encrypted = await encryptWithPassword(mnemonic, pw);
|
const encrypted = await encryptWithPassword(mnemonic, pw);
|
||||||
@@ -229,9 +225,7 @@ async function importPrivateKey(ctx) {
|
|||||||
if (!pw) return;
|
if (!pw) return;
|
||||||
const duplicate = findWalletByAddress(addr);
|
const duplicate = findWalletByAddress(addr);
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
showFlash(
|
showFlash("This address already exists in a wallet.");
|
||||||
"This address already exists in wallet (" + duplicate.name + ").",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const encrypted = await encryptWithPassword(key, pw);
|
const encrypted = await encryptWithPassword(key, pw);
|
||||||
@@ -258,36 +252,29 @@ async function importXprvKey(ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isValidXprv(xprv)) {
|
if (!isValidXprv(xprv)) {
|
||||||
showFlash(
|
showFlash("That extended private key is not valid.");
|
||||||
"That extended private key is not valid. Please check it and try again.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isMasterExtendedKey(xprv)) {
|
if (!isMasterExtendedKey(xprv)) {
|
||||||
showFlash(
|
showFlash("Please paste the master key, not a child key.");
|
||||||
"That is an account-level or child key, which cannot be imported. " +
|
|
||||||
"Please paste the master extended private key for the wallet.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = hdWalletFromXprv(xprv);
|
result = hdWalletFromXprv(xprv);
|
||||||
} catch {
|
} catch {
|
||||||
showFlash(
|
showFlash("That extended private key is not valid.");
|
||||||
"That extended private key is not valid. Please check it and try again.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { xpub, firstAddress } = result;
|
const { xpub, firstAddress } = result;
|
||||||
const xpubDup = findWalletByXpub(xpub);
|
const xpubDup = findWalletByXpub(xpub);
|
||||||
if (xpubDup) {
|
if (xpubDup) {
|
||||||
showFlash("This key is already added (" + xpubDup.name + ").");
|
showFlash("This key is already added.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const addrDup = findWalletByAddress(firstAddress);
|
const addrDup = findWalletByAddress(firstAddress);
|
||||||
if (addrDup) {
|
if (addrDup) {
|
||||||
showFlash("Address already exists in wallet (" + addrDup.name + ").");
|
showFlash("Address already exists in a wallet.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pw = validatePassword();
|
const pw = validatePassword();
|
||||||
|
|||||||
@@ -225,6 +225,13 @@ function clearFlash() {
|
|||||||
$("flash-msg").textContent = "";
|
$("flash-msg").textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The flash line reserves the height of exactly one line, so a message that
|
||||||
|
// wraps pushes the whole screen below it down, which README's No Layout Shift
|
||||||
|
// rule forbids. Every message must fit on one line of the popup's monospace
|
||||||
|
// font: 50 characters at most, and nothing of unbounded length, such as a
|
||||||
|
// wallet name or text from a server, may be put into one. The longest message
|
||||||
|
// is measured by "the longest flash message fits on one line (#252)" in
|
||||||
|
// tests/e2e/run.js; point that test at any message longer than it.
|
||||||
function showFlash(msg, duration = 2000) {
|
function showFlash(msg, duration = 2000) {
|
||||||
clearFlash();
|
clearFlash();
|
||||||
$("flash-msg").textContent = msg;
|
$("flash-msg").textContent = msg;
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ function validateToAddress(value) {
|
|||||||
if (checksummed !== v) {
|
if (checksummed !== v) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
error: "Address checksum is invalid. Please double-check the address.",
|
error: "Address checksum is invalid. Check the address.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
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 provider = getProvider(state.rpcUrl, state.networkId);
|
||||||
const resolved = await provider.resolveName(to);
|
const resolved = await provider.resolveName(to);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
showFlash("Could not resolve " + to);
|
showFlash("That ENS name has no address.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolvedTo = resolved;
|
resolvedTo = resolved;
|
||||||
|
|||||||
@@ -236,18 +236,12 @@ function init(ctx) {
|
|||||||
const json = await resp.json();
|
const json = await resp.json();
|
||||||
if (json.error) {
|
if (json.error) {
|
||||||
log.errorf("RPC validation error:", json.error);
|
log.errorf("RPC validation error:", json.error);
|
||||||
showFlash("Endpoint returned error: " + json.error.message);
|
showFlash("Endpoint returned an error.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const net = currentNetwork();
|
const net = currentNetwork();
|
||||||
if (json.result !== net.chainId) {
|
if (json.result !== net.chainId) {
|
||||||
showFlash(
|
showFlash("Wrong network: expected " + net.name + ".");
|
||||||
"Wrong network (expected " +
|
|
||||||
net.name +
|
|
||||||
", got chain " +
|
|
||||||
json.result +
|
|
||||||
").",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -115,9 +115,7 @@ function init(_ctx) {
|
|||||||
$("btn-settings-addtoken-manual").addEventListener("click", async () => {
|
$("btn-settings-addtoken-manual").addEventListener("click", async () => {
|
||||||
const addr = $("settings-addtoken-address").value.trim();
|
const addr = $("settings-addtoken-address").value.trim();
|
||||||
if (!addr || !addr.startsWith("0x")) {
|
if (!addr || !addr.startsWith("0x")) {
|
||||||
showFlash(
|
showFlash("Enter a valid contract address starting with 0x.");
|
||||||
"Please enter a valid contract address starting with 0x.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isTracked(addr)) {
|
if (isTracked(addr)) {
|
||||||
|
|||||||
@@ -41,12 +41,10 @@ const DEFECTS = {
|
|||||||
"changed or removed, and this wallet stays until you delete " +
|
"changed or removed, and this wallet stays until you delete " +
|
||||||
"it yourself.",
|
"it yourself.",
|
||||||
],
|
],
|
||||||
// One sentence for the places that have room for one: the flash on a
|
// One line, for the flash on a blocked Send and the inline error on
|
||||||
// blocked Send, the inline error on the approval screens.
|
// the approval screens. It must fit on the flash line; see showFlash()
|
||||||
shortMessage:
|
// in src/popup/views/helpers.js.
|
||||||
"This wallet cannot sign, because it was imported from an " +
|
shortMessage: "This wallet cannot sign. See the wallet list.",
|
||||||
"extended private key that is not a master key. The wallet list " +
|
|
||||||
"explains what happened.",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1400,6 +1400,63 @@ test("a rejected dust threshold shifts no layout (#233)", async (env) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------------ the flash line (#252)
|
||||||
|
|
||||||
|
// Every flash message must fit the one line #flash-msg reserves (see
|
||||||
|
// showFlash() in src/popup/views/helpers.js). This drives the longest one
|
||||||
|
// and measures the line's height with it.
|
||||||
|
//
|
||||||
|
// The line is measured in the monospace font the popup declares. Firefox
|
||||||
|
// draws the popup in it; Chromium draws it in the system font instead
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/418), which is narrower, so a
|
||||||
|
// message that wraps in Firefox would still fit here and the test would pass.
|
||||||
|
test("the longest flash message fits on one line (#252)", async (env) => {
|
||||||
|
const page = await openPopup(env.ctx, env.popupUrl);
|
||||||
|
try {
|
||||||
|
await page.setViewportSize(POPUP_VIEWPORT);
|
||||||
|
await openSettings(page);
|
||||||
|
await page.click("#btn-settings-add-token");
|
||||||
|
await visible(page, "#view-settings-addtoken");
|
||||||
|
|
||||||
|
const font = await page.evaluate(() => {
|
||||||
|
const line = document.getElementById("flash-msg");
|
||||||
|
line.style.fontFamily = "var(--font-mono)";
|
||||||
|
return getComputedStyle(line).fontFamily;
|
||||||
|
});
|
||||||
|
assert(
|
||||||
|
font.includes("monospace"),
|
||||||
|
"the flash line is not in the monospace font: " + font,
|
||||||
|
);
|
||||||
|
|
||||||
|
const before = await page.evaluate(measureFlashLine);
|
||||||
|
await page.fill("#settings-addtoken-address", "not an address");
|
||||||
|
await page.click("#btn-settings-addtoken-manual");
|
||||||
|
const after = await waitForFilledFlashLine(page);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
after.flashHeight === before.flashHeight,
|
||||||
|
"the flash line is " +
|
||||||
|
before.flashHeight +
|
||||||
|
"px empty and " +
|
||||||
|
after.flashHeight +
|
||||||
|
"px with " +
|
||||||
|
JSON.stringify(after.text) +
|
||||||
|
", so the message wraps",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
after.text === "Enter a valid contract address starting with 0x.",
|
||||||
|
"the screen flashed " +
|
||||||
|
JSON.stringify(after.text) +
|
||||||
|
", not the message this test measures",
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.click("#btn-settings-addtoken-back");
|
||||||
|
await visible(page, "#view-settings");
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --------------------------------------------- confirmation screen (#238)
|
// --------------------------------------------- confirmation screen (#238)
|
||||||
//
|
//
|
||||||
// The screen that decides what gets signed. The arithmetic underneath it
|
// The screen that decides what gets signed. The arithmetic underneath it
|
||||||
|
|||||||
Reference in New Issue
Block a user