Bug 1530029 - Pressing enter on the autocomplete footer should open the password manager dialog. r=MattN
authorprathiksha <prathikshaprasadsuman@gmail.com>
Tue, 12 Mar 2019 00:21:13 +0000
changeset 521486 bd7db47ad0786b0f1545207d237293b78db8adf6
parent 521485 89988d424c06a3333eb15a880bc1c4c6bbb1c070
child 521487 89679dcecb8a59dcc4272d1258faaf55be47ef8e
push id10866
push usernerli@mozilla.com
push dateTue, 12 Mar 2019 18:59:09 +0000
treeherdermozilla-beta@445c24a51727 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1530029
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 1530029 - Pressing enter on the autocomplete footer should open the password manager dialog. r=MattN Differential Revision: https://phabricator.services.mozilla.com/D21603
browser/components/BrowserGlue.jsm
toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
toolkit/components/passwordmgr/LoginHelper.jsm
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -534,16 +534,17 @@ const listeners = {
     "Reader:UpdateReaderButton": ["ReaderParent"],
     // PLEASE KEEP THIS LIST IN SYNC WITH THE MOBILE LISTENERS IN BrowserCLH.js
     "PasswordManager:findLogins": ["LoginManagerParent"],
     "PasswordManager:findRecipes": ["LoginManagerParent"],
     "PasswordManager:onFormSubmit": ["LoginManagerParent"],
     "PasswordManager:autoCompleteLogins": ["LoginManagerParent"],
     "PasswordManager:removeLogin": ["LoginManagerParent"],
     "PasswordManager:insecureLoginFormPresent": ["LoginManagerParent"],
+    "PasswordManager:OpenPreferences": ["LoginManagerParent"],
     // PLEASE KEEP THIS LIST IN SYNC WITH THE MOBILE LISTENERS IN BrowserCLH.js
     "rtcpeer:CancelRequest": ["webrtcUI"],
     "rtcpeer:Request": ["webrtcUI"],
     "webrtc:CancelRequest": ["webrtcUI"],
     "webrtc:Request": ["webrtcUI"],
     "webrtc:StopRecording": ["webrtcUI"],
     "webrtc:UpdateBrowserIndicators": ["webrtcUI"],
   },
--- a/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
+++ b/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
@@ -46,17 +46,17 @@ function LoginAutoCompleteResult(aSearch
         duplicates.add(login.username);
       }
       seen.add(login.username);
     }
     return duplicates;
   }
 
   this._showInsecureFieldWarning = (!isSecure && LoginHelper.showInsecureFieldWarning) ? 1 : 0;
-  this._showAutoCompleteFooter = LoginHelper.showAutoCompleteFooter ? 1 : 0;
+  this._showAutoCompleteFooter = (LoginHelper.showAutoCompleteFooter && LoginHelper.enabled) ? 1 : 0;
   this.searchString = aSearchString;
   this.logins = matchingLogins.sort(loginSort);
   this.matchCount = matchingLogins.length + this._showInsecureFieldWarning + this._showAutoCompleteFooter;
   this._messageManager = messageManager;
   this._stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
   this._dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, { dateStyle: "medium" });
 
   this._isPasswordField = isPasswordField;
