fix: show every password error in its own fixed-height line (closes #493)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

The add wallet screen reported a missing, short or mismatched password in
the flash line at the top of the popup, and the private key export,
recovery phrase and delete wallet screens each wrote to a line of their own
above the password field. All four now use showError() and hideError() with
a fixed-height error line below the field, as the send confirmation and
approval screens do. On the add wallet screen the line sits beside the
Import button, which keeps its place at 360x600. The line clears when the
screen is shown again and when the password is tried again. Other add
wallet messages stay in the flash line.

Model: opus-5-5
This commit was merged in pull request #496.
This commit is contained in:
2026-10-07 10:59:16 +02:00
parent 29ba54d5b6
commit 447d714313
12 changed files with 386 additions and 79 deletions
+11 -8
View File
@@ -1444,14 +1444,17 @@ view would leave a wallet one click from deletion.
without it, the lost-password route on DeleteWallet is the first they without it, the lost-password route on DeleteWallet is the first they
would hear of it. The hint line reserves its height, so switching tabs would hear of it. The hint line reserves its height, so switching tabs
cannot move the password fields under the pointer. cannot move the password fields under the pointer.
- "Import" button - "Import" button, with the error line beside it so that it adds no height
to a screen whose button already starts near the bottom of the popup
- **Transitions**: - **Transitions**:
- "Import" with a valid entry and a matching password of at least 12 - "Import" with a valid entry and a matching password of at least 12
characters → creates the wallet, clears the navigation stack, and → characters → creates the wallet, clears the navigation stack, and →
**Home**. The phrase and xprv modes then scan for further used addresses **Home**. The phrase and xprv modes then scan for further used addresses
and report the count as a flash message. and report the count as a flash message.
- "Import" with an invalid entry, a duplicate wallet or address, or a short - "Import" with a missing, short or mismatched password → full-sentence
or mismatched password → flash message, no screen change error on the error line, no screen change
- "Import" with an invalid entry or a duplicate wallet or address → flash
message, no screen change
- "Back" → previous screen (Welcome, Home, or Settings) - "Back" → previous screen (Welcome, Home, or Settings)
#### AddressDetail (`address`) #### AddressDetail (`address`)
@@ -1490,8 +1493,8 @@ view would leave a wallet one click from deletion.
copy) copy)
- Warning that anyone holding the private key can transfer all funds from - Warning that anyone holding the private key can transfer all funds from
the address the address
- Error line - Password input, error line and "Reveal" button, shown until the key is
- Password input and "Reveal" button, shown until the key is revealed revealed
- The private key on a highlighted background, tap to copy, shown only after - The private key on a highlighted background, tap to copy, shown only after
the password has been accepted the password has been accepted
- **Transitions**: - **Transitions**:
@@ -1829,8 +1832,8 @@ view would leave a wallet one click from deletion.
- Wallet name - Wallet name
- Warning box stating that anyone holding these words can take everything in - Warning box stating that anyone holding these words can take everything in
the wallet, from any device, without the password the wallet, from any device, without the password
- Error line - Password input, error line and "Reveal" button, shown until the password
- Password input + "Reveal" button, shown until the password is accepted is accepted
- The recovery phrase itself, in full and click-to-copy, shown only after a - The recovery phrase itself, in full and click-to-copy, shown only after a
correct password and in place of the password prompt correct password and in place of the password prompt
- **Transitions**: - **Transitions**:
@@ -1859,8 +1862,8 @@ view would leave a wallet one click from deletion.
- "Back" button, "Delete Wallet" heading - "Back" button, "Delete Wallet" heading
- Warning naming the wallet and stating that deletion is permanent and any - Warning naming the wallet and stating that deletion is permanent and any
funds are unrecoverable without the recovery phrase funds are unrecoverable without the recovery phrase
- Error line
- Password input - Password input
- Error line
- "Confirm Delete" button - "Confirm Delete" button
- An underlined "I have lost my password" control - An underlined "I have lost my password" control
- **Transitions**: - **Transitions**:
+13
View File
@@ -44,6 +44,19 @@ then continue tagging as milestones land.
# Completed Steps # Completed Steps
- 2026-10-07: Every password error in the popup is shown the same way
([#493](https://git.eeqj.de/sneak/AutistMask/issues/493)): with `showError()`
and `hideError()` in a fixed-height error line below the password field, as
the send confirmation and approval screens already did. The add wallet screen
showed a missing, short or mismatched password in the flash line, and the
private key export, recovery phrase and delete wallet screens each had a line
of their own above the field. On the add wallet screen the line sits beside
the Import button, so the button stays where it was at 360x600. Each line
clears when the screen is shown again and when the password is tried again.
The add wallet screen's other messages, such as an invalid recovery phrase, a
duplicate wallet and the address scan, stay in the flash line.
`tests/passwordErrorLines.test.js` drives all four screens in the popup.
- 2026-10-07: Pre-1.0 security review of the extension - 2026-10-07: Pre-1.0 security review of the extension
([#383](https://git.eeqj.de/sneak/AutistMask/issues/383)), reading the tree at ([#383](https://git.eeqj.de/sneak/AutistMask/issues/383)), reading the tree at
`99292b9` for key handling, the DEBUG mode policy, and what the background `99292b9` for key handling, the DEBUG mode policy, and what the background
+30 -20
View File
@@ -207,12 +207,22 @@
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
/> />
</div> </div>
<button <!-- The error line sits beside Import, not above it: at
id="btn-add-wallet-confirm" 360x600 the button already starts near the bottom of
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" the popup, and a line of its own would push it below
> the fold. The longest error fits on one line here. -->
Import <div class="flex items-center gap-2">
</button> <button
id="btn-add-wallet-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Import
</button>
<div
id="add-wallet-password-error"
class="text-xs min-h-[1.25rem] invisible"
></div>
</div>
</div> </div>
<!-- ============ MAIN VIEW: ALL WALLETS & ADDRESSES ============ --> <!-- ============ MAIN VIEW: ALL WALLETS & ADDRESSES ============ -->
@@ -396,10 +406,6 @@
Warning: anyone with this private key can access and Warning: anyone with this private key can access and
transfer all funds from this address. Never share it. transfer all funds from this address. Never share it.
</p> </p>
<div
id="export-privkey-flash"
class="text-xs mb-2 min-h-[1.25rem] invisible"
></div>
<div id="export-privkey-password-section" class="mb-2"> <div id="export-privkey-password-section" class="mb-2">
<label class="block mb-1">Password</label> <label class="block mb-1">Password</label>
<input <input
@@ -408,9 +414,13 @@
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="Enter your password to continue" placeholder="Enter your password to continue"
/> />
<div
id="export-privkey-password-error"
class="text-xs mt-2 mb-2 min-h-[1.25rem] invisible"
></div>
<button <button
id="btn-export-privkey-confirm" id="btn-export-privkey-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mt-2" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
Reveal Reveal
</button> </button>
@@ -1116,10 +1126,6 @@
<strong id="delete-wallet-name"></strong> is permanent. Any <strong id="delete-wallet-name"></strong> is permanent. Any
funds will be unrecoverable without your recovery phrase. funds will be unrecoverable without your recovery phrase.
</p> </p>
<div
id="delete-wallet-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1">Password</label> <label class="block mb-1">Password</label>
<input <input
@@ -1129,6 +1135,10 @@
placeholder="Enter your password to confirm" placeholder="Enter your password to confirm"
/> />
</div> </div>
<div
id="delete-wallet-password-error"
class="text-xs mb-2 min-h-[1.25rem] invisible"
></div>
<button <button
id="btn-delete-wallet-confirm" id="btn-delete-wallet-confirm"
class="border border-border text-red-500 px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border text-red-500 px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
@@ -1273,10 +1283,6 @@
this wallet, from any device, without your password. Never this wallet, from any device, without your password. Never
type them into a website and never show them to anyone. type them into a website and never show them to anyone.
</div> </div>
<div
id="show-phrase-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<div id="show-phrase-password-section" class="mb-2"> <div id="show-phrase-password-section" class="mb-2">
<label class="block mb-1">Password</label> <label class="block mb-1">Password</label>
<input <input
@@ -1285,9 +1291,13 @@
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="Enter your password to continue" placeholder="Enter your password to continue"
/> />
<div
id="show-phrase-password-error"
class="text-xs mt-2 mb-2 min-h-[1.25rem] invisible"
></div>
<button <button
id="btn-show-phrase-reveal" id="btn-show-phrase-reveal"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mt-2" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
Reveal Reveal
</button> </button>
+12 -4
View File
@@ -2,6 +2,8 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
showError,
hideError,
goBack, goBack,
clearViewStack, clearViewStack,
onViewLeave, onViewLeave,
@@ -98,6 +100,7 @@ function clear() {
$("add-wallet-password").value = ""; $("add-wallet-password").value = "";
$("add-wallet-password-confirm").value = ""; $("add-wallet-password-confirm").value = "";
$("add-wallet-phrase-warning").style.visibility = "hidden"; $("add-wallet-phrase-warning").style.visibility = "hidden";
hideError("add-wallet-password-error");
} }
// Each wallet has its own password (its own encryptedSecret), so adding a // Each wallet has its own password (its own encryptedSecret), so adding a
@@ -125,15 +128,18 @@ function validatePassword() {
const pw = $("add-wallet-password").value; const pw = $("add-wallet-password").value;
const pw2 = $("add-wallet-password-confirm").value; const pw2 = $("add-wallet-password-confirm").value;
if (!pw) { if (!pw) {
showFlash("Please choose a password."); showError("add-wallet-password-error", "Please choose a password.");
return null; return null;
} }
if (pw.length < 12) { if (pw.length < 12) {
showFlash("Password must be at least 12 characters."); showError(
"add-wallet-password-error",
"Password must be at least 12 characters.",
);
return null; return null;
} }
if (pw !== pw2) { if (pw !== pw2) {
showFlash("Passwords do not match."); showError("add-wallet-password-error", "Passwords do not match.");
return null; return null;
} }
return pw; return pw;
@@ -342,8 +348,10 @@ function init(ctx) {
$("add-wallet-phrase-warning").style.visibility = "visible"; $("add-wallet-phrase-warning").style.visibility = "visible";
}); });
// Import / confirm // Import / confirm. Each press starts with no password error on screen:
// validatePassword() puts it back if the password is still wrong.
$("btn-add-wallet-confirm").addEventListener("click", async () => { $("btn-add-wallet-confirm").addEventListener("click", async () => {
hideError("add-wallet-password-error");
if (currentMode === "mnemonic") { if (currentMode === "mnemonic") {
await importMnemonic(ctx); await importMnemonic(ctx);
} else if (currentMode === "privkey") { } else if (currentMode === "privkey") {
+16 -11
View File
@@ -2,6 +2,8 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
showError,
hideError,
goBack, goBack,
clearViewStack, clearViewStack,
onViewLeave, onViewLeave,
@@ -55,8 +57,7 @@ function confirmKey(name) {
function clear() { function clear() {
deleteWalletIndex = null; deleteWalletIndex = null;
$("delete-wallet-password").value = ""; $("delete-wallet-password").value = "";
$("delete-wallet-flash").textContent = ""; hideError("delete-wallet-password-error");
$("delete-wallet-flash").style.visibility = "hidden";
} }
// The lost-password screen holds no secret — a wallet name is not one — // The lost-password screen holds no secret — a wallet name is not one —
@@ -232,19 +233,22 @@ function init(_ctx) {
$("btn-delete-wallet-confirm").addEventListener("click", async () => { $("btn-delete-wallet-confirm").addEventListener("click", async () => {
const pw = $("delete-wallet-password").value; const pw = $("delete-wallet-password").value;
if (!pw) { if (!pw) {
$("delete-wallet-flash").textContent = showError(
"Please enter your password."; "delete-wallet-password-error",
$("delete-wallet-flash").style.visibility = "visible"; "Please enter your password.",
);
return; return;
} }
if (deleteWalletIndex === null) { if (deleteWalletIndex === null) {
$("delete-wallet-flash").textContent = showError(
"No wallet selected for deletion."; "delete-wallet-password-error",
$("delete-wallet-flash").style.visibility = "visible"; "No wallet selected for deletion.",
);
return; return;
} }
hideError("delete-wallet-password-error");
const btn = $("btn-delete-wallet-confirm"); const btn = $("btn-delete-wallet-confirm");
btn.disabled = true; btn.disabled = true;
btn.classList.add("text-muted"); btn.classList.add("text-muted");
@@ -256,9 +260,10 @@ function init(_ctx) {
try { try {
await decryptWithPassword(wallet.encryptedSecret, pw); await decryptWithPassword(wallet.encryptedSecret, pw);
} catch { } catch {
$("delete-wallet-flash").textContent = showError(
"That password is incorrect. Please try again."; "delete-wallet-password-error",
$("delete-wallet-flash").style.visibility = "visible"; "That password is incorrect. Please try again.",
);
btn.disabled = false; btn.disabled = false;
btn.classList.remove("text-muted"); btn.classList.remove("text-muted");
return; return;
+13 -12
View File
@@ -20,6 +20,8 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
showError,
hideError,
flashCopyFeedback, flashCopyFeedback,
goBack, goBack,
onViewLeave, onViewLeave,
@@ -56,11 +58,6 @@ function isCurrentReveal(generation) {
); );
} }
function fail(message) {
$("export-privkey-flash").textContent = message;
$("export-privkey-flash").style.visibility = "visible";
}
// Wipe every trace of the key and drop the address selection. Safe to call // Wipe every trace of the key and drop the address selection. Safe to call
// when nothing was ever revealed, and safe to call twice. // when nothing was ever revealed, and safe to call twice.
function clear() { function clear() {
@@ -71,8 +68,7 @@ function clear() {
$("export-privkey-password").value = ""; $("export-privkey-password").value = "";
$("export-privkey-result").classList.add("hidden"); $("export-privkey-result").classList.add("hidden");
$("export-privkey-password-section").classList.remove("hidden"); $("export-privkey-password-section").classList.remove("hidden");
$("export-privkey-flash").textContent = ""; hideError("export-privkey-password-error");
$("export-privkey-flash").style.visibility = "hidden";
} }
function show(walletIdx, addrIdx) { function show(walletIdx, addrIdx) {
@@ -112,15 +108,19 @@ function show(walletIdx, addrIdx) {
async function reveal() { async function reveal() {
const password = $("export-privkey-password").value; const password = $("export-privkey-password").value;
if (!password) { if (!password) {
fail("Please enter your password."); showError(
"export-privkey-password-error",
"Please enter your password.",
);
return; return;
} }
if (walletIndex === null) { if (walletIndex === null) {
fail("No address is selected."); showError("export-privkey-password-error", "No address is selected.");
return; return;
} }
const wallet = state.wallets[walletIndex]; const wallet = state.wallets[walletIndex];
hideError("export-privkey-password-error");
const btn = $("btn-export-privkey-confirm"); const btn = $("btn-export-privkey-confirm");
btn.disabled = true; btn.disabled = true;
btn.classList.add("text-muted"); btn.classList.add("text-muted");
@@ -140,11 +140,12 @@ async function reveal() {
$("export-privkey-password-section").classList.add("hidden"); $("export-privkey-password-section").classList.add("hidden");
$("export-privkey-value").textContent = signer.privateKey; $("export-privkey-value").textContent = signer.privateKey;
$("export-privkey-result").classList.remove("hidden"); $("export-privkey-result").classList.remove("hidden");
$("export-privkey-flash").textContent = "";
$("export-privkey-flash").style.visibility = "hidden";
} catch { } catch {
if (!isCurrentReveal(generation)) return; if (!isCurrentReveal(generation)) return;
fail("That password is incorrect. Please try again."); showError(
"export-privkey-password-error",
"That password is incorrect. Please try again.",
);
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.classList.remove("text-muted"); btn.classList.remove("text-muted");
+14 -13
View File
@@ -21,6 +21,8 @@ const {
$, $,
showView, showView,
showFlash, showFlash,
showError,
hideError,
flashCopyFeedback, flashCopyFeedback,
goBack, goBack,
onViewLeave, onViewLeave,
@@ -52,11 +54,6 @@ function isCurrentReveal(generation) {
); );
} }
function fail(message) {
$("show-phrase-flash").textContent = message;
$("show-phrase-flash").style.visibility = "visible";
}
// Wipe every trace of the phrase and drop the wallet selection. Safe to // Wipe every trace of the phrase and drop the wallet selection. Safe to
// call when nothing was ever revealed, and safe to call twice. // call when nothing was ever revealed, and safe to call twice.
function clear() { function clear() {
@@ -66,8 +63,7 @@ function clear() {
$("show-phrase-password").value = ""; $("show-phrase-password").value = "";
$("show-phrase-result").classList.add("hidden"); $("show-phrase-result").classList.add("hidden");
$("show-phrase-password-section").classList.remove("hidden"); $("show-phrase-password-section").classList.remove("hidden");
$("show-phrase-flash").textContent = ""; hideError("show-phrase-password-error");
$("show-phrase-flash").style.visibility = "hidden";
} }
function show(walletIdx) { function show(walletIdx) {
@@ -90,19 +86,23 @@ function show(walletIdx) {
async function reveal() { async function reveal() {
const password = $("show-phrase-password").value; const password = $("show-phrase-password").value;
if (!password) { if (!password) {
fail("Please enter your password."); showError("show-phrase-password-error", "Please enter your password.");
return; return;
} }
if (walletIndex === null) { if (walletIndex === null) {
fail("No wallet is selected."); showError("show-phrase-password-error", "No wallet is selected.");
return; return;
} }
const wallet = state.wallets[walletIndex]; const wallet = state.wallets[walletIndex];
if (!walletHasRecoveryPhrase(wallet)) { if (!walletHasRecoveryPhrase(wallet)) {
fail("This wallet does not have a recovery phrase."); showError(
"show-phrase-password-error",
"This wallet does not have a recovery phrase.",
);
return; return;
} }
hideError("show-phrase-password-error");
const btn = $("btn-show-phrase-reveal"); const btn = $("btn-show-phrase-reveal");
btn.disabled = true; btn.disabled = true;
btn.classList.add("text-muted"); btn.classList.add("text-muted");
@@ -120,13 +120,14 @@ async function reveal() {
$("show-phrase-password-section").classList.add("hidden"); $("show-phrase-password-section").classList.add("hidden");
$("show-phrase-value").textContent = phrase; $("show-phrase-value").textContent = phrase;
$("show-phrase-result").classList.remove("hidden"); $("show-phrase-result").classList.remove("hidden");
$("show-phrase-flash").textContent = "";
$("show-phrase-flash").style.visibility = "hidden";
} catch { } catch {
if (!isCurrentReveal(generation)) return; if (!isCurrentReveal(generation)) return;
// Deliberately not the caught error: the message is fixed so that // Deliberately not the caught error: the message is fixed so that
// nothing derived from the ciphertext or the attempt can surface. // nothing derived from the ciphertext or the attempt can surface.
fail("That password is incorrect. Please try again."); showError(
"show-phrase-password-error",
"That password is incorrect. Please try again.",
);
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.classList.remove("text-muted"); btn.classList.remove("text-muted");
+2
View File
@@ -39,6 +39,8 @@ jest.doMock("../src/popup/views/helpers", () => ({
$: element, $: element,
showView: () => {}, showView: () => {},
showFlash: () => {}, showFlash: () => {},
showError: () => {},
hideError: () => {},
goBack: () => {}, goBack: () => {},
clearViewStack: () => {}, clearViewStack: () => {},
onViewLeave: () => {}, onViewLeave: () => {},
+1 -1
View File
@@ -253,7 +253,7 @@ describe("reaching the screen", () => {
const { decryptWithPassword } = require("../src/shared/vault"); const { decryptWithPassword } = require("../src/shared/vault");
decryptWithPassword.mockRejectedValue(new Error("nope")); decryptWithPassword.mockRejectedValue(new Error("nope"));
await click("btn-delete-wallet-confirm"); await click("btn-delete-wallet-confirm");
expect(node("delete-wallet-flash").textContent).toBe( expect(node("delete-wallet-password-error").textContent).toBe(
"That password is incorrect. Please try again.", "That password is incorrect. Please try again.",
); );
}); });
+109 -6
View File
@@ -809,7 +809,7 @@ async function secretScreenState(page, view) {
return page.evaluate( return page.evaluate(
(v) => ({ (v) => ({
value: document.getElementById(v + "-value").textContent, value: document.getElementById(v + "-value").textContent,
error: document.getElementById(v + "-flash").textContent, error: document.getElementById(v + "-password-error").textContent,
html: document.getElementById("view-" + v).innerHTML, html: document.getElementById("view-" + v).innerHTML,
resultHidden: document resultHidden: document
.getElementById(v + "-result") .getElementById(v + "-result")
@@ -906,7 +906,8 @@ test("a wrong password reveals nothing (#161)", async (env) => {
await env.page.click("#btn-show-phrase-reveal"); await env.page.click("#btn-show-phrase-reveal");
await env.page.waitForFunction( await env.page.waitForFunction(
() => () =>
document.getElementById("show-phrase-flash").textContent.length > 0, document.getElementById("show-phrase-password-error").textContent
.length > 0,
null, null,
{ timeout: 60000 }, { timeout: 60000 },
); );
@@ -1993,13 +1994,14 @@ test("an over-long flash message keeps to one line (#252)", async (env) => {
// Every screen that asks for a password reserves room for one line of error. // Every screen that asks for a password reserves room for one line of error.
// The two on the dApp approval screens also have a border and padding, which // The two on the dApp approval screens also have a border and padding, which
// that reserved height has to cover too. // that reserved height has to cover too. The add wallet screen's line sits
// beside its button rather than above it, and has its own test below.
const PASSWORD_ERROR_CONTAINERS = [ const PASSWORD_ERROR_CONTAINERS = [
"approve-tx-error", "approve-tx-error",
"approve-sign-error", "approve-sign-error",
"export-privkey-flash", "export-privkey-password-error",
"show-phrase-flash", "show-phrase-password-error",
"delete-wallet-flash", "delete-wallet-password-error",
"confirm-tx-password-error", "confirm-tx-password-error",
]; ];
@@ -2064,6 +2066,107 @@ test("a password error moves nothing on any screen (#297)", async (env) => {
} }
}); });
// ------------------------------------------ add wallet Import button (#493)
// At 360x600 the add wallet screen's Import button already starts near the
// bottom of the popup, so its password error line sits beside the button
// rather than above it. Measures the button and the line empty and again
// filled with the longest error addWallet.js puts there. Runs in the page.
function measureImportButton() {
const button = document.getElementById("btn-add-wallet-confirm");
const line = document.getElementById("add-wallet-password-error");
const measure = () => {
const b = button.getBoundingClientRect();
const l = line.getBoundingClientRect();
return {
buttonTop: b.top + window.scrollY,
buttonBottom: b.bottom + window.scrollY,
lineTop: l.top + window.scrollY,
lineBottom: l.bottom + window.scrollY,
};
};
const empty = measure();
line.textContent = "Password must be at least 12 characters.";
line.style.visibility = "visible";
const filled = measure();
line.textContent = "";
line.style.visibility = "hidden";
return { empty, filled };
}
test("the add wallet password error leaves Import where it was (#493)", async (env) => {
const page = await openPopup(env.ctx, env.popupUrl);
try {
await page.setViewportSize(POPUP_VIEWPORT);
// Brought up by toggling classes, as in the test above. The note
// addWallet.js shows once a wallet exists is the only part of the
// screen that differs between the first wallet and a later one.
await page.evaluate(() => {
const screen = document.getElementById("view-add-wallet");
for (const view of document.querySelectorAll(".view")) {
view.classList.toggle("hidden", view !== screen);
}
});
for (const walletExists of [false, true]) {
await page.evaluate(
(shown) =>
document
.getElementById("add-wallet-separate-password-note")
.classList.toggle("hidden", !shown),
walletExists,
);
for (const tab of ["tab-mnemonic", "tab-privkey", "tab-xprv"]) {
await page.click("#" + tab);
const { empty, filled } =
await page.evaluate(measureImportButton);
const where =
"#" +
tab +
(walletExists
? " with a wallet already added"
: " for the first wallet");
// Printed pass or fail, as the dust threshold test does.
console.log(
"# add wallet Import top, " +
where +
": " +
empty.buttonTop +
"px",
);
for (const m of [empty, filled]) {
assert(
m.lineTop >= m.buttonTop &&
m.lineBottom <= m.buttonBottom,
"the error line on " +
where +
" does not fit beside Import, so it adds height: " +
JSON.stringify(m),
);
}
assert(
filled.buttonTop === empty.buttonTop,
"Import moved " +
(filled.buttonTop - empty.buttonTop) +
"px when the error appeared on " +
where,
);
if (!walletExists) {
assert(
empty.buttonTop < POPUP_VIEWPORT.height,
"Import starts at " +
empty.buttonTop +
"px on " +
where +
", below the fold",
);
}
}
}
} 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
+6 -4
View File
@@ -207,7 +207,7 @@ describe("a decrypt still running when the screen is left", () => {
}); });
// Same hole on the failure path: a wrong-password error written after // Same hole on the failure path: a wrong-password error written after
// the wipe would restore the flash line on a screen the user has left. // the wipe would restore the error line on a screen the user has left.
test("never writes the failure message either", async () => { test("never writes the failure message either", async () => {
const { helpers, vault, exportPrivkey } = load(); const { helpers, vault, exportPrivkey } = load();
exportPrivkey.show(0, 0); exportPrivkey.show(0, 0);
@@ -217,8 +217,10 @@ describe("a decrypt still running when the screen is left", () => {
reveal.reject(new Error("decryption failed")); reveal.reject(new Error("decryption failed"));
await reveal.pending; await reveal.pending;
expect(node("export-privkey-flash").textContent).toBe(""); expect(node("export-privkey-password-error").textContent).toBe("");
expect(node("export-privkey-flash").style.visibility).toBe("hidden"); expect(node("export-privkey-password-error").style.visibility).toBe(
"hidden",
);
}); });
}); });
@@ -260,7 +262,7 @@ describe("a reveal that is not interrupted", () => {
await reveal.pending; await reveal.pending;
expect(node("export-privkey-value").textContent).toBe(""); expect(node("export-privkey-value").textContent).toBe("");
expect(node("export-privkey-flash").textContent).toBe( expect(node("export-privkey-password-error").textContent).toBe(
"That password is incorrect. Please try again.", "That password is incorrect. Please try again.",
); );
}); });
+159
View File
@@ -0,0 +1,159 @@
// Every screen that asks for a password shows a password error the same way:
// through showError() and hideError() in src/popup/views/helpers.js, in a
// fixed-height error line below the password field, and never in the flash
// line at the top of the popup
// (https://git.eeqj.de/sneak/AutistMask/issues/493). These boot the real popup
// over src/popup/index.html, so each error line has to exist in the markup,
// and check that the error appears in it and clears again.
jest.mock("../src/shared/vault", () => ({
decryptWithPassword: jest.fn(),
encryptWithPassword: jest.fn(),
}));
const {
bootPopup,
cleanupPopup,
unversionedValidProfile,
} = require("./support/popupBoot");
const PASSWORD = "correct horse battery staple";
const WRONG_PASSWORD = "That password is incorrect. Please try again.";
afterEach(() => {
cleanupPopup();
});
// The error line as the user sees it.
function errorLine(page, id) {
return {
inMarkup: page.document.authoredIds.has(id),
text: page.text(id),
visibility: page.node(id).style.visibility,
};
}
function shown(text) {
return { inMarkup: true, text, visibility: "visible" };
}
const cleared = { inMarkup: true, text: "", visibility: "hidden" };
describe("the add wallet screen", () => {
const ERROR = "add-wallet-password-error";
// First run: Welcome, "Add wallet", then the die for a valid phrase.
async function openAddWallet() {
const page = await bootPopup(undefined);
await page.click("btn-welcome-add");
await page.click("btn-generate-phrase");
return page;
}
function setPasswords(page, password, confirm) {
page.node("add-wallet-password").value = password;
page.node("add-wallet-password-confirm").value = confirm;
}
test("shows a password problem below the password fields, and clears it on the next press", async () => {
const page = await openAddWallet();
setPasswords(page, "short", "short");
await page.click("btn-add-wallet-confirm");
expect(errorLine(page, ERROR)).toEqual(
shown("Password must be at least 12 characters."),
);
expect(page.text("flash-msg")).toBe("");
// The password is fixed and the phrase emptied: the password error
// goes, and the phrase problem is still reported in the flash line.
setPasswords(page, PASSWORD, PASSWORD);
page.node("wallet-mnemonic").value = "";
await page.click("btn-add-wallet-confirm");
expect(errorLine(page, ERROR)).toEqual(cleared);
expect(page.text("flash-msg")).toBe(
"Enter a recovery phrase, or press the die.",
);
// Leaving clears the flash line and stops its timer, which would
// otherwise fire after this page is gone.
await page.click("btn-add-wallet-back");
});
test("clears the error when the screen is shown again", async () => {
const page = await openAddWallet();
setPasswords(page, PASSWORD, PASSWORD + " typo");
await page.click("btn-add-wallet-confirm");
expect(errorLine(page, ERROR)).toEqual(
shown("Passwords do not match."),
);
await page.click("btn-add-wallet-back");
await page.click("btn-welcome-add");
expect(errorLine(page, ERROR)).toEqual(cleared);
});
});
describe.each([
{
screen: "the private key export screen",
open: () => require("../src/popup/views/exportPrivkey").show(0, 0),
field: "export-privkey-password",
button: "btn-export-privkey-confirm",
error: "export-privkey-password-error",
},
{
screen: "the recovery phrase screen",
open: () => require("../src/popup/views/showPhrase").show(0),
field: "show-phrase-password",
button: "btn-show-phrase-reveal",
error: "show-phrase-password-error",
},
{
screen: "the delete wallet screen",
open: () => require("../src/popup/views/deleteWallet").show(0),
field: "delete-wallet-password",
button: "btn-delete-wallet-confirm",
error: "delete-wallet-password-error",
},
])("$screen", ({ open, field, button, error }) => {
// Opens the screen and enters a password the vault rejects.
async function failedAttempt() {
const page = await bootPopup(unversionedValidProfile());
open();
const { decryptWithPassword } = require("../src/shared/vault");
decryptWithPassword.mockRejectedValue(new Error("wrong password"));
page.node(field).value = "not the password";
await page.click(button);
return { page, decryptWithPassword };
}
test("shows a wrong password below the password field", async () => {
const { page } = await failedAttempt();
expect(errorLine(page, error)).toEqual(shown(WRONG_PASSWORD));
});
test("clears the error while the next password is checked", async () => {
const { page, decryptWithPassword } = await failedAttempt();
let rejectDecrypt;
decryptWithPassword.mockReturnValue(
new Promise((resolve, reject) => {
rejectDecrypt = reject;
}),
);
page.node(field).value = "another guess";
const pressed = page.click(button);
await page.settle();
expect(errorLine(page, error)).toEqual(cleared);
rejectDecrypt(new Error("wrong password"));
await pressed;
expect(errorLine(page, error)).toEqual(shown(WRONG_PASSWORD));
});
test("clears the error when the screen is shown again", async () => {
const { page } = await failedAttempt();
open();
expect(errorLine(page, error)).toEqual(cleared);
});
});