commit cff6812467590c42d7ffd62ff42c8cbe49624af7
parent bb394607c19d28513b93074216de5ae93fb7af6f
Author: mailelucks <maile.lucks@gmail.com>
Date: Sat, 22 Nov 2025 13:47:04 +0000
Bug 1996184 - Remove single `this.isInvalid` that was being used to track both origin and password validity r=mtigley,credential-management-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D270884
Diffstat:
3 files changed, 141 insertions(+), 11 deletions(-)
diff --git a/browser/components/aboutlogins/content/components/input-field/login-password-field.mjs b/browser/components/aboutlogins/content/components/input-field/login-password-field.mjs
@@ -41,7 +41,11 @@ class LoginPasswordField extends MozLitElement {
}
get #password() {
- return !this.newPassword && this.concealed && this.value
+ if (!this.value) {
+ return "";
+ }
+
+ return !this.newPassword && this.concealed
? LoginPasswordField.CONCEALED_PASSWORD_TEXT
: this.value;
}
diff --git a/toolkit/components/satchel/megalist/content/components/login-form/login-form.mjs b/toolkit/components/satchel/megalist/content/components/login-form/login-form.mjs
@@ -27,6 +27,8 @@ export class LoginForm extends MozLitElement {
passwordValue: { type: String },
passwordVisible: { type: Boolean },
_showDeleteCard: { type: Boolean, state: true },
+ _originInvalid: { type: Boolean, state: true },
+ _passwordInvalid: { type: Boolean, state: true },
};
static queries = {
@@ -43,8 +45,9 @@ export class LoginForm extends MozLitElement {
this.originValue = "";
this.usernameValue = "";
this.passwordValue = "";
- this.isInvalid = false;
this._showDeleteCard = false;
+ this._originInvalid = false;
+ this._passwordInvalid = false;
}
async firstUpdated() {
@@ -97,7 +100,12 @@ export class LoginForm extends MozLitElement {
}
#shouldShowWarning(field, input, warning) {
- if (!input.checkValidity() || this.isInvalid) {
+ const fieldInvalid =
+ warning === this.originWarning
+ ? this._originInvalid
+ : this._passwordInvalid;
+
+ if (!input.checkValidity() || fieldInvalid) {
// FIXME: for some reason checkValidity does not apply the :invalid style
// to the field. For now, we reset the input value to "" apply :invalid
// styling.
@@ -107,11 +115,21 @@ export class LoginForm extends MozLitElement {
warning.setAttribute("message", input.validationMessage);
warning.classList.add("invalid-input");
field.setAttribute("aria-describedby", warning.id);
+ if (warning === this.originWarning) {
+ this._originInvalid = true;
+ } else if (warning === this.passwordWarning) {
+ this._passwordInvalid = true;
+ }
return true;
}
field.removeAttribute("aria-describedby");
this.#removeWarning(warning);
+ if (warning === this.originWarning) {
+ this._originInvalid = false;
+ } else if (warning === this.passwordWarning) {
+ this._passwordInvalid = false;
+ }
return false;
}
@@ -119,13 +137,18 @@ export class LoginForm extends MozLitElement {
const field = e.target;
const warning =
field.name === "origin" ? this.originWarning : this.passwordWarning;
+ const isValid = field.input.checkValidity();
- if (field.input.checkValidity()) {
+ if (isValid) {
this.#removeWarning(warning);
field.removeAttribute("aria-describedby");
}
- this.isInvalid = !field.input.checkValidity();
+ if (field.name === "origin") {
+ this._originInvalid = !isValid;
+ } else if (field.name === "password") {
+ this._passwordInvalid = !isValid;
+ }
}
onCancel(e) {
diff --git a/toolkit/components/satchel/megalist/content/tests/browser/browser_passwords_create_login.js b/toolkit/components/satchel/megalist/content/tests/browser/browser_passwords_create_login.js
@@ -60,7 +60,10 @@ function waitForPopup(megalist, element) {
info(`Wait for ${element} popup`);
const loginForm = megalist.querySelector("login-form");
const popupPromise = BrowserTestUtils.waitForCondition(
- () => loginForm.shadowRoot.querySelector(`${element}`),
+ () =>
+ loginForm.shadowRoot
+ .querySelector(`${element}`)
+ .classList.contains("invalid-input"),
`${element} popup did not render.`
);
return popupPromise;
@@ -166,24 +169,124 @@ add_task(async function test_add_login_empty_origin() {
SidebarController.hide();
});
-add_task(async function test_add_login_empty_password() {
+add_task(async function test_add_login_empty_password_and_resubmit() {
const megalist = await openPasswordsSidebar();
await waitForSnapshots();
await openLoginForm(megalist, false);
- addLogin(megalist, {
- ...TEST_LOGIN_1,
- password: "",
- });
+ const loginForm = megalist.querySelector("login-form");
+ setInputValue(loginForm, "login-origin-field", TEST_LOGIN_1.origin);
+ setInputValue(loginForm, "login-username-field", TEST_LOGIN_1.username);
+ setInputValue(loginForm, "login-password-field", "");
+
+ const saveButton = loginForm.shadowRoot.querySelector(
+ "moz-button[type=primary]"
+ );
+ info("Submitting empty password once.");
+ saveButton.buttonEl.click();
+
+ await waitForPopup(megalist, "password-warning");
+ ok(
+ !loginForm.shadowRoot
+ .querySelector("origin-warning")
+ .classList.contains("invalid-input"),
+ "Origin field should not be marked invalid when only the password is empty."
+ );
+ info("Submitting empty password a second time without changing value.");
+ saveButton.buttonEl.click();
await waitForPopup(megalist, "password-warning");
+ ok(
+ loginForm.isConnected,
+ "Login form remains open after repeated invalid submissions."
+ );
+
const logins = await Services.logins.getAllLogins();
is(logins.length, 0, "No login was added after submitting form.");
+ info("Entering a valid password clears the warning.");
+ setInputValue(loginForm, "login-password-field", TEST_LOGIN_1.password);
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ !loginForm.shadowRoot
+ .querySelector("password-warning")
+ .classList.contains("invalid-input"),
+ "Password warning should be removed after entering a valid password."
+ );
+
LoginTestUtils.clearData();
info("Closing the sidebar");
SidebarController.hide();
});
+add_task(async function test_edit_login_empty_password_requires_new_value() {
+ const login = TEST_LOGIN_1;
+ await LoginTestUtils.addLogin(login);
+
+ const megalist = await openPasswordsSidebar();
+ await checkAllLoginsRendered(megalist);
+
+ const passwordCard = megalist.querySelector("password-card");
+ await waitForReauth(() => passwordCard.editBtn.click());
+ await BrowserTestUtils.waitForCondition(
+ () => megalist.querySelector("login-form"),
+ "Login form failed to render in edit mode."
+ );
+
+ let loginForm = megalist.querySelector("login-form");
+ setInputValue(loginForm, "login-password-field", "");
+
+ const saveButton = loginForm.shadowRoot.querySelector(
+ "moz-button[type=primary]"
+ );
+ info("Submitting edit form with empty password.");
+ saveButton.buttonEl.click();
+ await waitForPopup(megalist, "password-warning");
+
+ const passwordField = loginForm.shadowRoot.querySelector(
+ "login-password-field"
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => passwordField.input.value === "",
+ "Password input should remain empty after an invalid submission."
+ );
+ is(passwordField.value, "", "Password component state cleared.");
+
+ info("Trying to save again after the first error.");
+ saveButton.buttonEl.click();
+ await BrowserTestUtils.waitForCondition(() => {
+ const form = megalist.querySelector("login-form");
+ return (
+ form &&
+ form.shadowRoot
+ .querySelector("password-warning")
+ .classList.contains("invalid-input")
+ );
+ }, "Password warning should persist after repeated invalid edits.");
+
+ loginForm = megalist.querySelector("login-form");
+ ok(loginForm, "Login form remains open after invalid edit submissions.");
+
+ const logins = await Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Stored login count unchanged.");
+ Assert.equal(
+ logins[0].password,
+ login.password,
+ "Existing login password not overwritten with empty value."
+ );
+
+ LoginTestUtils.clearData();
+ info("Closing the sidebar");
+ SidebarController.hide();
+ const closeWithoutSavingButton = await BrowserTestUtils.waitForCondition(() =>
+ megalist
+ .querySelector("notification-message-bar")
+ ?.shadowRoot.querySelector("moz-message-bar")
+ ?.querySelector("#primary-action")
+ );
+ info("Closing without saving");
+ closeWithoutSavingButton.buttonEl.click();
+});
+
add_task(async function test_view_login_command() {
Services.fog.testResetFOG();
await Services.fog.testFlushAllChildren();