diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js | 1845 |
1 files changed, 1845 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js new file mode 100644 index 0000000000..bbcab81854 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js @@ -0,0 +1,1845 @@ +/** + * Test using the generated passwords produces the right doorhangers/notifications + */ + +/* eslint no-shadow:"off" */ + +"use strict"; + +// The origin for the test URIs. +const TEST_ORIGIN = "https://example.com"; +const FORM_PAGE_PATH = + "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html"; +const passwordInputSelector = "#form-basic-password"; +const usernameInputSelector = "#form-basic-username"; + +requestLongerTimeout(2); + +async function task_setup() { + Services.logins.removeAllUserFacingLogins(); + LoginTestUtils.resetGeneratedPasswordsCache(); + await cleanupPasswordNotifications(); + await LoginTestUtils.remoteSettings.setupImprovedPasswordRules(); +} + +async function setup_withOneLogin(username = "username", password = "pass1") { + // Reset to a single, known login + await task_setup(); + let login = await LoginTestUtils.addLogin({ username, password }); + return login; +} + +async function setup_withNoLogins() { + // Reset to a single, known login + await task_setup(); + Assert.equal( + Services.logins.getAllLogins().length, + 0, + "0 logins at the start of the test" + ); +} + +async function fillGeneratedPasswordFromACPopup( + browser, + passwordInputSelector +) { + let popup = document.getElementById("PopupAutoComplete"); + Assert.ok(popup, "Got popup"); + await openACPopup(popup, browser, passwordInputSelector); + await fillGeneratedPasswordFromOpenACPopup(browser, passwordInputSelector); +} + +async function checkPromptContents( + anchorElement, + browser, + expectedPasswordLength = 0 +) { + let { panel } = PopupNotifications; + Assert.ok(PopupNotifications.isPanelOpen, "Confirm popup is open"); + let notificationElement = panel.childNodes[0]; + if (expectedPasswordLength) { + info( + `Waiting for password value to be ${expectedPasswordLength} chars long` + ); + await BrowserTestUtils.waitForCondition(() => { + return ( + notificationElement.querySelector("#password-notification-password") + .value.length == expectedPasswordLength + ); + }, "Wait for nsLoginManagerPrompter writeDataToUI()"); + } + + return { + passwordValue: notificationElement.querySelector( + "#password-notification-password" + ).value, + usernameValue: notificationElement.querySelector( + "#password-notification-username" + ).value, + }; +} + +async function verifyGeneratedPasswordWasFilled( + browser, + passwordInputSelector +) { + await SpecialPowers.spawn( + browser, + [[passwordInputSelector]], + function checkFinalFieldValue(inputSelector) { + let { LoginTestUtils: LTU } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" + ); + let passwordInput = content.document.querySelector(inputSelector); + Assert.equal( + passwordInput.value.length, + LTU.generation.LENGTH, + "Password field was filled with generated password" + ); + } + ); +} + +async function openFormInNewTab(url, formValues, taskFn) { + let formFilled = listenForTestNotification("FormProcessed"); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + await SimpleTest.promiseFocus(browser.ownerGlobal); + await formFilled; + + await SpecialPowers.spawn( + browser, + [formValues], + async function prepareAndCheckForm({ + password: passwordProps, + username: usernameProps, + }) { + let doc = content.document; + // give the form an action so we can know when submit is complete + doc.querySelector("form").action = "/"; + + let props = passwordProps; + if (props) { + // We'll reuse the form_basic.html, but ensure we'll get the generated password autocomplete option + let field = doc.querySelector(props.selector); + if (props.type) { + // Change the type from 'password' to something else. + field.type = props.type; + } + + field.setAttribute("autocomplete", "new-password"); + if (props.hasOwnProperty("expectedValue")) { + Assert.equal( + field.value, + props.expectedValue, + "Check autofilled password value" + ); + } + } + props = usernameProps; + if (props) { + let field = doc.querySelector(props.selector); + if (props.hasOwnProperty("expectedValue")) { + Assert.equal( + field.value, + props.expectedValue, + "Check autofilled username value" + ); + } + } + } + ); + + if (formValues.password && formValues.password.setValue !== undefined) { + info( + "Editing the password, expectedMessage? " + + formValues.password.expectedMessage + ); + let messagePromise = formValues.password.expectedMessage + ? listenForTestNotification(formValues.password.expectedMessage) + : Promise.resolve(); + await changeContentInputValue( + browser, + formValues.password.selector, + formValues.password.setValue + ); + await messagePromise; + info("messagePromise resolved"); + } + + if (formValues.username && formValues.username.setValue !== undefined) { + info( + "Editing the username, expectedMessage? " + + formValues.username.expectedMessage + ); + let messagePromise = formValues.username.expectedMessage + ? listenForTestNotification(formValues.username.expectedMessage) + : Promise.resolve(); + await changeContentInputValue( + browser, + formValues.username.selector, + formValues.username.setValue + ); + await messagePromise; + info("messagePromise resolved"); + } + + await taskFn(browser); + await closePopup( + browser.ownerDocument.getElementById("confirmation-hint") + ); + } + ); +} + +async function openAndVerifyDoorhanger(browser, type, expected) { + // check a dismissed prompt was shown with extraAttr attribute + let notif = getCaptureDoorhanger(type); + Assert.ok(notif, `${type} doorhanger was created`); + Assert.equal( + notif.dismissed, + expected.dismissed, + "Check notification dismissed property" + ); + Assert.equal( + notif.anchorElement.getAttribute("extraAttr"), + expected.anchorExtraAttr, + "Check icon extraAttr attribute" + ); + let { panel } = PopupNotifications; + // if the doorhanged is dimissed, we will open it to check panel contents + if (panel.state !== "open") { + let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown"); + if (panel.state !== "showing") { + // synthesize click on anchor as this also blurs the form field triggering + // a change event + EventUtils.synthesizeMouseAtCenter(notif.anchorElement, {}); + } + await promiseShown; + } + let { passwordValue, usernameValue } = await checkPromptContents( + notif.anchorElement, + browser, + expected.passwordLength + ); + Assert.equal( + passwordValue.length, + expected.passwordLength || LoginTestUtils.generation.LENGTH, + "Doorhanger password field has generated 15-char value" + ); + Assert.equal( + usernameValue, + expected.usernameValue, + "Doorhanger username field was popuplated" + ); + return notif; +} + +async function appendContentInputvalue(browser, selector, str) { + await ContentTask.spawn( + browser, + { selector, str }, + async function ({ selector, str }) { + const EventUtils = ContentTaskUtils.getEventUtils(content); + let input = content.document.querySelector(selector); + input.focus(); + input.select(); + await EventUtils.synthesizeKey("KEY_ArrowRight", {}, content); + let changedPromise = ContentTaskUtils.waitForEvent(input, "change"); + if (str) { + await EventUtils.sendString(str, content); + } + input.blur(); + await changedPromise; + } + ); + info("Input value changed"); + await TestUtils.waitForTick(); +} + +async function submitForm(browser) { + // Submit the form + info("Now submit the form"); + let correctPathNamePromise = BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn(browser, [], async function () { + content.document.querySelector("form").submit(); + }); + await correctPathNamePromise; + await SpecialPowers.spawn(browser, [], async () => { + let win = content; + await ContentTaskUtils.waitForCondition(() => { + return ( + win.location.pathname == "/" && win.document.readyState == "complete" + ); + }, "Wait for form submission load"); + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ], + }); + // assert that there are no logins + let logins = Services.logins.getAllLogins(); + Assert.equal(logins.length, 0, "There are no logins"); +}); + +add_task(async function autocomplete_generated_password_auto_saved() { + // confirm behavior when filling a generated password via autocomplete + // when there are no other logins + await setup_withNoLogins(); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { selector: passwordInputSelector, expectedValue: "" }, + username: { selector: usernameInputSelector, expectedValue: "" }, + }, + async function taskFn(browser) { + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + // Let the hint hide itself this first time + let forceClosePopup = false; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector); + let [{ username, password }] = await storageChangedPromise; + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + + // Make sure confirmation hint was shown + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + + // Check properties of the newly auto-saved login + Assert.equal(username, "", "Saved login should have no username"); + Assert.equal( + password.length, + LoginTestUtils.generation.LENGTH, + "Saved login should have generated password" + ); + + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, DONT_CHANGE_BUTTON); + await promiseHidden; + + // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger + Assert.ok( + !notif.anchorElement.hasAttribute("extraAttr"), + "Check if the extraAttr attribute was removed" + ); + await cleanupDoorhanger(notif); + + storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + let [autoSavedLogin] = Services.logins.getAllLogins(); + info("waiting for submitForm"); + await submitForm(browser); + await storageChangedPromise; + verifyLogins([ + { + timesUsed: autoSavedLogin.timesUsed + 1, + username: "", + }, + ]); + } + ); +}); + +add_task( + async function autocomplete_generated_password_with_confirm_field_auto_saved() { + // confirm behavior when filling a generated password via autocomplete + // when there are no other logins and the form has a confirm password field + const FORM_WITH_CONFIRM_FIELD_PAGE_PATH = + "/browser/toolkit/components/passwordmgr/test/browser/form_basic_with_confirm_field.html"; + const confirmPasswordInputSelector = "#form-basic-confirm-password"; + await setup_withNoLogins(); + await openFormInNewTab( + TEST_ORIGIN + FORM_WITH_CONFIRM_FIELD_PAGE_PATH, + { + password: { selector: passwordInputSelector, expectedValue: "" }, + username: { selector: usernameInputSelector, expectedValue: "" }, + }, + async function taskFn(browser) { + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + // Let the hint hide itself this first time + let forceClosePopup = false; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector); + let [{ username, password }] = await storageChangedPromise; + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + await verifyGeneratedPasswordWasFilled( + browser, + confirmPasswordInputSelector + ); + + // Make sure confirmation hint was shown + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + + // Check properties of the newly auto-saved login + Assert.equal(username, "", "Saved login should have no username"); + Assert.equal( + password.length, + LoginTestUtils.generation.LENGTH, + "Saved login should have generated password" + ); + + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, DONT_CHANGE_BUTTON); + await promiseHidden; + + // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger + Assert.ok( + !notif.anchorElement.hasAttribute("extraAttr"), + "Check if the extraAttr attribute was removed" + ); + await cleanupDoorhanger(notif); + + storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + let [autoSavedLogin] = Services.logins.getAllLogins(); + info("waiting for submitForm"); + await submitForm(browser); + await storageChangedPromise; + verifyLogins([ + { + timesUsed: autoSavedLogin.timesUsed + 1, + username: "", + }, + ]); + } + ); + } +); + +add_task(async function autocomplete_generated_password_saved_empty_username() { + // confirm behavior when filling a generated password via autocomplete + // when there is an existing saved login with a "" username + await setup_withOneLogin("", "xyzpassword"); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "xyzpassword", + setValue: "", + expectedMessage: "PasswordEditedOrGenerated", + }, + username: { selector: usernameInputSelector, expectedValue: "" }, + }, + async function taskFn(browser) { + let [savedLogin] = Services.logins.getAllLogins(); + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector); + await waitForDoorhanger(browser, "password-change"); + info("Waiting to openAndVerifyDoorhanger"); + await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + await hideDoorhangerPopup(); + info("Waiting to verifyGeneratedPasswordWasFilled"); + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + + info("waiting for submitForm"); + await submitForm(browser); + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: false, + anchorExtraAttr: "", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, CHANGE_BUTTON); + await promiseHidden; + + info("Waiting for modifyLogin"); + await storageChangedPromise; + verifyLogins([ + { + timesUsed: savedLogin.timesUsed + 1, + username: "", + }, + ]); + await cleanupDoorhanger(notif); // cleanup the doorhanger for next test + } + ); +}); + +add_task(async function autocomplete_generated_password_saved_username() { + // confirm behavior when filling a generated password via autocomplete + // into a form with username matching an existing saved login + await setup_withOneLogin("user1", "xyzpassword"); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "xyzpassword", + setValue: "", + expectedMessage: "PasswordEditedOrGenerated", + }, + username: { + selector: usernameInputSelector, + expectedValue: "user1", + }, + }, + async function taskFn(browser) { + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + // We don't need to wait to confirm the hint hides itelf every time + let forceClosePopup = true; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector); + + // Make sure confirmation hint was shown + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + + info("waiting for addLogin"); + await storageChangedPromise; + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + + // Check properties of the newly auto-saved login + let [user1LoginSnapshot, autoSavedLogin] = verifyLogins([ + { + username: "user1", + password: "xyzpassword", // user1 is unchanged + }, + { + timesUsed: 1, + username: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }, + ]); + + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "user1", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, DONT_CHANGE_BUTTON); + await promiseHidden; + + // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger + Assert.ok( + !notif.anchorElement.hasAttribute("extraAttr"), + "Check if the extraAttr attribute was removed" + ); + await cleanupDoorhanger(notif); + + storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + info("waiting for submitForm"); + await submitForm(browser); + promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, CHANGE_BUTTON); + await promiseHidden; + await storageChangedPromise; + verifyLogins([ + { + timesUsed: user1LoginSnapshot.timesUsed + 1, + username: "user1", + password: autoSavedLogin.password, + }, + ]); + } + ); +}); + +add_task(async function ac_gen_pw_saved_empty_un_stored_non_empty_un_in_form() { + // confirm behavior when when the form's username field has a non-empty value + // and there is an existing saved login with a "" username + await setup_withOneLogin("", "xyzpassword"); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "xyzpassword", + setValue: "", + expectedMessage: "PasswordEditedOrGenerated", + }, + username: { + selector: usernameInputSelector, + expectedValue: "", + setValue: "myusername", + // with an empty password value, no message is sent for a username change + expectedMessage: "", + }, + }, + async function taskFn(browser) { + let [savedLogin] = Services.logins.getAllLogins(); + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector); + await waitForDoorhanger(browser, "password-save"); + info("Waiting to openAndVerifyDoorhanger"); + await openAndVerifyDoorhanger(browser, "password-save", { + dismissed: true, + anchorExtraAttr: "", + usernameValue: "myusername", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + await hideDoorhangerPopup(); + info("Waiting to verifyGeneratedPasswordWasFilled"); + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + + info("waiting for submitForm"); + await submitForm(browser); + let notif = await openAndVerifyDoorhanger(browser, "password-save", { + dismissed: false, + anchorExtraAttr: "", + usernameValue: "myusername", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, REMEMBER_BUTTON); + await promiseHidden; + + info("Waiting for addLogin"); + await storageChangedPromise; + verifyLogins([ + { + timesUsed: savedLogin.timesUsed, + username: "", + password: "xyzpassword", + }, + { + timesUsed: 1, + username: "myusername", + }, + ]); + await cleanupDoorhanger(notif); // cleanup the doorhanger for next test + } + ); +}); + +add_task(async function contextfill_generated_password_saved_empty_username() { + // confirm behavior when filling a generated password via context menu + // when there is an existing saved login with a "" username + await setup_withOneLogin("", "xyzpassword"); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "xyzpassword", + setValue: "", + expectedMessage: "PasswordEditedOrGenerated", + }, + username: { selector: usernameInputSelector, expectedValue: "" }, + }, + async function taskFn(browser) { + let [savedLogin] = Services.logins.getAllLogins(); + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + await doFillGeneratedPasswordContextMenuItem( + browser, + passwordInputSelector + ); + await waitForDoorhanger(browser, "password-change"); + info("Waiting to openAndVerifyDoorhanger"); + await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + await hideDoorhangerPopup(); + info("Waiting to verifyGeneratedPasswordWasFilled"); + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + + info("waiting for submitForm"); + await submitForm(browser); + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: false, + anchorExtraAttr: "", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, CHANGE_BUTTON); + await promiseHidden; + + info("Waiting for modifyLogin"); + await storageChangedPromise; + verifyLogins([ + { + timesUsed: savedLogin.timesUsed + 1, + username: "", + }, + ]); + await cleanupDoorhanger(notif); // cleanup the doorhanger for next test + } + ); +}); + +async function autocomplete_generated_password_edited_no_auto_save( + passwordType = "password" +) { + // confirm behavior when filling a generated password via autocomplete + // when there is an existing saved login with a "" username and then editing + // the password and autocompleting again. + await setup_withOneLogin("", "xyzpassword"); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "xyzpassword", + setValue: "", + type: passwordType, + expectedMessage: "PasswordEditedOrGenerated", + }, + username: { selector: usernameInputSelector, expectedValue: "" }, + }, + async function taskFn(browser) { + let [savedLogin] = Services.logins.getAllLogins(); + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector); + info( + "Filled generated password, waiting for dismissed password-change doorhanger" + ); + await waitForDoorhanger(browser, "password-change"); + info("Waiting to openAndVerifyDoorhanger"); + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, DONT_CHANGE_BUTTON); + await promiseHidden; + + info("Waiting to verifyGeneratedPasswordWasFilled"); + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + + await BrowserTestUtils.sendChar("!", browser); + await BrowserTestUtils.sendChar("@", browser); + await BrowserTestUtils.synthesizeKey("KEY_Tab", undefined, browser); + + await waitForDoorhanger(browser, "password-change"); + info("Waiting to openAndVerifyDoorhanger"); + notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH + 2, + }); + + promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, DONT_CHANGE_BUTTON); + await promiseHidden; + + verifyLogins([ + { + timesUsed: savedLogin.timesUsed, + username: "", + password: "xyzpassword", + }, + ]); + + info("waiting for submitForm"); + await submitForm(browser); + notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: false, + anchorExtraAttr: "", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH + 2, + }); + + promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, CHANGE_BUTTON); + await promiseHidden; + + info("Waiting for modifyLogin"); + await storageChangedPromise; + verifyLogins([ + { + timesUsed: savedLogin.timesUsed + 1, + username: "", + }, + ]); + await cleanupDoorhanger(notif); // cleanup the doorhanger for next test + } + ); + + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); +} + +add_task(autocomplete_generated_password_edited_no_auto_save); + +add_task( + async function autocomplete_generated_password_edited_no_auto_save_type_text() { + await autocomplete_generated_password_edited_no_auto_save("text"); + } +); + +add_task(async function contextmenu_fill_generated_password_and_set_username() { + // test when filling with a generated password and editing the username in the form + // * the prompt should display the form's username + // * the auto-saved login should have "" for username + // * confirming the prompt should edit the "" login and add the username + await setup_withOneLogin("olduser", "xyzpassword"); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "xyzpassword", + setValue: "", + expectedMessage: "PasswordEditedOrGenerated", + }, + username: { + selector: usernameInputSelector, + expectedValue: "olduser", + setValue: "differentuser", + // with an empty password value, no message is sent for a username change + expectedMessage: "", + }, + }, + async function taskFn(browser) { + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + await SpecialPowers.spawn( + browser, + [[passwordInputSelector, usernameInputSelector]], + function checkEmptyPasswordField([passwordSelector, usernameSelector]) { + Assert.equal( + content.document.querySelector(passwordSelector).value, + "", + "Password field is empty" + ); + } + ); + + // Let the hint hide itself this first time + let forceClosePopup = false; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + info("waiting to fill generated password using context menu"); + await doFillGeneratedPasswordContextMenuItem( + browser, + passwordInputSelector + ); + + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + info("waiting for dismissed password-change notification"); + await waitForDoorhanger(browser, "password-change"); + + info("waiting for addLogin"); + await storageChangedPromise; + + // Check properties of the newly auto-saved login + verifyLogins([ + null, // ignore the first one + { + timesUsed: 1, + username: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }, + ]); + + info("Waiting to openAndVerifyDoorhanger"); + await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "differentuser", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + await hideDoorhangerPopup(); + info("Waiting to verifyGeneratedPasswordWasFilled"); + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + + info("waiting for submitForm"); + await submitForm(browser); + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: false, + anchorExtraAttr: "", + usernameValue: "differentuser", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + + storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + clickDoorhangerButton(notif, CHANGE_BUTTON); + await promiseHidden; + + info("Waiting for modifyLogin"); + await storageChangedPromise; + verifyLogins([ + null, + { + username: "differentuser", + passwordLength: LoginTestUtils.generation.LENGTH, + timesUsed: 2, + }, + ]); + await cleanupDoorhanger(notif); // cleanup the doorhanger for next test + } + ); +}); + +add_task(async function contextmenu_password_change_form_without_username() { + // test doorhanger behavior when a generated password is filled into a change-password + // form with no username + await setup_withOneLogin("user1", "xyzpassword"); + await LoginTestUtils.addLogin({ username: "username2", password: "pass2" }); + const passwordInputSelector = "#newpass"; + + const CHANGE_FORM_PATH = + "/browser/toolkit/components/passwordmgr/test/browser/form_password_change.html"; + await openFormInNewTab( + TEST_ORIGIN + CHANGE_FORM_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "", + }, + }, + async function taskFn(browser) { + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + + // We don't need to wait to confirm the hint hides itelf every time + let forceClosePopup = true; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + // Make the 2nd field use a generated password + info("Using contextmenu to fill with a generated password"); + await doFillGeneratedPasswordContextMenuItem( + browser, + passwordInputSelector + ); + + info("waiting for dismissed password-change notification"); + await waitForDoorhanger(browser, "password-change"); + + // Make sure confirmation hint was shown + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + + info("waiting for addLogin"); + await storageChangedPromise; + // Check properties of the newly auto-saved login + verifyLogins([ + null, // ignore the first one + null, // ignore the 2nd one + { + timesUsed: 1, + username: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }, + ]); + + info("Waiting to openAndVerifyDoorhanger"); + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }); + // remove notification so we can unambiguously check no new notification gets created later + await cleanupDoorhanger(notif); + + info("Waiting to verifyGeneratedPasswordWasFilled"); + await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector); + + storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + let { timeLastUsed } = Services.logins.getAllLogins()[2]; + + info("waiting for submitForm"); + await submitForm(browser); + + info("Waiting for modifyLogin"); + await storageChangedPromise; + verifyLogins([ + null, // ignore the first one + null, // ignore the 2nd one + { + timesUsed: 2, + usedSince: timeLastUsed, + }, + ]); + // Check no new doorhanger was shown + notif = getCaptureDoorhanger("password-change"); + Assert.ok(!notif, "No new doorhanger should be shown"); + await cleanupDoorhanger(); // cleanup for next test + } + ); +}); + +add_task( + async function autosaved_login_updated_to_existing_login_via_doorhanger() { + // test when filling with a generated password and editing the username in the + // doorhanger to match an existing login: + // * the matching login should be updated + // * the auto-saved login should be deleted + // * the metadata for the matching login should be updated + // * the by-origin cache for the password should point at the updated login + await setup_withOneLogin("user1", "xyzpassword"); + await LoginTestUtils.addLogin({ + username: "user2", + password: "abcpassword", + }); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "", + }, + username: { + selector: usernameInputSelector, + expectedValue: "", + }, + }, + async function taskFn(browser) { + await SimpleTest.promiseFocus(browser.ownerGlobal); + + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + // We don't need to wait to confirm the hint hides itelf every time + let forceClosePopup = true; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + info("waiting to fill generated password using context menu"); + await doFillGeneratedPasswordContextMenuItem( + browser, + passwordInputSelector + ); + + info("waiting for dismissed password-change notification"); + await waitForDoorhanger(browser, "password-change"); + + // Make sure confirmation hint was shown + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + + info("waiting for addLogin"); + await storageChangedPromise; + info("addLogin promise resolved"); + // Check properties of the newly auto-saved login + let [user1LoginSnapshot, unused, autoSavedLogin] = verifyLogins([ + null, // ignore the first one + null, // ignore the 2nd one + { + timesUsed: 1, + username: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }, + ]); + info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid); + info("unused, guid: " + unused.guid); + info("autoSavedLogin, guid: " + autoSavedLogin.guid); + + info("verifyLogins ok"); + let passwordCacheEntry = + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( + "https://example.com" + ); + + Assert.ok( + passwordCacheEntry, + "Got the cached generated password entry for https://example.com" + ); + Assert.equal( + passwordCacheEntry.value, + autoSavedLogin.password, + "Cached password matches the auto-saved login password" + ); + Assert.equal( + passwordCacheEntry.storageGUID, + autoSavedLogin.guid, + "Cached password guid matches the auto-saved login guid" + ); + + info("Waiting to openAndVerifyDoorhanger"); + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "", + password: autoSavedLogin.password, + }); + Assert.ok(notif, "Got password-change notification"); + + info("Calling updateDoorhangerInputValues"); + await updateDoorhangerInputValues({ + username: "user1", + }); + info("doorhanger inputs updated"); + + let loginModifiedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (subject, data) => { + if (data == "modifyLogin") { + info("passwordmgr-storage-changed, action: " + data); + info("subject: " + JSON.stringify(subject)); + return true; + } + return false; + } + ); + let loginRemovedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (subject, data) => { + if (data == "removeLogin") { + info("passwordmgr-storage-changed, action: " + data); + info("subject: " + JSON.stringify(subject)); + return true; + } + return false; + } + ); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + info("clicking change button"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + await promiseHidden; + + info("Waiting for modifyLogin promise"); + await loginModifiedPromise; + + info("Waiting for removeLogin promise"); + await loginRemovedPromise; + + info("storage-change promises resolved"); + // Check the auto-saved login was removed and the original login updated + verifyLogins([ + { + username: "user1", + password: autoSavedLogin.password, + timeCreated: user1LoginSnapshot.timeCreated, + timeLastUsed: user1LoginSnapshot.timeLastUsed, + passwordChangedSince: autoSavedLogin.timePasswordChanged, + }, + null, // ignore user2 + ]); + + // Check we have no notifications at this point + Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open"); + Assert.ok( + !PopupNotifications.getNotification("password", browser), + "No notifications" + ); + + // make sure the cache entry is unchanged with the removal of the auto-saved login + Assert.equal( + autoSavedLogin.password, + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( + "https://example.com" + ).value, + "Generated password cache entry has the expected password value" + ); + } + ); + } +); + +add_task(async function autosaved_login_updated_to_existing_login_onsubmit() { + // test when selecting auto-saved generated password in a form filled with an + // existing login and submitting the form: + // * the matching login should be updated + // * the auto-saved login should be deleted + // * the metadata for the matching login should be updated + // * the by-origin cache for the password should point at the updated login + + // clear both fields which should be autofilled with our single login + await setup_withOneLogin("user1", "xyzpassword"); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "xyzpassword", + setValue: "", + expectedMessage: "PasswordEditedOrGenerated", + }, + username: { + selector: usernameInputSelector, + expectedValue: "user1", + setValue: "", + // with an empty password value, no message is sent for a username change + expectedMessage: "", + }, + }, + async function taskFn(browser) { + await SimpleTest.promiseFocus(browser.ownerGlobal); + + // first, create an auto-saved login with generated password + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + // We don't need to wait to confirm the hint hides itelf every time + let forceClosePopup = true; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + info("waiting to fill generated password using context menu"); + await doFillGeneratedPasswordContextMenuItem( + browser, + passwordInputSelector + ); + + info("waiting for dismissed password-change notification"); + await waitForDoorhanger(browser, "password-change"); + + // Make sure confirmation hint was shown + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + + info("waiting for addLogin"); + await storageChangedPromise; + info("addLogin promise resolved"); + // Check properties of the newly auto-saved login + let [user1LoginSnapshot, autoSavedLogin] = verifyLogins([ + null, // ignore the first one + { + timesUsed: 1, + username: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }, + ]); + info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid); + info("autoSavedLogin, guid: " + autoSavedLogin.guid); + + info("verifyLogins ok"); + let passwordCacheEntry = + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( + "https://example.com" + ); + + Assert.ok( + passwordCacheEntry, + "Got the cached generated password entry for https://example.com" + ); + Assert.equal( + passwordCacheEntry.value, + autoSavedLogin.password, + "Cached password matches the auto-saved login password" + ); + Assert.equal( + passwordCacheEntry.storageGUID, + autoSavedLogin.guid, + "Cached password guid matches the auto-saved login guid" + ); + + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "", + password: autoSavedLogin.password, + }); + await cleanupDoorhanger(notif); + + // now update and submit the form with the user1 username and the generated password + info(`submitting form`); + let submitResults = await submitFormAndGetResults( + browser, + "formsubmit.sjs", + { + "#form-basic-username": "user1", + } + ); + Assert.equal( + submitResults.username, + "user1", + "Form submitted with expected username" + ); + Assert.equal( + submitResults.password, + autoSavedLogin.password, + "Form submitted with expected password" + ); + info( + `form was submitted, got username/password ${submitResults.username}/${submitResults.password}` + ); + + await waitForDoorhanger(browser, "password-change"); + notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: false, + anchorExtraAttr: "", + usernameValue: "user1", + password: autoSavedLogin.password, + }); + + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + let loginModifiedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => { + if (data == "modifyLogin") { + info("passwordmgr-storage-changed, action: " + data); + info("subject: " + JSON.stringify(_)); + return true; + } + return false; + } + ); + let loginRemovedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => { + if (data == "removeLogin") { + info("passwordmgr-storage-changed, action: " + data); + info("subject: " + JSON.stringify(_)); + return true; + } + return false; + } + ); + + info("clicking change button"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + await promiseHidden; + + info("Waiting for modifyLogin promise"); + await loginModifiedPromise; + + info("Waiting for removeLogin promise"); + await loginRemovedPromise; + + info("storage-change promises resolved"); + // Check the auto-saved login was removed and the original login updated + verifyLogins([ + { + username: "user1", + password: autoSavedLogin.password, + timeCreated: user1LoginSnapshot.timeCreated, + timeLastUsed: user1LoginSnapshot.timeLastUsed, + passwordChangedSince: autoSavedLogin.timePasswordChanged, + }, + ]); + + // Check we have no notifications at this point + Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open"); + Assert.ok( + !PopupNotifications.getNotification("password", browser), + "No notifications" + ); + + // make sure the cache entry is unchanged with the removal of the auto-saved login + Assert.equal( + autoSavedLogin.password, + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( + "https://example.com" + ).value, + "Generated password cache entry has the expected password value" + ); + } + ); +}); + +add_task(async function form_change_from_autosaved_login_to_existing_login() { + // test when changing from a generated password in a form to an existing saved login + // * the auto-saved login should not be deleted + // * the metadata for the matching login should be updated + // * the by-origin cache for the password should point at the autosaved login + + await setup_withOneLogin("user1", "xyzpassword"); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + { + password: { + selector: passwordInputSelector, + expectedValue: "xyzpassword", + setValue: "", + expectedMessage: "PasswordEditedOrGenerated", + }, + username: { + selector: usernameInputSelector, + expectedValue: "user1", + setValue: "", + // with an empty password value, no message is sent for a username change + expectedMessage: "", + }, + }, + async function taskFn(browser) { + await SimpleTest.promiseFocus(browser); + + // first, create an auto-saved login with generated password + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + // We don't need to wait to confirm the hint hides itelf every time + let forceClosePopup = true; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + info("Filling generated password from AC menu"); + await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector); + + info("waiting for dismissed password-change notification"); + await waitForDoorhanger(browser, "password-change"); + + // Make sure confirmation hint was shown + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + + info("waiting for addLogin"); + await storageChangedPromise; + info("addLogin promise resolved"); + // Check properties of the newly auto-saved login + let [user1LoginSnapshot, autoSavedLogin] = verifyLogins([ + null, // ignore the first one + { + timesUsed: 1, + username: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }, + ]); + info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid); + info("autoSavedLogin, guid: " + autoSavedLogin.guid); + + info("verifyLogins ok"); + let passwordCacheEntry = + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( + "https://example.com" + ); + + Assert.ok( + passwordCacheEntry, + "Got the cached generated password entry for https://example.com" + ); + Assert.equal( + passwordCacheEntry.value, + autoSavedLogin.password, + "Cached password matches the auto-saved login password" + ); + Assert.equal( + passwordCacheEntry.storageGUID, + autoSavedLogin.guid, + "Cached password guid matches the auto-saved login guid" + ); + + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "", + password: autoSavedLogin.password, + }); + + // close but don't remove the doorhanger, we want to ensure it is updated/replaced on further form edits + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + let PN = notif.owner; + PN.panel.hidePopup(); + await promiseHidden; + await TestUtils.waitForTick(); + + // now update the form with the user1 username and password + info(`updating form`); + let passwordEditedMessages = listenForTestNotification( + "PasswordEditedOrGenerated", + 2 + ); + let passwordChangeDoorhangerPromise = waitForDoorhanger( + browser, + "password-change" + ); + let hintDidShow = false; + let hintPromiseShown = BrowserTestUtils.waitForPopupEvent( + document.getElementById("confirmation-hint"), + "shown" + ); + hintPromiseShown.then(() => (hintDidShow = true)); + + info("Entering username and password for the previously saved login"); + + await changeContentFormValues(browser, { + [passwordInputSelector]: user1LoginSnapshot.password, + [usernameInputSelector]: user1LoginSnapshot.username, + }); + info( + "form edited, waiting for test notification of PasswordEditedOrGenerated" + ); + + await passwordEditedMessages; + info("Resolved listenForTestNotification promise"); + + await passwordChangeDoorhangerPromise; + // wait to ensure there's no confirmation hint + try { + await TestUtils.waitForCondition( + () => { + return hintDidShow; + }, + `Waiting for confirmationHint popup`, + undefined, + 25 + ); + } catch (ex) { + info("Got expected timeout from the waitForCondition: ", ex); + } finally { + Assert.ok(!hintDidShow, "No confirmation hint shown"); + } + + // the previous doorhanger would have old values, verify it was updated/replaced with new values from the form + notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "", + usernameValue: user1LoginSnapshot.username, + passwordLength: user1LoginSnapshot.password.length, + }); + await cleanupDoorhanger(notif); + + storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ); + + // submit the form to ensure the correct updates are made + await submitForm(browser); + info("form submitted, waiting for storage changed"); + await storageChangedPromise; + + // Check the auto-saved login has not changed and only metadata on the original login updated + verifyLogins([ + { + username: "user1", + password: "xyzpassword", + timeCreated: user1LoginSnapshot.timeCreated, + usedSince: user1LoginSnapshot.timeLastUsed, + }, + { + username: "", + password: autoSavedLogin.password, + timeCreated: autoSavedLogin.timeCreated, + timeLastUsed: autoSavedLogin.timeLastUsed, + }, + ]); + + // Check we have no notifications at this point + Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open"); + Assert.ok( + !PopupNotifications.getNotification("password", browser), + "No notifications" + ); + + // make sure the cache entry is unchanged with the removal of the auto-saved login + Assert.equal( + autoSavedLogin.password, + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get( + "https://example.com" + ).value, + "Generated password cache entry has the expected password value" + ); + } + ); +}); + +add_task(async function form_edit_username_and_password_of_generated_login() { + // test when changing the username and then the password in a form with a generated password (bug 1625242) + // * the toast is not shown for the username change as the auto-saved login is not modified + // * the dismissed doorhanger for the username change has the correct username and password + // * the toast is shown for the change to the generated password + // * the dismissed doorhanger for the password change has the correct username and password + + await setup_withNoLogins(); + await openFormInNewTab( + TEST_ORIGIN + FORM_PAGE_PATH, + {}, + async function taskFn(browser) { + await SimpleTest.promiseFocus(browser); + + // first, create an auto-saved login with generated password + let storageChangedPromise = TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "addLogin" + ); + // We don't need to wait to confirm the hint hides itelf every time + let forceClosePopup = true; + let hintShownAndVerified = verifyConfirmationHint( + browser, + forceClosePopup + ); + + info("Filling generated password from context menu"); + // there's no new-password field in this form so we'll use the context menu + await doFillGeneratedPasswordContextMenuItem( + browser, + passwordInputSelector + ); + + info("waiting for dismissed password-change notification"); + await waitForDoorhanger(browser, "password-change"); + + // Make sure confirmation hint was shown + info("waiting for verifyConfirmationHint"); + await hintShownAndVerified; + + info("waiting for addLogin"); + await storageChangedPromise; + info("addLogin promise resolved"); + // Check properties of the newly auto-saved login + let [autoSavedLoginSnapshot] = verifyLogins([ + { + timesUsed: 1, + username: "", + passwordLength: LoginTestUtils.generation.LENGTH, + }, + ]); + + let notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: "attention", + usernameValue: "", + password: autoSavedLoginSnapshot.password, + }); + + // close but don't remove the doorhanger, we want to ensure it is updated/replaced on further form edits + let promiseHidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + let PN = notif.owner; + PN.panel.hidePopup(); + await promiseHidden; + await TestUtils.waitForTick(); + + // change the username then the password in the form + for (let { + fieldSelector, + fieldValue, + expectedConfirmation, + expectedDoorhangerUsername, + expectedDoorhangerPassword, + expectedDoorhangerType, + } of [ + { + fieldSelector: usernameInputSelector, + fieldValue: "someuser", + expectedConfirmation: false, + expectedDoorhangerUsername: "someuser", + expectedDoorhangerPassword: autoSavedLoginSnapshot.password, + expectedDoorhangerType: "password-change", + }, + { + fieldSelector: passwordInputSelector, + fieldValue: "!!", + expectedConfirmation: true, + expectedDoorhangerUsername: "someuser", + expectedDoorhangerPassword: autoSavedLoginSnapshot.password + "!!", + expectedDoorhangerType: "password-change", + }, + ]) { + let loginModifiedPromise = expectedConfirmation + ? TestUtils.topicObserved( + "passwordmgr-storage-changed", + (_, data) => data == "modifyLogin" + ) + : Promise.resolve(); + + // now edit the field value + let passwordEditedMessage = listenForTestNotification( + "PasswordEditedOrGenerated" + ); + let passwordChangeDoorhangerPromise = waitForDoorhanger( + browser, + expectedDoorhangerType + ); + let hintDidShow = false; + let hintPromiseShown = BrowserTestUtils.waitForPopupEvent( + document.getElementById("confirmation-hint"), + "shown" + ); + hintPromiseShown.then(() => (hintDidShow = true)); + + info(`updating form: ${fieldSelector}: ${fieldValue}`); + await appendContentInputvalue(browser, fieldSelector, fieldValue); + info( + "form edited, waiting for test notification of PasswordEditedOrGenerated" + ); + await passwordEditedMessage; + info( + "Resolved listenForTestNotification promise, waiting for doorhanger" + ); + await passwordChangeDoorhangerPromise; + // wait for possible confirmation hint + try { + info("Waiting for hintDidShow"); + await TestUtils.waitForCondition( + () => hintDidShow, + `Waiting for confirmationHint popup`, + undefined, + 25 + ); + } catch (ex) { + info("Got expected timeout from the waitForCondition: " + ex); + } finally { + info("confirmationHint check done, assert on hintDidShow"); + Assert.equal( + hintDidShow, + expectedConfirmation, + "Confirmation hint shown" + ); + } + info( + "Waiting for loginModifiedPromise, expectedConfirmation? " + + expectedConfirmation + ); + await loginModifiedPromise; + + // the previous doorhanger would have old values, verify it was updated/replaced with new values from the form + info("Verifying the doorhanger"); + notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: true, + anchorExtraAttr: expectedConfirmation ? "attention" : "", + usernameValue: expectedDoorhangerUsername, + passwordLength: expectedDoorhangerPassword.length, + }); + await cleanupDoorhanger(notif); + } + + // submit the form to verify we still get the right doorhanger values + let passwordChangeDoorhangerPromise = waitForDoorhanger( + browser, + "password-change" + ); + await submitForm(browser); + info("form submitted, waiting for doorhanger"); + await passwordChangeDoorhangerPromise; + notif = await openAndVerifyDoorhanger(browser, "password-change", { + dismissed: false, + anchorExtraAttr: "", + usernameValue: "someuser", + passwordLength: LoginTestUtils.generation.LENGTH + 2, + }); + await cleanupDoorhanger(notif); + } + ); +}); |