Bug 1189524 - Provide visual feedback to the user when login fields are autofilled and autocompleted. r=MattN
authorprathiksha <prathikshaprasadsuman@gmail.com>
Wed, 06 Feb 2019 23:50:04 -0800
changeset 457532 bcfe1e75c5016b7e5fed79c13d97eec73434b289
parent 457531 4fe6c3dcad6373481c96bb8bde8b43631ee9a95b
child 457533 04eecadabb485bd398c6c37e258ba47b946e7982
push id111739
push usermozilla@noorenberghe.ca
push dateThu, 07 Feb 2019 07:59:17 +0000
treeherdermozilla-inbound@bcfe1e75c501 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1189524
milestone67.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 1189524 - Provide visual feedback to the user when login fields are autofilled and autocompleted. r=MattN Differential Revision: https://phabricator.services.mozilla.com/D18153
browser/base/content/content.js
mobile/android/components/BrowserCLH.js
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/test/mochitest/mochitest.ini
toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html
toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html
toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -56,17 +56,17 @@ addEventListener("DOMInputPasswordAdded"
   LoginManagerContent.onDOMInputPasswordAdded(event, content);
   let formLike = LoginFormFactory.createFromField(event.originalTarget);
   InsecurePasswordUtils.reportInsecurePasswords(formLike);
 });
 addEventListener("DOMAutoComplete", function(event) {
   if (shouldIgnoreLoginManagerEvent(event)) {
     return;
   }
-  LoginManagerContent.onUsernameInput(event);
+  LoginManagerContent.onDOMAutoComplete(event);
 });
 
 ContentMetaHandler.init(this);
 
 // This is a temporary hack to prevent regressions (bug 1471327).
 void content;
 
 addEventListener("DOMWindowFocus", function(event) {
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -209,25 +209,17 @@ BrowserCLH.prototype = {
       }
       this.LoginManagerContent.onDOMInputPasswordAdded(event, event.target.ownerGlobal.top);
     }, options);
 
     aWindow.addEventListener("DOMAutoComplete", event => {
       if (shouldIgnoreLoginManagerEvent(event)) {
         return;
       }
-      this.LoginManagerContent.onUsernameInput(event);
-    }, options);
-
-    aWindow.addEventListener("blur", event => {
-      if (ChromeUtils.getClassName(event.target) !== "HTMLInputElement" ||
-          shouldIgnoreLoginManagerEvent(event)) {
-        return;
-      }
-      this.LoginManagerContent.onUsernameInput(event);
+      this.LoginManagerContent.onDOMAutoComplete(event);
     }, options);
 
     aWindow.addEventListener("pageshow", event => {
       // XXXbz what about non-HTML documents??
       if (ChromeUtils.getClassName(event.target) == "HTMLDocument") {
         this.LoginManagerContent.onPageShow(event, event.target.defaultView.top);
       }
     }, options);
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -5,16 +5,17 @@
 "use strict";
 
 var EXPORTED_SYMBOLS = [ "LoginManagerContent",
                          "LoginFormFactory",
                          "UserAutoCompleteResult" ];
 
 const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
 const AUTOCOMPLETE_AFTER_RIGHT_CLICK_THRESHOLD_MS = 400;
+const AUTOFILL_STATE = "-moz-autofill";
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const {PrivateBrowsingUtils} = ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 const {PromiseUtils} = ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
 ChromeUtils.defineModuleGetter(this, "FormLikeFactory",
@@ -600,52 +601,59 @@ var LoginManagerContent = {
       return;
     }
 
     log("maybeOpenAutocompleteAfterFocus: Opening the autocomplete popup");
     this._formFillService.showPopup();
   },
 
   /**
-   * Listens for DOMAutoComplete and blur events on an input field.
+   * Listens for DOMAutoComplete event on login form.
    */