--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -26,16 +26,17 @@ const {XPCOMUtils} = ChromeUtils.import(
  */
 var LoginHelper = {
   debug: null,
   enabled: null,
   formlessCaptureEnabled: null,
   schemeUpgrades: null,
   insecureAutofill: null,
   privateBrowsingCaptureEnabled: null,
+  showAutoCompleteFooter: null,
 
   init() {
     // Watch for pref changes to update cached pref values.
     Services.prefs.addObserver("signon.", () => this.updateSignonPrefs());
     this.updateSignonPrefs();
   },
 
   updateSignonPrefs() {
@@ -44,18 +45,18 @@ var LoginHelper = {
     this.debug = Services.prefs.getBoolPref("signon.debug");
     this.enabled = Services.prefs.getBoolPref("signon.rememberSignons");
     this.formlessCaptureEnabled = Services.prefs.getBoolPref("signon.formlessCapture.enabled");
     this.insecureAutofill = Services.prefs.getBoolPref("signon.autofillForms.http");
     this.privateBrowsingCaptureEnabled =
       Services.prefs.getBoolPref("signon.privateBrowsingCapture.enabled");
 
     this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
+    this.showAutoCompleteFooter = Services.prefs.getBoolPref("signon.showAutoCompleteFooter");
     this.storeWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff");
-    this.showAutoCompleteFooter = Services.prefs.getBoolPref("signon.showAutoCompleteFooter");
   },
 
   createLogger(aLogPrefix) {
     let getMaxLogLevel = () => {
       return this.debug ? "Debug" : "Warn";
     };
 
     let logger;
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -173,16 +173,19 @@ var LoginManagerContent = {
   _onVisibleTasksByDocument: new WeakMap(),
 
   // Map from form login requests to information about that request.
   _requests: new Map(),
 
   // Number of outstanding requests to each manager.
   _managers: new Map(),
 
+  // Input element on which enter keydown event was fired.
+  _keyDownEnterForInput: null,
+
   _takeRequest(msg) {
     let data = msg.data;
     let request = this._requests.get(data.requestId);
 
     this._requests.delete(data.requestId);
 
     let count = this._managers.get(msg.target);
     if (--count === 0) {
@@ -216,46 +219,83 @@ var LoginManagerContent = {
     messageManager.sendAsyncMessage(name, messageData);
 
     let deferred = PromiseUtils.defer();
     requestData.promise = deferred;
     this._requests.set(requestId, requestData);
     return deferred.promise;
   },
 
+  _onKeyDown(event) {
+    let focusedElement = LoginManagerContent._formFillService.focusedInput;
+    if (event.keyCode != event.DOM_VK_RETURN || focusedElement != event.target) {
+      this._keyDownEnterForInput = null;
+      return;
+    }
+    LoginManagerContent._keyDownEnterForInput = focusedElement;
+  },
+
+  _onPopupClosed(selectedRowStyle, mm) {
+    let focusedElement = LoginManagerContent._formFillService.focusedInput;
+    let eventTarget = LoginManagerContent._keyDownEnterForInput;
+    if (!eventTarget || eventTarget !== focusedElement ||
+        selectedRowStyle != "loginsFooter") {
+      this._keyDownEnterForInput = null;
+      return;
+    }
+    let hostname = eventTarget.ownerDocument.documentURIObject.host;
+    mm.sendAsyncMessage("PasswordManager:OpenPreferences", {hostname});
+  },
+
   receiveMessage(msg, topWindow) {
     if (msg.name == "PasswordManager:fillForm") {
       this.fillForm({
         topDocument: topWindow.document,
         loginFormOrigin: msg.data.loginFormOrigin,
         loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
         recipes: msg.data.recipes,
         inputElement: msg.objects.inputElement,
       });
       return;
     }
 
-    let request = this._takeRequest(msg);
     switch (msg.name) {
       case "PasswordManager:loginsFound": {
         let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
+        let request = this._takeRequest(msg);
         request.promise.resolve({
           form: request.form,
           loginsFound,
           recipes: msg.data.recipes,
         });
         break;
       }
 
       case "PasswordManager:loginsAutoCompleted": {
         let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
         let messageManager = msg.target;
+        let request = this._takeRequest(msg);
         request.promise.resolve({ logins: loginsFound, messageManager });
         break;
       }
+
+      case "FormAutoComplete:PopupOpened": {
+        let {chromeEventHandler} = msg.target.docShell;
+        chromeEventHandler.addEventListener("keydown", this._onKeyDown,
+                                            true);
+        break;
+      }
+
+      case "FormAutoComplete:PopupClosed": {
+        this._onPopupClosed(msg.data.selectedRowStyle, msg.target);
+        let {chromeEventHandler} = msg.target.docShell;
+        chromeEventHandler.removeEventListener("keydown", this._onKeyDown,
+                                               true);
+        break;
+      }
     }
   },
 
   /**
    * Get relevant logins and recipes from the parent
    *
    * @param {HTMLFormElement} form - form to get login data for
    * @param {Object} options
@@ -305,16 +345,21 @@ var LoginManagerContent = {
                         actionOrigin,
                         searchString: aSearchString,
                         previousResult,
                         rect: aRect,
                         isSecure: InsecurePasswordUtils.isFormSecure(form),
                         isPasswordField: aElement.type == "password",
     };
 
+    if (LoginHelper.showAutoCompleteFooter) {
+      messageManager.addMessageListener("FormAutoComplete:PopupOpened", this);
+      messageManager.addMessageListener("FormAutoComplete:PopupClosed", this);
+    }
+
     return this._sendRequest(messageManager, requestData,
                              "PasswordManager:autoCompleteLogins",
                              messageData);
   },
 
   setupEventListeners(global) {
     global.addEventListener("pageshow", (event) => {
       this.onPageShow(event);
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -110,16 +110,21 @@ var LoginManagerParent = {
         break;
       }
 
       case "PasswordManager:removeLogin": {
         let login = LoginHelper.vanillaObjectToLogin(data.login);
         AutoCompletePopup.removeLogin(login);
         break;
       }
+
+      case "PasswordManager:OpenPreferences": {
+        LoginHelper.openPasswordManager(msg.target.ownerGlobal, msg.data.hostname);
+        break;
+      }
     }
 
     return undefined;
   },
 
   /**
    * Trigger a login form fill and send relevant data (e.g. logins and recipes)
    * to the child process (LoginManagerContent).
--- a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js
@@ -16,51 +16,58 @@ function loginList() {
       hostname: "https://example.com",
       formSubmitURL: "https://example.com",
       username: "username2",
       password: "password2",
     }),
   ];
 }
 
+function openPopup(popup, browser) {
+  return new Promise(async (resolve) => {
+    let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+    await SimpleTest.promiseFocus(browser);
+    info("content window focused");
+
+    // Focus the username field to open the popup.
+    await ContentTask.spawn(browser, null, function openAutocomplete() {
+      content.document.getElementById("form-basic-username").focus();
+    });
+
+    let shown = await promiseShown;
+    ok(shown, "autocomplete popup shown");
+    resolve(shown);
+  });
+}
+
 /**
  * Initialize logins and set prefs needed for the test.
  */
 add_task(async function test_initialize() {
   Services.prefs.setBoolPref("signon.showAutoCompleteFooter", true);
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref("signon.showAutoCompleteFooter");
   });
 
   for (let login of loginList()) {
     Services.logins.addLogin(login);
   }
 });
 
-add_task(async function test_autocomplete_footer() {
+add_task(async function test_autocomplete_footer_onclick() {
   let url = TEST_HOSTNAME + BASIC_FORM_PAGE_PATH;
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url,
-  }, async function(browser) {
+  }, async function footer_onclick(browser) {
     let popup = document.getElementById("PopupAutoComplete");
     ok(popup, "Got popup");
 
-    let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
-
-    await SimpleTest.promiseFocus(browser);
-    info("content window focused");
-
-    // Focus the username field to open the popup.
-    await ContentTask.spawn(browser, null, function openAutocomplete() {
-      content.document.getElementById("form-basic-username").focus();
-    });
-
-    await promiseShown;
-    ok(promiseShown, "autocomplete shown");
+    await openPopup(popup, browser);
 
     let footer = popup.querySelector(`[originaltype="loginsFooter"]`);
     ok(footer, "Got footer richlistitem");
 
     await TestUtils.waitForCondition(() => {
       return !EventUtils.isHidden(footer);
     }, "Waiting for footer to become visible");
 
@@ -74,8 +81,46 @@ add_task(async function test_autocomplet
     await TestUtils.waitForCondition(() => {
       return window.document.getElementById("filter").value == "example.com";
     }, "Waiting for the search string to filter logins");
 
     window.close();
     popup.hidePopup();
   });
 });
+
+add_task(async function test_autocomplete_footer_keydown() {
+  let url = TEST_HOSTNAME + BASIC_FORM_PAGE_PATH;
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url,
+  }, async function footer_enter_keydown(browser) {
+    let popup = document.getElementById("PopupAutoComplete");
+    ok(popup, "Got popup");
+
+    await openPopup(popup, browser);
+
+    let footer = popup.querySelector(`[originaltype="loginsFooter"]`);
+    ok(footer, "Got footer richlistitem");
+
+    await TestUtils.waitForCondition(() => {
+      return !EventUtils.isHidden(footer);
+    }, "Waiting for footer to become visible");
+
+    await EventUtils.synthesizeKey("KEY_ArrowDown");
+    await EventUtils.synthesizeKey("KEY_ArrowDown");
+    await EventUtils.synthesizeKey("KEY_ArrowDown");
+    await EventUtils.synthesizeKey("KEY_Enter");
+
+    await TestUtils.waitForCondition(() => {
+      return Services.wm.getMostRecentWindow("Toolkit:PasswordManager") !== null;
+    }, "Waiting for the password manager dialog to open");
+    info("Login dialog was opened");
+
+    let window = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
+    await TestUtils.waitForCondition(() => {
+      return window.document.getElementById("filter").value == "example.com";
+    }, "Waiting for the search string to filter logins");
+
+    window.close();
+    popup.hidePopup();
+  });
+});