Bug 1571465 - Stop treating field as generated password ones after blanking. r=sfoster
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Mon, 12 Aug 2019 23:24:46 +0000
changeset 488106 2758d131787315018cf4092f6dd0bd5952e47996
parent 488105 59db91645f2e4c7edf66d8250c2d102fe8c6f2c9
child 488107 21fccebed05de6fcbb24d34d817b3dbba0c10ac8
push id113900
push usercbrindusan@mozilla.com
push dateThu, 15 Aug 2019 09:53:50 +0000
treeherdermozilla-inbound@0db07ff50ab5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfoster
bugs1571465
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1571465 - Stop treating field as generated password ones after blanking. r=sfoster Differential Revision: https://phabricator.services.mozilla.com/D41146
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/test/mochitest/test_autocomplete_new_password.html
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -205,16 +205,22 @@ const observer = {
       }
 
       // Used to watch for changes to fields filled with generated passwords.
       case "change": {
         LoginManagerContent._generatedPasswordFilledOrEdited(aEvent.target);
         break;
       }
 
+      // Used to watch for changes to fields filled with generated passwords.
+      case "input": {
+        LoginManagerContent._maybeStopTreatingAsGeneratedPasswordField(aEvent);
+        break;
+      }
+
       case "keydown": {
         if (
           aEvent.keyCode == aEvent.DOM_VK_TAB ||
           aEvent.keyCode == aEvent.DOM_VK_RETURN
         ) {
           LoginManagerContent.onUsernameAutocompleted(aEvent.target);
         }
         break;
@@ -1517,16 +1523,37 @@ this.LoginManagerContent = {
       usernameField: mockUsername,
       newPasswordField: mockPassword,
       oldPasswordField: mockOldPassword,
       openerTopWindowID,
       dismissedPrompt,
     });
   },
 
+  _maybeStopTreatingAsGeneratedPasswordField(event) {
+    let passwordField = event.target;
+
+    // If the field isn't empty then keep treating it as a generated password field.
+    if (passwordField.value) {
+      return;
+    }
+
+    log("_maybeStopTreatingAsGeneratedPasswordField: Stopping");
+    // Remove all the event listeners added in _generatedPasswordFilledOrEdited
+    for (let eventType of ["blur", "change", "focus", "input"]) {
+      passwordField.removeEventListener(eventType, observer, {
+        capture: true,
+        mozSystemGroup: true,
+      });
+    }
+
+    // Mask the password field
+    this._togglePasswordFieldMasking(passwordField, false);
+  },
+
   /**
    * Notify the parent that a generated password was filled into a field or
    * edited so that it can potentially be saved.
    * @param {HTMLInputElement} passwordField
    */
   _generatedPasswordFilledOrEdited(passwordField) {
     log("_generatedPasswordFilledOrEdited", passwordField);
 
@@ -1535,34 +1562,27 @@ this.LoginManagerContent = {
         "A generated password was filled while the password manager was disabled."
       );
     }
 
     let win = passwordField.ownerGlobal;
 
     this._highlightFilledField(passwordField);
 
-    passwordField.addEventListener("blur", observer, {
-      capture: true,
-      mozSystemGroup: true,
-    });
-    passwordField.addEventListener("focus", observer, {
-      capture: true,
-      mozSystemGroup: true,
-    });
-
+    // change: Listen for changes to the field filled with the generated password so we can preserve edits.
+    // input: Listen for the field getting blanked (without blurring) or a paste
+    for (let eventType of ["blur", "change", "focus", "input"]) {
+      passwordField.addEventListener(eventType, observer, {
+        capture: true,
+        mozSystemGroup: true,
+      });
+    }
     // Unmask the password field
     this._togglePasswordFieldMasking(passwordField, true);
 
-    // Listen for changes to the field filled with the generated password so we can preserve edits.
-    passwordField.addEventListener("change", observer, {
-      capture: true,
-      mozSystemGroup: true,
-    });
-
     if (PrivateBrowsingUtils.isContentWindowPrivate(win)) {
       log(
         "_generatedPasswordFilledOrEdited: not automatically saving the password in private browsing mode"
       );
       return;
     }
 
     let loginForm = LoginFormFactory.createFromField(passwordField);
--- a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_new_password.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_new_password.html
@@ -191,16 +191,17 @@ add_task(async function test_autofillAut
   await promiseNoUnexpectedPopupShown();
 
   info("Removing all logins to test auto-saving of generated passwords");
   await LoginManager.removeAllLogins();
 
   while (pword.value) {
     synthesizeKey("KEY_Backspace");
   }
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field");
 
   info("This time select the generated password");
   shownPromise = promiseACShown();
   synthesizeKey("KEY_ArrowDown");
   results = await shownPromise;
   expectedACLabels = [
     "Use a Securely Generated Password",
   ];
@@ -232,35 +233,54 @@ add_task(async function test_autofillAut
   ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format");
   LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After fill");
 
   info("Check field is masked upon blurring");
   synthesizeKey("KEY_Tab"); // blur
   LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "After blur");
   synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
   LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After shift-tab to focus again");
+  // Remove selection for OS where the whole value is selected upon focus.
+  synthesizeKey("KEY_ArrowRight");
 
   while (pword.value) {
     LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, pword.value);
     synthesizeKey("KEY_Backspace");
   }
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field");
+
+  info("Blur the empty field to trigger a 'change' event");
+  synthesizeKey("KEY_Tab"); // blur
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after blanking");
+  synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after blanking");
+
+  info("Type a single character after blanking");
+  synthesizeKey("@");
+
+  info("Blur the single-character field to trigger a 'change' event");
+  synthesizeKey("KEY_Tab"); // blur
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after backspacing");
+  synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after backspacing");
+  synthesizeKey("KEY_Backspace"); // Blank the field again
 
   shownPromise = promiseACShown();
   synthesizeKey("KEY_ArrowDown");
   results = await shownPromise;
   expectedACLabels = [
     LABEL_NO_USERNAME,
     "Use a Securely Generated Password",
   ];
   checkAutoCompleteResults(results, expectedACLabels, "example.com", "Check all rows are correct");
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Enter");
   await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
-  // Same generated password should be used.
+  // Same generated password should be used, even despite the 'change' to @ earlier.
   checkForm(2, "", generatedPW);
   LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "Second fill of the generated pw");
 
   info("filled generated password again, ensure we don't record another generatedpassword autocomplete telemetry event");
   let telemetryEvents;
   expectedCount = 2;
   try {
     telemetryEvents = await waitForTelemetryEventsCondition(events => events.length == expectedCount,
@@ -275,16 +295,25 @@ add_task(async function test_autofillAut
   is(logins[0].password, generatedPW, "Password is the same");
 
   info("filling the saved login to ensure the field is masked again");
 
   while (pword.value) {
     LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, pword.value);
     synthesizeKey("KEY_Backspace");
   }
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blanked field again");
+
+  info("Blur the field to trigger a 'change' event again");
+  synthesizeKey("KEY_Tab"); // blur
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Blur after blanking again");
+  synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+  LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Focus again after blanking again");
+  // Remove selection for OS where the whole value is selected upon focus.
+  synthesizeKey("KEY_ArrowRight");
 
   shownPromise = promiseACShown();
   synthesizeKey("KEY_ArrowDown");
   results = await shownPromise;
   expectedACLabels = [
     LABEL_NO_USERNAME,
     "Use a Securely Generated Password",
   ];