-  onUsernameInput(event) {
+  onDOMAutoComplete(event) {
     if (!event.isTrusted) {
       return;
     }
 
     if (!gEnabled) {
       return;
     }
 
-    var acInputField = event.target;
+    let acInputField = event.target;
 
     // This is probably a bit over-conservatative.
     if (ChromeUtils.getClassName(acInputField.ownerDocument) != "HTMLDocument") {
       return;
     }
 
-    if (!LoginHelper.isUsernameFieldType(acInputField)) {
+    if (!LoginFormFactory.createFromField(acInputField)) {
       return;
     }
 
-    var acForm = LoginFormFactory.createFromField(acInputField);
-    if (!acForm) {
-      return;
+    if (LoginHelper.isUsernameFieldType(acInputField)) {
+      this.onUsernameInput(event);
     }
+  },
+
+  /**
+   * Calls fill form on the username field.
+   */
+  onUsernameInput(event) {
+    let acInputField = event.target;
 
     // If the username is blank, bail out now -- we don't want
     // fillForm() to try filling in a login without a username
     // to filter on (bug 471906).
     if (!acInputField.value) {
       return;
     }
 
     log("onUsernameInput from", event.type);
 
+    let acForm = LoginFormFactory.createFromField(acInputField);
     let doc = acForm.ownerDocument;
     let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
     let recipes = LoginRecipesContent.getRecipes(formOrigin, doc.defaultView);
 
     // Make sure the username field fillForm will use is the
     // same field as the autocomplete was activated on.
     var [usernameField, passwordField, ignored] =
         this._getFormFields(acForm, false, recipes);
@@ -993,16 +1001,38 @@ var LoginManagerContent = {
                                       formSubmitURL,
                                       usernameField: mockUsername,
                                       newPasswordField: mockPassword,
                                       oldPasswordField: mockOldPassword,
                                       openerTopWindowID,
                                     });
   },
 
+  /** Remove login field highlight when its value is cleared or overwritten.
+   */
+  _removeFillFieldHighlight(event) {
+    let winUtils = event.target.ownerGlobal.windowUtils;
+    winUtils.removeManuallyManagedState(event.target, AUTOFILL_STATE);
+  },
+
+  /**
+   * Highlight login fields on autocomplete or autofill on page load.
+   * @param {Node} element that needs highlighting.
+   */
+  _highlightFilledField(element) {
+    let winUtils = element.ownerGlobal.windowUtils;
+
+    winUtils.addManuallyManagedState(element, AUTOFILL_STATE);
+    // Remove highlighting when the field is changed.
+    element.addEventListener("input", this._removeFillFieldHighlight, {
+      mozSystemGroup: true,
+      once: true,
+    });
+  },
+
   /**
    * Attempt to find the username and password fields in a form, and fill them
    * in using the provided logins and recipes.
    *
    * @param {LoginForm} form
    * @param {nsILoginInfo[]} foundLogins an array of nsILoginInfo that could be
             used for the form
    * @param {Set} recipes a set of recipes that could be used to affect how the
@@ -1243,18 +1273,22 @@ var LoginManagerContent = {
 
         let userNameDiffers = selectedLogin.username != usernameField.value;
         // Don't replace the username if it differs only in case, and the user triggered
         // this autocomplete. We assume that if it was user-triggered the entered text
         // is desired.
         let userEnteredDifferentCase = userTriggered && userNameDiffers &&
                usernameField.value.toLowerCase() == selectedLogin.username.toLowerCase();
 
-        if (!disabledOrReadOnly && !userEnteredDifferentCase && userNameDiffers) {
-          usernameField.setUserInput(selectedLogin.username);
+        if (!disabledOrReadOnly) {
+          if (!userEnteredDifferentCase && userNameDiffers) {
+            usernameField.setUserInput(selectedLogin.username);
+          }
+
+          this._highlightFilledField(usernameField);
         }
       }
 
       let doc = form.ownerDocument;
       if (passwordField.value != selectedLogin.password) {
         passwordField.setUserInput(selectedLogin.password);
         let autoFilledLogin = {
           guid: selectedLogin.QueryInterface(Ci.nsILoginMetaInfo).guid,
@@ -1262,16 +1296,18 @@ var LoginManagerContent = {
           usernameField: usernameField ? Cu.getWeakReference(usernameField) : null,
           password: selectedLogin.password,
           passwordField: Cu.getWeakReference(passwordField),
         };
         log("Saving autoFilledLogin", autoFilledLogin.guid, "for", form.rootElement);
         this.stateForDocument(doc).fillsByRootElement.set(form.rootElement, autoFilledLogin);
       }
 
+      this._highlightFilledField(passwordField);
+
       log("_fillForm succeeded");
       autofillResult = AUTOFILL_RESULT.FILLED;
 
       let win = doc.defaultView;
       let messageManager = win.docShell.messageManager;
       messageManager.sendAsyncMessage("LoginStats:LoginFillSuccessful");
     } finally {
       if (autofillResult == -1) {
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -16,21 +16,26 @@ support-files =
   ../browser/formless_basic.html
   ../browser/form_cross_origin_secure_action.html
   auth2/authenticate.sjs
   pwmgr_common.js
   pwmgr_common_parent.js
   ../authenticate.sjs
 skip-if = toolkit == 'android' && !isFennec # Don't run on GeckoView
 
+[test_autocomplete_highlight.html]
+scheme = https
+skip-if = toolkit == 'android' # autocomplete
 [test_autocomplete_https_upgrade.html]
 skip-if = toolkit == 'android' # autocomplete
 [test_autocomplete_sandboxed.html]
 scheme = https
 skip-if = toolkit == 'android' # autocomplete
+[test_autofill_highlight.html]
+scheme = https
 [test_autofill_https_upgrade.html]
 skip-if = toolkit == 'android' # Bug 1259768
 [test_autofill_sandboxed.html]
 scheme = https
 skip-if = toolkit == 'android'
 [test_autofill_password-only.html]
 [test_autofocus_js.html]
 skip-if = toolkit == 'android' # autocomplete
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test form field autofill highlight</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+  <script type="text/javascript" src="pwmgr_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+runInParent(function initLogins() {
+  const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+  let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"]
+                        .createInstance(Ci.nsILoginInfo);
+  login1.init("https://example.com", "https://autocomplete", null,
+              "user1", "pass1", "", "");
+
+  let login2 = Cc["@mozilla.org/login-manager/loginInfo;1"]
+                        .createInstance(Ci.nsILoginInfo);
+  login2.init("https://example.com", "https://autocomplete", null,
+              "user2", "pass2", "", "");
+
+  Services.logins.addLogin(login1);
+  Services.logins.addLogin(login2);
+});
+
+</script>
+<body>
+<p id="display"></p>
+<div id="content">
+  <form id="form1" action="https://autocomplete" onsubmit="return false;">
+    <input  type="text"       id="uname">
+    <input  type="password"   id="pword">
+    <button type="submit">Submit</button>
+  </form>
+<pre id="test">
+<script>
+let {ContentTaskUtils} = SpecialPowers.Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
+
+add_task(async function test_field_highlight_on_autocomplete() {
+  // Test username autocomplete.
+  let username = document.getElementById("uname");
+  let password = document.getElementById("pword");
+
+  username.focus();
+
+  let shownPromise = promiseACShown();
+  synthesizeKey("KEY_ArrowDown");
+  await shownPromise;
+  synthesizeKey("KEY_ArrowDown");
+  await synthesizeKey("KEY_Enter");
+
+  await ContentTaskUtils.waitForCondition(() => {
+    return document.defaultView.getComputedStyle(username).getPropertyValue("filter") !== "none";
+  }, "Highlight was successfully applied to the username field on username autocomplete");
+
+  isnot(document.defaultView.getComputedStyle(password).getPropertyValue("filter"), "none",
+        "Highlight was successfully applied to the password field on username autocomplete");
+
+  // Clear existing highlight on login fields. We check by pressing the tab key after backspace
+  // (by shifting focus to the next element) because the tab key is known to cause a bug where the
+  // highlight is applied once again.
+  username.focus();
+  synthesizeKey("KEY_Backspace");
+  synthesizeKey("KEY_Tab");
+  is(document.defaultView.getComputedStyle(username).getPropertyValue("filter"), "none",
+     "Highlight was successfully removed on the username field");
+
+  synthesizeKey("KEY_Backspace");
+  synthesizeKey("KEY_Tab");
+  is(document.defaultView.getComputedStyle(password).getPropertyValue("filter"), "none",
+     "Highlight was successfully removed on the password field");
+});
+</script>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test form field autofill highlight</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script type="text/javascript" src="pwmgr_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+runInParent(function initLogins() {
+  const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+  let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"]
+                        .createInstance(Ci.nsILoginInfo);
+  login1.init("https://example.com", "https://autofill", null,
+              "user1", "pass1", "", "");
+
+  Services.logins.addLogin(login1);
+});
+</script>
+<body>
+<p id="display"></p>
+
+<div id="content">
+  <form id="form1" action="https://autofill" onsubmit="return false;">
+    <input  type="text"       id="uname">
+    <input  type="password"   id="pword">
+    <button type="submit">Submit</button>
+  </form>
+
+<pre id="test">
+<script>
+let {ContentTaskUtils} = SpecialPowers.Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
+
+add_task(async function test_field_highlight_on_autofill() {
+  let username = document.getElementById("uname");
+  let password = document.getElementById("pword");
+
+  await ContentTaskUtils.waitForCondition(() => {
+    return document.defaultView.getComputedStyle(username).getPropertyValue("filter") !== "none";
+  }, "Highlight was successfully applied to the username field on page load autofill");
+
+  isnot(document.defaultView.getComputedStyle(password).getPropertyValue("filter"), "none",
+        "Highlight was successfully applied to the password field on page load autofill");
+
+  // Test that initiating a change on the input value will remove the highlight. We check by pressing
+  // the tab key after backspace(by shifting focus to the next element) because the tab key is known to
+  // cause a bug where the highlight is applied once again.
+  username.focus();
+  synthesizeKey("KEY_Backspace");
+  synthesizeKey("KEY_Tab");
+
+  let computedStyle = document.defaultView.getComputedStyle(username);
+  is(computedStyle.getPropertyValue("filter"), "none", "Highlight was successfully removed on change in value of username input element");
+
+  synthesizeKey("KEY_Backspace");
+  synthesizeKey("KEY_Tab");
+  computedStyle = document.defaultView.getComputedStyle(password);
+  is(computedStyle.getPropertyValue("filter"), "none", "Highlight was successfully removed on change in value of password input element");
+});
+</script>
+</body>
+</html>
+
--- a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
@@ -1,17 +1,16 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
-  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script>
 const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
 const CROSS_ORIGIN_SECURE_PATH = TESTS_DIR + "mochitest/form_cross_origin_secure_action.html";