Bug 1217134 - Replace show password placeholder with conventional show password checkbox; r?MattN draft
authorgasolin <gasolin@gmail.com>
Wed, 13 Apr 2016 11:37:38 +0800
changeset 364695 b681f08dbcd06c3fae9a037591defcecf55299d1
parent 362784 fd84d2a8fe7c488456b52d65087f993de5d262bc
child 520369 c933f030a8f851db2d007af3e8bb9f7fce4c3a57
push id17545
push userbmo:gasolin@mozilla.com
push dateMon, 09 May 2016 02:32:32 +0000
reviewersMattN
bugs1217134
milestone49.0a1
Bug 1217134 - Replace show password placeholder with conventional show password checkbox; r?MattN MozReview-Commit-ID: 26bja1C8vl2
browser/base/content/browser.css
browser/base/content/popup-notifications.inc
toolkit/components/passwordmgr/nsLoginManagerPrompter.js
toolkit/components/passwordmgr/test/browser/browser.ini
toolkit/components/passwordmgr/test/browser/browser_notifications.js
toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1287,32 +1287,8 @@ toolbarpaletteitem[place="palette"][hidd
 
 .popup-notification-invalid-input {
   box-shadow: 0 0 1.5px 1px red;
 }
 
 .popup-notification-invalid-input[focused] {
   box-shadow: 0 0 2px 2px rgba(255,0,0,0.4);
 }
-
-#password-notification-password::after {
-  color: hsl(0,0%,60%);
-  content: attr(show-content);
-  pointer-events: none;
-  position: absolute;
-  right: 0;
-  transition: color 250ms;
-}
-
-#password-notification-password:hover::after {
-  color: hsl(210,100%,50%);
-}
-
-#password-notification-password[focused]::after {
-  content: none;
-}
-
-/* Bug 1175941: Disable the transition on 10.10 due to flickering, possibly due to an OS X bug. */
-@media (-moz-mac-yosemite-theme) {
-  #password-notification-password::after {
-    transition: none;
-  }
-}
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -50,16 +50,17 @@
         <label id="pointerLock-cancel">&pointerLock.notification.message;</label>
       </popupnotificationcontent>
     </popupnotification>
 
     <popupnotification id="password-notification" hidden="true">
       <popupnotificationcontent orient="vertical">
         <textbox id="password-notification-username"/>
         <textbox id="password-notification-password" type="password" show-content=""/>
+        <checkbox id="password-notification-visibilityToggle" hidden="true"/>
       </popupnotificationcontent>
     </popupnotification>
 
     <stack id="login-fill-doorhanger" hidden="true">
       <vbox id="login-fill-mainview">
         <description id="login-fill-testing"
                      value="Thanks for testing the login fill doorhanger!"/>
         <textbox id="login-fill-filter"/>
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -332,17 +332,17 @@ LoginManagerPrompter.prototype = {
                            this._pwmgr.getLoginSavingEnabled(hostname);
 
       // if checkBoxLabel is null, the checkbox won't be shown at all.
       if (canRememberLogin)
         checkBoxLabel = this._getLocalizedString("rememberPassword");
 
       // Look for existing logins.
       var foundLogins = this._pwmgr.findLogins({}, hostname, null,
-                                               realm);
+                                  realm);
 
       // XXX Like the original code, we can't deal with multiple
       // account selection. (bug 227632)
       if (foundLogins.length > 0) {
         selectedLogin = foundLogins[0];
 
         // If the caller provided a username, try to use it. If they
         // provided only a password, this will try to find a password-only
@@ -433,17 +433,17 @@ LoginManagerPrompter.prototype = {
 
       // if checkBoxLabel is null, the checkbox won't be shown at all.
       if (canRememberLogin)
         checkBoxLabel = this._getLocalizedString("rememberPassword");
 
       if (!aPassword.value) {
         // Look for existing logins.
         var foundLogins = this._pwmgr.findLogins({}, hostname, null,
-                                                 realm);
+                                          realm);
 
         // XXX Like the original code, we can't deal with multiple
         // account selection (bug 227632). We can deal with finding the
         // account based on the supplied username - but in this case we'll
         // just return the first match.
         for (var i = 0; i < foundLogins.length; ++i) {
           if (foundLogins[i].username == username) {
             aPassword.value = foundLogins[i].password;
@@ -520,27 +520,25 @@ LoginManagerPrompter.prototype = {
   promptAuth : function (aChannel, aLevel, aAuthInfo) {
     var selectedLogin = null;
     var checkbox = { value : false };
     var checkboxLabel = null;
     var epicfail = false;
     var canAutologin = false;
 
     try {
-
       this.log("===== promptAuth called =====");
 
       // If the user submits a login but it fails, we need to remove the
       // notification bar that was displayed. Conveniently, the user will
       // be prompted for authentication again, which brings us here.
       this._removeLoginNotifications();
 
       var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
 
-
       // Looks for existing logins to prefill the prompt with.
       var foundLogins = this._pwmgr.findLogins({},
                                   hostname, null, httpRealm);
       this.log("found " + foundLogins.length + " matching logins.");
 
       // XXX Can't select from multiple accounts yet. (bug 227632)
       if (foundLogins.length > 0) {
         selectedLogin = foundLogins[0];
@@ -615,19 +613,17 @@ LoginManagerPrompter.prototype = {
         this.log("New login seen for " + username +
                  " @ " + hostname + " (" + httpRealm + ")");
 
         let notifyObj = this._getPopupNote() || notifyBox;
         if (notifyObj)
           this._showSaveLoginNotification(notifyObj, newLogin);
         else
           this._pwmgr.addLogin(newLogin);
-
       } else if (password != selectedLogin.password) {
-
         this.log("Updating password for " + username +
                  " @ " + hostname + " (" + httpRealm + ")");
         let notifyObj = this._getPopupNote() || notifyBox;
         if (notifyObj)
           this._showChangeLoginNotification(notifyObj,
                                             selectedLogin, newLogin);
         else
           this._updateLogin(selectedLogin, newLogin);
@@ -850,77 +846,50 @@ LoginManagerPrompter.prototype = {
 
     let writeDataToUI = () => {
       // setAttribute is used since the <textbox> binding may not be attached yet.
       chromeDoc.getElementById("password-notification-username")
                .setAttribute("placeholder", usernamePlaceholder);
       chromeDoc.getElementById("password-notification-username")
                .setAttribute("value", login.username);
 
+      let toggleCheckbox = chromeDoc.getElementById("password-notification-visibilityToggle");
+      toggleCheckbox.removeAttribute("checked");
       let passwordField = chromeDoc.getElementById("password-notification-password");
       // Ensure the type is reset so the field is masked.
       passwordField.setAttribute("type", "password");
       passwordField.setAttribute("value", login.password);
-      if (Services.prefs.getBoolPref("signon.rememberSignons.visibilityToggle")) {
-        passwordField.setAttribute("show-content", showPasswordPlaceholder);
-      } else {
-        passwordField.setAttribute("show-content", "");
-      }
       updateButtonLabel();
     };
 
     let readDataFromUI = () => {
       login.username =
         chromeDoc.getElementById("password-notification-username").value;
       login.password =
         chromeDoc.getElementById("password-notification-password").value;
     };
 
     let onInput = () => {
       readDataFromUI();
       updateButtonLabel();
     };
 
-    let onPasswordFocus = (focusEvent) => {
+    let onVisibilityToggle = (commandEvent) => {
       let passwordField = chromeDoc.getElementById("password-notification-password");
       // Gets the caret position before changing the type of the textbox
       let selectionStart = passwordField.selectionStart;
       let selectionEnd = passwordField.selectionEnd;
-      if (focusEvent.rangeParent != null) {
-        // Check for a click over the SHOW placeholder
-        selectionStart = passwordField.value.length;
-        selectionEnd = passwordField.value.length;
+      passwordField.setAttribute("type", commandEvent.target.checked ? "" : "password");
+      if (!passwordField.hasAttribute("focused")) {
+        return;
       }
-      passwordField.setAttribute("type", "");
       passwordField.selectionStart = selectionStart;
       passwordField.selectionEnd = selectionEnd;
     };
 
-    let onPasswordBlur = () => {
-      // Use setAttribute in case the <textbox> binding isn't applied.
-      chromeDoc.getElementById("password-notification-password").setAttribute("type", "password");
-    };
-
-    let onNotificationClick = (clickEvent) => {
-      // Removes focus from textboxes when we click elsewhere on the doorhanger.
-      let focusedElement = Services.focus.focusedElement;
-      if (!focusedElement || focusedElement.nodeName != "html:input") {
-        // No input is focused so we don't need to blur
-        return;
-      }
-
-      let focusedBindingParent = chromeDoc.getBindingParent(focusedElement);
-      if (!focusedBindingParent || focusedBindingParent.nodeName != "textbox" ||
-          clickEvent.explicitOriginalTarget == focusedBindingParent) {
-        // The focus wasn't in a textbox or the click was in the focused textbox.
-        return;
-      }
-      focusedBindingParent.blur();
-    };
-
     let persistData = () => {
       let foundLogins = Services.logins.findLogins({}, login.hostname,
                                                    login.formSubmitURL,
                                                    login.httpRealm);
       let logins = this._filterUpdatableLogins(login, foundLogins);
 
       if (logins.length == 0) {
         // The original login we have been provided with might have its own
@@ -968,17 +937,18 @@ LoginManagerPrompter.prototype = {
       callback: () => {
         histogram.add(PROMPT_NEVER);
         Services.logins.setLoginSavingEnabled(login.hostname, false);
         browser.focus();
       }
     }] : null;
 
     let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder");
-    let showPasswordPlaceholder = this._getLocalizedString("showPasswordPlaceholder");
+    let togglePasswordLabel = this._getLocalizedString("togglePasswordLabel");
+    let togglePasswordAccessKey = this._getLocalizedString("togglePasswordAccessKey");
     let displayHost = this._getShortDisplayHost(login.hostname);
 
     this._getPopupNote().show(
       browser,
       "password",
       promptMsg,
       "password-notification-icon",
       mainAction,
@@ -992,42 +962,40 @@ LoginManagerPrompter.prototype = {
           switch (topic) {
             case "showing":
               currentNotification = this;
               chromeDoc.getElementById("password-notification-username")
                        .addEventListener("input", onInput);
               chromeDoc.getElementById("password-notification-password")
                        .addEventListener("input", onInput);
               if (Services.prefs.getBoolPref("signon.rememberSignons.visibilityToggle")) {
-                chromeDoc.getElementById("password-notification-password")
-                         .addEventListener("focus", onPasswordFocus);
+                chromeDoc.getElementById("password-notification-visibilityToggle")
+                         .addEventListener("command", onVisibilityToggle);
+                chromeDoc.getElementById("password-notification-visibilityToggle")
+                         .setAttribute("label", togglePasswordLabel);
+                chromeDoc.getElementById("password-notification-visibilityToggle")
+                         .setAttribute("accesskey", togglePasswordAccessKey);
+                chromeDoc.getElementById("password-notification-visibilityToggle")
+                         .removeAttribute("hidden");
               }
-              chromeDoc.getElementById("password-notification-password")
-                       .addEventListener("blur", onPasswordBlur);
               break;
             case "shown":
-              chromeDoc.getElementById("notification-popup")
-                         .addEventListener("click", onNotificationClick);
               writeDataToUI();
               break;
             case "dismissed":
               readDataFromUI();
               // Fall through.
             case "removed":
               currentNotification = null;
-              chromeDoc.getElementById("notification-popup")
-                       .removeEventListener("click", onNotificationClick);
               chromeDoc.getElementById("password-notification-username")
                        .removeEventListener("input", onInput);
               chromeDoc.getElementById("password-notification-password")
                        .removeEventListener("input", onInput);
-              chromeDoc.getElementById("password-notification-password")
-                       .removeEventListener("focus", onPasswordFocus);
-              chromeDoc.getElementById("password-notification-password")
-                       .removeEventListener("blur", onPasswordBlur);
+              chromeDoc.getElementById("password-notification-visibilityToggle")
+                       .removeEventListener("command", onVisibilityToggle);
               break;
           }
           return false;
         },
       }
     );
   },
 
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -34,17 +34,17 @@ support-files =
   subtst_notifications_change_p.html
 [browser_DOMFormHasPassword.js]
 [browser_DOMInputPasswordAdded.js]
 [browser_filldoorhanger.js]
 [browser_hasInsecureLoginForms.js]
 [browser_hasInsecureLoginForms_streamConverter.js]
 [browser_insecurePasswordWarning.js]
 [browser_notifications.js]
-skip-if = true # Intermittent failures: Bug 1182296, bug 1148771
+[browser_notifications_2.js]
 [browser_passwordmgr_editing.js]
 skip-if = os == "linux"
 [browser_context_menu.js]
 skip-if = e10s
 [browser_passwordmgr_contextmenu.js]
 [browser_passwordmgr_fields.js]
 [browser_passwordmgr_observers.js]
 [browser_passwordmgr_sort.js]
--- a/toolkit/components/passwordmgr/test/browser/browser_notifications.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications.js
@@ -44,29 +44,29 @@ add_task(function* test_save_change() {
         function* ({ username, password }) {
           let doc = content.document;
           doc.getElementById("form-basic-username").value = username;
           doc.getElementById("form-basic-password").value = password;
           doc.getElementById("form-basic").submit();
         });
       yield promiseShown;
 
+      let notificationElement = PopupNotifications.panel.childNodes[0];
       // Check the actual content of the popup notification.
-      Assert.equal(document.getElementById("password-notification-username")
+      Assert.equal(notificationElement.querySelector("#password-notification-username")
                            .getAttribute("value"), username);
-      Assert.equal(document.getElementById("password-notification-password")
+      Assert.equal(notificationElement.querySelector("#password-notification-password")
                            .getAttribute("value"), password);
 
       // Simulate the action on the notification to request the login to be
       // saved, and wait for the data to be updated or saved based on the type
       // of operation we expect.
       let expectedNotification = oldPassword ? "modifyLogin" : "addLogin";
       let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
                          (_, data) => data == expectedNotification);
-      let notificationElement = PopupNotifications.panel.childNodes[0];
       notificationElement.button.doCommand();
       let [result] = yield promiseLogin;
 
       // Check that the values in the database match the expected values.
       let login = oldPassword ? result.QueryInterface(Ci.nsIArray)
                                       .queryElementAt(1, Ci.nsILoginInfo)
                               : result.QueryInterface(Ci.nsILoginInfo);
       Assert.equal(login.username, username);
@@ -126,16 +126,17 @@ add_task(function* test_edit_username() 
     if (testCase.usernameInPageExists) {
       Services.logins.addLogin(LoginTestUtils.testData.formLogin({
         hostname: "https://example.com",
         formSubmitURL: "https://example.com",
         username: testCase.usernameInPage,
         password: "old password",
       }));
     }
+
     if (testCase.usernameChangedToExists) {
       Services.logins.addLogin(LoginTestUtils.testData.formLogin({
         hostname: "https://example.com",
         formSubmitURL: "https://example.com",
         username: testCase.usernameChangedTo,
         password: "old password",
       }));
     }
@@ -153,35 +154,35 @@ add_task(function* test_edit_username() 
         function* (usernameInPage) {
           let doc = content.document;
           doc.getElementById("form-basic-username").value = usernameInPage;
           doc.getElementById("form-basic-password").value = "password";
           doc.getElementById("form-basic").submit();
         });
       yield promiseShown;
 
+      let notificationElement = PopupNotifications.panel.childNodes[0];
       // Modify the username in the dialog if requested.
       if (testCase.usernameChangedTo) {
-        document.getElementById("password-notification-username")
+        notificationElement.querySelector("#password-notification-username")
                 .setAttribute("value", testCase.usernameChangedTo);
       }
 
       // We expect a modifyLogin notification if the final username used by the
       // dialog exists in the logins database, otherwise an addLogin one.
       let expectModifyLogin = testCase.usernameChangedTo
                               ? testCase.usernameChangedToExists
                               : testCase.usernameInPageExists;
 
       // Simulate the action on the notification to request the login to be
       // saved, and wait for the data to be updated or saved based on the type
       // of operation we expect.
       let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin";
       let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
                          (_, data) => data == expectedNotification);
-      let notificationElement = PopupNotifications.panel.childNodes[0];
       notificationElement.button.doCommand();
       let [result] = yield promiseLogin;
 
       // Check that the values in the database match the expected values.
       let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray)
                                             .queryElementAt(1, Ci.nsILoginInfo)
                                     : result.QueryInterface(Ci.nsILoginInfo);
       Assert.equal(login.username, testCase.usernameChangedTo ||
@@ -195,18 +196,18 @@ add_task(function* test_edit_username() 
 });
 
 /**
  * Test changing the password inside the doorhanger notification for passwords.
  *
  * We check the following cases:
  *   - Editing the password of a new login.
  *   - Editing the password of an existing login.
- *   - Changing the username and password to an existing login with different password.
- *   - Changing the username and password to an existing login with the same password;
+ *   - Changing both username and password to an existing login.
+ *   - Changing the username to an existing login.
  *   - Editing username to an empty one and a new password.
  *
  * If both the username and password matches an already existing login, we should not
  * update it's password, but only it's usage timestamp and count.
  */
 add_task(function* test_edit_password() {
   let testCases = [{
     usernameInPage: "username",
@@ -236,31 +237,32 @@ add_task(function* test_edit_password() 
     timesUsed: 2,
     checkPasswordNotUpdated: true,
   }, {
     usernameInPage: "newUsername",
     usernameChangedTo: "",
     usernameChangedToExists: true,
     passwordInPage: "password",
     passwordChangedTo: "newPassword",
-    timesUsed: 1,
+    timesUsed: 2,
   }];
 
   for (let testCase of testCases) {
     info("Test case: " + JSON.stringify(testCase));
 
     // Create the pre-existing logins when needed.
     if (testCase.usernameInPageExists) {
       Services.logins.addLogin(LoginTestUtils.testData.formLogin({
         hostname: "https://example.com",
         formSubmitURL: "https://example.com",
         username: testCase.usernameInPage,
         password: testCase.passwordInStorage,
       }));
     }
+
     if (testCase.usernameChangedToExists) {
       Services.logins.addLogin(LoginTestUtils.testData.formLogin({
         hostname: "https://example.com",
         formSubmitURL: "https://example.com",
         username: testCase.usernameChangedTo,
         password: testCase.passwordChangedTo,
       }));
     }
@@ -278,41 +280,41 @@ add_task(function* test_edit_password() 
         function* (testCase) {
           let doc = content.document;
           doc.getElementById("form-basic-username").value = testCase.usernameInPage;
           doc.getElementById("form-basic-password").value = testCase.passwordInPage;
           doc.getElementById("form-basic").submit();
         });
       yield promiseShown;
 
+      let notificationElement = PopupNotifications.panel.childNodes[0];
       // Modify the username in the dialog if requested.
       if (testCase.usernameChangedTo) {
-        document.getElementById("password-notification-username")
+        notificationElement.querySelector("#password-notification-username")
                 .setAttribute("value", testCase.usernameChangedTo);
       }
 
       // Modify the password in the dialog if requested.
       if (testCase.passwordChangedTo) {
-        document.getElementById("password-notification-password")
+        notificationElement.querySelector("#password-notification-password")
                 .setAttribute("value", testCase.passwordChangedTo);
       }
 
       // We expect a modifyLogin notification if the final username used by the
       // dialog exists in the logins database, otherwise an addLogin one.
-      let expectModifyLogin = testCase.usernameChangedTo
+      let expectModifyLogin = typeof testCase.usernameChangedTo !== "undefined"
                               ? testCase.usernameChangedToExists
                               : testCase.usernameInPageExists;
 
       // Simulate the action on the notification to request the login to be
       // saved, and wait for the data to be updated or saved based on the type
       // of operation we expect.
       let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin";
       let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
                          (_, data) => data == expectedNotification);
-      let notificationElement = PopupNotifications.panel.childNodes[0];
       notificationElement.button.doCommand();
       let [result] = yield promiseLogin;
 
       // Check that the values in the database match the expected values.
       let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray)
                                             .queryElementAt(1, Ci.nsILoginInfo)
                                     : result.QueryInterface(Ci.nsILoginInfo);
 
@@ -330,122 +332,8 @@ add_task(function* test_edit_password() 
         Assert.ok(meta.timeCreated == meta.timePasswordChanged);
       }
     });
 
     // Clean up the database before the next test case is executed.
     Services.logins.removeAllLogins();
   }
 });
-
-/**
- * Test that the doorhanger main action button is disabled
- * when the password field is empty.
- *
- * Also checks that submiting an empty password throws an error.
- *
- * We skip Linux for now because focusing elements on the doorhanger
- * doesn't work as expected (Bug 1182296)
- */
-add_task(function* test_empty_password() {
-  if (Services.appinfo.OS == "Linux") {
-    Assert.ok(true, "Skipping test on Linux.");
-    return;
-  }
-  yield BrowserTestUtils.withNewTab({
-      gBrowser,
-      url: "https://example.com/browser/toolkit/components/" +
-           "passwordmgr/test/browser/form_basic.html",
-    }, function* (browser) {
-      // Submit the form in the content page with the credentials from the test
-      // case. This will cause the doorhanger notification to be displayed.
-      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
-                                                       "popupshown");
-      yield ContentTask.spawn(browser, null,
-        function* () {
-          let doc = content.document;
-          doc.getElementById("form-basic-username").value = "username";
-          doc.getElementById("form-basic-password").value = "p";
-          doc.getElementById("form-basic").submit();
-        });
-      yield promiseShown;
-
-      let notificationElement = PopupNotifications.panel.childNodes[0];
-      let passwordTextbox = notificationElement.querySelector("#password-notification-password");
-
-      // Focus the password textbox
-      let focusPassword = BrowserTestUtils.waitForEvent(passwordTextbox, "focus");
-      passwordTextbox.focus();
-      yield focusPassword;
-
-      // Wait for the textbox type to change
-      yield ContentTaskUtils.waitForCondition(() => passwordTextbox.type == "", "Password textbox changed type");
-
-      // Synthesize input to empty the field
-      EventUtils.synthesizeKey("VK_RIGHT", {});
-      yield EventUtils.synthesizeKey("VK_BACK_SPACE", {});
-
-      let mainActionButton = document.getAnonymousElementByAttribute(notificationElement.button, "anonid", "button");
-
-      // Wait for main button to get disabled
-      yield ContentTaskUtils.waitForCondition(() => mainActionButton.disabled, "Main action button is disabled");
-
-      // Makes sure submiting an empty password throws an error
-      Assert.throws(notificationElement.button.doCommand(),
-                    "Can't add a login with a null or empty password.",
-                    "Should fail for an empty password");
-    });
-});
-
-/**
- * Checks that clicking at the doorhanger pane takes the focus
- * out of the password field.
- *
- * We skip Linux for now because focusing elements on the doorhanger
- * doesn't work as expected (Bug 1182296)
- */
-add_task(function* test_unfocus_click() {
-  if (Services.appinfo.OS == "Linux") {
-    Assert.ok(true, "Skipping test on Linux.");
-    return;
-  }
-  yield BrowserTestUtils.withNewTab({
-      gBrowser,
-      url: "https://example.com/browser/toolkit/components/" +
-           "passwordmgr/test/browser/form_basic.html",
-    }, function* (browser) {
-      // Submit the form in the content page with the credentials from the test
-      // case. This will cause the doorhanger notification to be displayed.
-
-      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
-                                                       "popupshown");
-      yield ContentTask.spawn(browser, null,
-        function* () {
-          let doc = content.document;
-          doc.getElementById("form-basic-username").value = "username";
-          doc.getElementById("form-basic-password").value = "password";
-          doc.getElementById("form-basic").submit();
-        });
-      yield promiseShown;
-
-      let notificationElement = PopupNotifications.panel.childNodes[0];
-      let passwordTextbox = notificationElement.querySelector("#password-notification-password");
-
-      // Focus the password textbox
-      let focusPassword = BrowserTestUtils.waitForEvent(passwordTextbox, "focus");
-      passwordTextbox.focus();
-      yield focusPassword;
-
-      // Wait for the textbox type to change
-      yield ContentTaskUtils.waitForCondition(() => passwordTextbox.type == "",
-                                              "Password textbox changed type");
-
-      let notificationIcon = document.getAnonymousElementByAttribute(notificationElement,
-                                                                     "class",
-                                                                     "popup-notification-icon");
-
-      yield EventUtils.synthesizeMouseAtCenter(notificationIcon, {});
-
-      // Wait for the textbox type to change back
-      yield ContentTaskUtils.waitForCondition(() => passwordTextbox.type == "password",
-                                              "Password textbox changed type back to password");
-    });
-});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
@@ -0,0 +1,83 @@
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
+
+/**
+ * Test that the doorhanger main action button is disabled
+ * when the password field is empty.
+ *
+ * Also checks that submiting an empty password throws an error.
+ */
+add_task(function* test_empty_password() {
+  yield BrowserTestUtils.withNewTab({
+      gBrowser,
+      url: "https://example.com/browser/toolkit/components/" +
+           "passwordmgr/test/browser/form_basic.html",
+    }, function* (browser) {
+      // Submit the form in the content page with the credentials from the test
+      // case. This will cause the doorhanger notification to be displayed.
+      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+                                                       "popupshown");
+      yield ContentTask.spawn(browser, null,
+        function* () {
+          let doc = content.document;
+          doc.getElementById("form-basic-username").value = "username";
+          doc.getElementById("form-basic-password").value = "p";
+          doc.getElementById("form-basic").submit();
+        });
+      yield promiseShown;
+
+      let notificationElement = PopupNotifications.panel.childNodes[0];
+      let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+      let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+      // Synthesize input to empty the field
+      passwordTextbox.focus();
+      yield EventUtils.synthesizeKey("VK_RIGHT", {});
+      yield EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+
+      let mainActionButton = document.getAnonymousElementByAttribute(notificationElement.button, "anonid", "button");
+      Assert.ok(mainActionButton.disabled, "Main action button is disabled");
+
+      // Makes sure submiting an empty password throws an error
+      Assert.throws(notificationElement.button.doCommand(),
+                    "Can't add a login with a null or empty password.",
+                    "Should fail for an empty password");
+    });
+});
+
+/**
+ * Test that the doorhanger password field shows plain or * text
+ * when the checkbox is checked.
+ *
+ */
+add_task(function* test_toggle_password() {
+  yield BrowserTestUtils.withNewTab({
+      gBrowser,
+      url: "https://example.com/browser/toolkit/components/" +
+           "passwordmgr/test/browser/form_basic.html",
+    }, function* (browser) {
+      // Submit the form in the content page with the credentials from the test
+      // case. This will cause the doorhanger notification to be displayed.
+      let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+                                                       "popupshown");
+      yield ContentTask.spawn(browser, null,
+        function* () {
+          let doc = content.document;
+          doc.getElementById("form-basic-username").value = "username";
+          doc.getElementById("form-basic-password").value = "p";
+          doc.getElementById("form-basic").submit();
+        });
+      yield promiseShown;
+
+      let notificationElement = PopupNotifications.panel.childNodes[0];
+      let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+      let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+      yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
+      Assert.ok(toggleCheckbox.checked);
+      Assert.equal(passwordTextbox.type, "", "Password textbox changed to plain text");
+
+      yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
+      Assert.ok(!toggleCheckbox.checked);
+      Assert.equal(passwordTextbox.type, "password", "Password textbox changed to * text");
+    });
+});
--- a/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
+++ b/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
@@ -10,29 +10,27 @@ rememberLoginMsg = Would you like %S to 
 rememberLoginMsgNoUser = Would you like %S to remember this password?
 rememberLoginButtonText = Remember
 rememberLoginButtonAccessKey = R
 updateLoginMsg = Would you like to update this login?
 updateLoginMsgNoUser = Would you like to update this password?
 updateLoginButtonText = Update
 updateLoginButtonAccessKey = U
 # LOCALIZATION NOTE (rememberPasswordMsg):
-# 1st string is the username for the login, 2nd is the login's hostname. 
+# 1st string is the username for the login, 2nd is the login's hostname.
 # Note that long usernames may be truncated.
 rememberPasswordMsg = Would you like to remember the password for ā€œ%1$Sā€ on %2$S?
 # LOCALIZATION NOTE (rememberPasswordMsgNoUsername):
 # String is the login's hostname.
 rememberPasswordMsgNoUsername = Would you like to remember the password on %S?
 # LOCALIZATION NOTE (noUsernamePlaceholder):
 # This is displayed in place of the username when it is missing.
 noUsernamePlaceholder=No username
-# LOCALIZATION NOTE (showPasswordPlaceholder):
-# This is displayed in the password field to indicate that the password will be
-# shown if focused.
-showPasswordPlaceholder=SHOW
+togglePasswordLabel=Show password
+togglePasswordAccessKey=S
 notNowButtonText = &Not Now
 notifyBarNotNowButtonText = Not Now
 notifyBarNotNowButtonAccessKey = N
 neverForSiteButtonText = Ne&ver for This Site
 notifyBarNeverRememberButtonText = Never Remember Password for This Site
 notifyBarNeverRememberButtonAccessKey = e
 rememberButtonText = &Remember
 notifyBarRememberPasswordButtonText = Remember Password