Bug 1185000 - Show a dismissed password manager doorhanger when credit card numbers are detected. r=jaws
authorprathiksha <prathikshaprasadsuman@gmail.com>
Fri, 19 Apr 2019 13:52:58 -0700
changeset 470329 f8541439e10186980ecea712820310b08144ec96
parent 470328 7701a0d0fb8bc54265cd7d2f55577e0356cdc43c
child 470330 b783cd5203ea589bb7505852e5108ed142d2d37a
child 470373 2e9187dc17a928962173b04bf39dcdee57805dd8
push id35899
push usershindli@mozilla.com
push dateMon, 22 Apr 2019 09:42:40 +0000
treeherdermozilla-central@b783cd5203ea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1185000
milestone68.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 1185000 - Show a dismissed password manager doorhanger when credit card numbers are detected. r=jaws In certain straight-forward cases where we detect a credit card number being used with password fields we will show a dismissed password manager doorhanger. The user can still choose to save in case the valid credit card number is actually their username or password. 1) If the Luhn checksum matches on the username field (see CreditCard.jsm) AND the password is 3 numerical digits (don't handle 4 for now even though it's used by Visa since there are banks that use 4 digits passwords for online banking still). 2) If the Luhn checksum matches on the password value AND we detect that the type=password field is a credit card field via autocomplete=cc-number. ** We must include the @autocomplete check otherwise sites will abuse this loophole on legit login forms and set autocomplete=cc-number on their password fields to avoid saving. For both of these cases we should `dismissed:true` doorhanger, rather than not showing one at all, in case there are false-negatives. Differential Revision: https://phabricator.services.mozilla.com/D25485
mobile/android/components/LoginManagerPrompter.js
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/passwordmgr/LoginManagerPrompter.jsm
toolkit/components/passwordmgr/nsILoginManagerPrompter.idl
toolkit/components/passwordmgr/test/browser/browser.ini
toolkit/components/passwordmgr/test/browser/browser_doorhanger_dismissed_for_ccnumber.js
--- a/mobile/android/components/LoginManagerPrompter.js
+++ b/mobile/android/components/LoginManagerPrompter.js
@@ -102,36 +102,38 @@ LoginManagerPrompter.prototype = {
   // setting this attribute is ignored because Android does not consider
   // opener windows when displaying login notifications
   set opener(aOpener) { },
 
   /*
    * promptToSavePassword
    *
    */
-  promptToSavePassword: function(aLogin) {
-    this._showSaveLoginNotification(aLogin);
+  promptToSavePassword: function(aLogin, dismissed) {
+    this._showSaveLoginNotification(aLogin, dismissed);
       Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
     Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save");
   },
 
   /*
    * _showLoginNotification
    *
    * Displays a notification doorhanger.
    * @param aBody
    *        String message to be displayed in the doorhanger
    * @param aButtons
    *        Buttons to display with the doorhanger
    * @param aUsername
    *        Username string used in creating a doorhanger action
    * @param aPassword
    *        Password string used in creating a doorhanger action
+   * @param dismissed
+   *        A boolean indicating if a prompt is dismissed by default.
    */
-  _showLoginNotification: function(aBody, aButtons, aUsername, aPassword) {
+  _showLoginNotification: function(aBody, aButtons, aUsername, aPassword, dismissed = false) {
     let actionText = {
       text: aUsername,
       type: "EDIT",
       bundle: { username: aUsername,
       password: aPassword },
     };
 
     // The page we're going to hasn't loaded yet, so we want to persist
@@ -140,31 +142,32 @@ LoginManagerPrompter.prototype = {
     // Sites like Gmail perform a funky redirect dance before you end up
     // at the post-authentication page. I don't see a good way to
     // heuristically determine when to ignore such location changes, so
     // we'll try ignoring location changes based on a time interval.
     let options = {
       persistWhileVisible: true,
       timeout: Date.now() + 10000,
       actionText: actionText,
+      dismissed,
     };
 
     let win = (this._browser && this._browser.contentWindow) || this._window;
     DoorHanger.show(win, aBody, "password", aButtons, options, "LOGIN");
   },
 
   /*
    * _showSaveLoginNotification
    *
    * Displays a notification doorhanger (rather than a popup), to allow the user to
    * save the specified login. This allows the user to see the results of
    * their login, and only save a login which they know worked.
    *
    */
-  _showSaveLoginNotification: function(aLogin) {
+  _showSaveLoginNotification: function(aLogin, dismissed) {
     let brandShortName = this._strBundle.brand.GetStringFromName("brandShortName");
     let notificationText  = this._getLocalizedString("saveLogin", [brandShortName]);
 
     // The callbacks in |buttons| have a closure to access the variables
     // in scope here; set one to |Services.logins| so we can get back to pwmgr
     // without a getService() call.
     var pwmgr = Services.logins;
     let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION");
@@ -186,41 +189,41 @@ LoginManagerPrompter.prototype = {
           }
           pwmgr.addLogin(aLogin);
           promptHistogram.add(PROMPT_ADD);
         },
         positive: true,
       },
     ];
 
-    this._showLoginNotification(notificationText, buttons, aLogin.username, aLogin.password);
+    this._showLoginNotification(notificationText, buttons, aLogin.username, aLogin.password, dismissed);
   },
 
   /*
    * promptToChangePassword
    *
    * Called when we think we detect a password change for an existing
    * login, when the form being submitted contains multiple password
    * fields.
    *
    */
-  promptToChangePassword: function(aOldLogin, aNewLogin) {
+  promptToChangePassword: function(aOldLogin, aNewLogin, dismissed) {
     this._showChangeLoginNotification(aOldLogin, aNewLogin.password);
     Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION").add(PROMPT_DISPLAYED);
     let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
     Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
   },
 
   /*
    * _showChangeLoginNotification
    *
    * Shows the Change Password notification doorhanger.
    *
    */
-  _showChangeLoginNotification: function(aOldLogin, aNewPassword) {
+  _showChangeLoginNotification: function(aOldLogin, aNewPassword, dismissed) {
     var notificationText;
     if (aOldLogin.username) {
       let displayUser = this._sanitizeUsername(aOldLogin.username);
       notificationText  = this._getLocalizedString("updatePassword", [displayUser]);
     } else {
       notificationText  = this._getLocalizedString("updatePasswordNoUser");
     }
 
@@ -242,17 +245,17 @@ LoginManagerPrompter.prototype = {
           self._updateLogin(aOldLogin, password);
 
           promptHistogram.add(PROMPT_UPDATE);
         },
         positive: true,
       },
     ];
 
-    this._showLoginNotification(notificationText, buttons, aOldLogin.username, aNewPassword);
+    this._showLoginNotification(notificationText, buttons, aOldLogin.username, aNewPassword, dismissed);
   },
 
   /*
    * promptToChangePasswordWithUsernames
    *
    * Called when we detect a password change in a form submission, but we
    * don't know which existing login (username) it's for. Asks the user
    * to select a username and confirm the password change.
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -16,16 +16,17 @@ var EXPORTED_SYMBOLS = ["LoginManagerCon
 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");
+const {CreditCard} = ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
 
 ChromeUtils.defineModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
 ChromeUtils.defineModuleGetter(this, "FormLikeFactory",
                                "resource://gre/modules/FormLikeFactory.jsm");
 ChromeUtils.defineModuleGetter(this, "LoginFormFactory",
                                "resource://gre/modules/LoginFormFactory.jsm");
 ChromeUtils.defineModuleGetter(this, "LoginRecipesContent",
                                "resource://gre/modules/LoginRecipes.jsm");
@@ -1154,25 +1155,37 @@ var LoginManagerContent = {
     });
 
     // Make sure to pass the opener's top ID in case it was in a frame.
     let openerTopWindowID = null;
     if (win.opener) {
       openerTopWindowID = win.opener.top.windowUtils.outerWindowID;
     }
 
+    // Dismiss prompt if the username field is a credit card number AND
+    // if the password field is a three digit number. Also dismiss prompt if
+    // the password is a credit card number and the password field has attribute
+    // autocomplete="cc-number".
+    let dismissedPrompt = false;
+    let newPasswordFieldValue = newPasswordField.value;
+    if ((CreditCard.isValidNumber(usernameValue) && newPasswordFieldValue.trim().match(/^[0-9]{3}$/)) ||
+        (CreditCard.isValidNumber(newPasswordFieldValue) && newPasswordField.getAutocompleteInfo().fieldName == "cc-number")) {
+      dismissedPrompt = true;
+    }
+
     let autoFilledLogin = this.stateForDocument(doc).fillsByRootElement.get(form.rootElement);
     messageManager.sendAsyncMessage("PasswordManager:onFormSubmit",
                                     { hostname,
                                       formSubmitURL,
                                       autoFilledLoginGuid: autoFilledLogin && autoFilledLogin.guid,
                                       usernameField: mockUsername,
                                       newPasswordField: mockPassword,
                                       oldPasswordField: mockOldPassword,
                                       openerTopWindowID,
+                                      dismissedPrompt,
                                     });
   },
 
   /** 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);
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -95,16 +95,17 @@ var LoginManagerParent = {
         // TODO Verify msg.target's principals against the formOrigin?
         this.onFormSubmit({hostname: data.hostname,
                            formSubmitURL: data.formSubmitURL,
                            autoFilledLoginGuid: data.autoFilledLoginGuid,
                            usernameField: data.usernameField,
                            newPasswordField: data.newPasswordField,
                            oldPasswordField: data.oldPasswordField,
                            openerTopWindowID: data.openerTopWindowID,
+                           dismissedPrompt: data.dismissedPrompt,
                            target: msg.target});
         break;
       }
 
       case "PasswordManager:insecureLoginFormPresent": {
         this.setHasInsecureLoginForms(msg.target, data.hasInsecureLoginForms);
         break;
       }
@@ -298,17 +299,17 @@ var LoginManagerParent = {
       requestId,
       logins: jsLogins,
     });
   },
 
   onFormSubmit({hostname, formSubmitURL, autoFilledLoginGuid,
                 usernameField, newPasswordField,
                 oldPasswordField, openerTopWindowID,
-                target}) {
+                dismissedPrompt, target}) {
     function getPrompter() {
       var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
                         createInstance(Ci.nsILoginManagerPrompter);
       prompterSvc.init(target.ownerGlobal);
       prompterSvc.browser = target;
 
       for (let win of Services.wm.getEnumerator(null)) {
         let tabbrowser = win.gBrowser;
@@ -382,17 +383,17 @@ var LoginManagerParent = {
           log("(Not prompting to save/change since we have no username and the " +
               "only saved password matches the new password)");
           return;
         }
 
         formLogin.username      = oldLogin.username;
         formLogin.usernameField = oldLogin.usernameField;
 
-        prompter.promptToChangePassword(oldLogin, formLogin);
+        prompter.promptToChangePassword(oldLogin, formLogin, dismissedPrompt);
       } else {
         // Note: It's possible that that we already have the correct u+p saved
         // but since we don't have the username, we don't know if the user is
         // changing a second account to the new password so we ask anyways.
 
         prompter.promptToChangePasswordWithUsernames(
           logins, logins.length, formLogin);
       }
@@ -440,32 +441,31 @@ var LoginManagerParent = {
 
     if (existingLogin) {
       log("Found an existing login matching this form submission");
 
       // Change password if needed.
       if (existingLogin.password != formLogin.password) {
         log("...passwords differ, prompting to change.");
         prompter = getPrompter();
-        prompter.promptToChangePassword(existingLogin, formLogin);
+        prompter.promptToChangePassword(existingLogin, formLogin, dismissedPrompt);
       } else if (!existingLogin.username && formLogin.username) {
         log("...empty username update, prompting to change.");
         prompter = getPrompter();
-        prompter.promptToChangePassword(existingLogin, formLogin);
+        prompter.promptToChangePassword(existingLogin, formLogin, dismissedPrompt);
       } else {
         recordLoginUse(existingLogin);
       }
 
       return;
     }
 
-
     // Prompt user to save login (via dialog or notification bar)
     prompter = getPrompter();
-    prompter.promptToSavePassword(formLogin);
+    prompter.promptToSavePassword(formLogin, dismissedPrompt);
   },
 
   /**
    * Maps all the <browser> elements for tabs in the parent process to the
    * current state used to display tab-specific UI.
    *
    * This mapping is not updated in case a web page is moved to a different
    * chrome window by the swapDocShells method. In this case, it is possible
--- a/toolkit/components/passwordmgr/LoginManagerPrompter.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerPrompter.jsm
@@ -807,22 +807,22 @@ LoginManagerPrompter.prototype = {
   set browser(aBrowser) {
     this._browser = aBrowser;
   },
 
   set openerBrowser(aOpenerBrowser) {
     this._openerBrowser = aOpenerBrowser;
   },
 
-  promptToSavePassword(aLogin) {
+  promptToSavePassword(aLogin, dismissed) {
     this.log("promptToSavePassword");
     var notifyObj = this._getPopupNote();
     if (notifyObj) {
       this._showLoginCaptureDoorhanger(aLogin, "password-save", {
-        dismissed: this._inPrivateBrowsing,
+        dismissed: this._inPrivateBrowsing || dismissed,
       });
       Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save");
     } else {
       this._showSaveLoginDialog(aLogin);
     }
   },
 
   /**
@@ -1171,47 +1171,53 @@ LoginManagerPrompter.prototype = {
    * Called when we think we detect a password or username change for
    * an existing login, when the form being submitted contains multiple
    * password fields.
    *
    * @param {nsILoginInfo} aOldLogin
    *                       The old login we may want to update.
    * @param {nsILoginInfo} aNewLogin
    *                       The new login from the page form.
+   * @param dismissed
+   *        A boolean indicating if the prompt should be automatically
+   *        dismissed on being shown.
    */
-  promptToChangePassword(aOldLogin, aNewLogin) {
+  promptToChangePassword(aOldLogin, aNewLogin, dismissed) {
     this.log("promptToChangePassword");
     let notifyObj = this._getPopupNote();
 
     if (notifyObj) {
       this._showChangeLoginNotification(notifyObj, aOldLogin,
-                                        aNewLogin);
+                                        aNewLogin, dismissed);
     } else {
       this._showChangeLoginDialog(aOldLogin, aNewLogin);
     }
   },
 
   /**
    * Shows the Change Password popup notification.
    *
    * @param aNotifyObj
    *        A popup notification.
    *
    * @param aOldLogin
    *        The stored login we want to update.
    *
    * @param aNewLogin
    *        The login object with the changes we want to make.
+   * @param dismissed
+   *        A boolean indicating if the prompt should be automatically
+   *        dismissed on being shown.
    */
-  _showChangeLoginNotification(aNotifyObj, aOldLogin, aNewLogin) {
+  _showChangeLoginNotification(aNotifyObj, aOldLogin, aNewLogin, dismissed = false) {
     aOldLogin.hostname = aNewLogin.hostname;
     aOldLogin.formSubmitURL = aNewLogin.formSubmitURL;
     aOldLogin.password = aNewLogin.password;
     aOldLogin.username = aNewLogin.username;
-    this._showLoginCaptureDoorhanger(aOldLogin, "password-change");
+    this._showLoginCaptureDoorhanger(aOldLogin, "password-change", {dismissed});
 
     let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
     Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
   },
 
 
   /**
    * Shows the Change Password dialog.
--- a/toolkit/components/passwordmgr/nsILoginManagerPrompter.idl
+++ b/toolkit/components/passwordmgr/nsILoginManagerPrompter.idl
@@ -44,30 +44,37 @@ interface nsILoginManagerPrompter : nsIS
    */
   attribute Element openerBrowser;
 
   /**
    * Ask the user if they want to save a login (Yes, Never, Not Now)
    *
    * @param aLogin
    *        The login to be saved.
+   * @param dismissed
+   *        A boolean value indicating whether the save logins doorhanger should
+   *        be dismissed automatically when shown.
    */
-  void promptToSavePassword(in nsILoginInfo aLogin);
+  void promptToSavePassword(in nsILoginInfo aLogin, in boolean dismissed);
 
   /**
    * Ask the user if they want to change a login's password or username.
    * If the user consents, modifyLogin() will be called.
    *
    * @param aOldLogin
    *        The existing login (with the old password).
    * @param aNewLogin
    *        The new login.
+   * @param dismissed
+   *        A boolean value indicating whether the save logins doorhanger should
+   *        be dismissed automatically when shown.
    */
   void promptToChangePassword(in nsILoginInfo aOldLogin,
-                              in nsILoginInfo aNewLogin);
+                              in nsILoginInfo aNewLogin,
+                              in boolean dismissed);
 
   /**
    * Ask the user if they want to change the password for one of
    * multiple logins, when the caller can't determine exactly which
    * login should be changed. If the user consents, modifyLogin() will
    * be called.
    *
    * @param logins
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -44,16 +44,17 @@ support-files =
   subtst_notifications_11.html
   subtst_notifications_11_popup.html
 skip-if = os == "linux" # Bug 1312981, bug 1313136
 [browser_context_menu_autocomplete_interaction.js]
 skip-if = verify
 [browser_username_select_dialog.js]
 support-files =
   subtst_notifications_change_p.html
+[browser_doorhanger_dismissed_for_ccnumber.js]
 [browser_DOMFormHasPassword.js]
 [browser_DOMInputPasswordAdded.js]
 skip-if = (os == "linux") || (os == "mac") # Bug 1337606
 [browser_exceptions_dialog.js]
 [browser_focus_before_first_DOMContentLoaded.js]
 support-files =
   file_focus_before_DOMContentLoaded.sjs
 [browser_formless_submit_chrome.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_dismissed_for_ccnumber.js
@@ -0,0 +1,115 @@
+"use strict";
+
+const TEST_HOSTNAME = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+
+function getSubmitMessage() {
+  info("getSubmitMessage");
+  return new Promise((resolve, reject) => {
+    Services.mm.addMessageListener("PasswordManager:onFormSubmit", function onFormSubmit() {
+      Services.mm.removeMessageListener("PasswordManager:onFormSubmit", onFormSubmit);
+      resolve();
+    });
+  });
+}
+
+add_task(async function test_doorhanger_dismissal_un() {
+  let url = TEST_HOSTNAME + BASIC_FORM_PAGE_PATH;
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url,
+  }, async function test_un_value_as_ccnumber(browser) {
+    // If the username field has a credit card number and if
+    // the password field is a three digit numberic value,
+    // we automatically dismiss the save logins prompt on submission.
+
+    let processedPromise = getSubmitMessage();
+    ContentTask.spawn(browser, null, async () => {
+      content.document.getElementById("form-basic-username").setUserInput("4111111111111111");
+      content.document.getElementById("form-basic-password").setUserInput("123");
+
+      content.document.getElementById("form-basic-submit").click();
+    });
+    await processedPromise;
+
+    let notif = getCaptureDoorhanger("password-save");
+    ok(notif, "got notification popup");
+    ok(notif.dismissed, "notification popup was automatically dismissed");
+  });
+});
+
+add_task(async function test_doorhanger_dismissal_pw() {
+  let url = TEST_HOSTNAME + BASIC_FORM_PAGE_PATH;
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url,
+  }, async function test_pw_value_as_ccnumber(browser) {
+    // If the password field has a credit card number and if
+    // the password field is also tagged autocomplete="cc-number",
+    // we automatically dismiss the save logins prompt on submission.
+
+    let processedPromise = getSubmitMessage();
+    ContentTask.spawn(browser, null, async () => {
+      content.document.getElementById("form-basic-username").setUserInput("aaa");
+      content.document.getElementById("form-basic-password").setUserInput("4111111111111111");
+      content.document.getElementById("form-basic-password").setAttribute("autocomplete", "cc-number");
+
+      content.document.getElementById("form-basic-submit").click();
+    });
+    await processedPromise;
+
+    let notif = getCaptureDoorhanger("password-save");
+    ok(notif, "got notification popup");
+    ok(notif.dismissed, "notification popup was automatically dismissed");
+  });
+});
+
+add_task(async function test_doorhanger_shown_on_un_with_invalid_ccnumber() {
+  let url = TEST_HOSTNAME + BASIC_FORM_PAGE_PATH;
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url,
+  }, async function test_un_with_invalid_cc_number(browser) {
+    // If the username field has a CC number that is invalid,
+    // we show the doorhanger to save logins like we usually do.
+
+    let processedPromise = getSubmitMessage();
+    ContentTask.spawn(browser, null, async () => {
+      content.document.getElementById("form-basic-username").value = "1234123412341234";
+      content.document.getElementById("form-basic-password").value = "411";
+
+      content.document.getElementById("form-basic-submit").click();
+    });
+    await processedPromise;
+
+    let notif = getCaptureDoorhanger("password-save");
+    ok(notif, "got notification popup");
+    ok(!notif.dismissed, "notification popup was not automatically dismissed");
+  });
+});
+
+add_task(async function test_doorhanger_dismissal_on_change() {
+  let url = TEST_HOSTNAME + BASIC_FORM_PAGE_PATH;
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url,
+  }, async function test_change_in_pw(browser) {
+    let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+                                                 Ci.nsILoginInfo, "init");
+    let login = new nsLoginInfo("https://example.org", "https://example.org", null,
+                                "4111111111111111", "111", "form-basic-username", "form-basic-password");
+    Services.logins.addLogin(login);
+
+    let processedPromise = getSubmitMessage();
+    ContentTask.spawn(browser, null, async () => {
+      content.document.getElementById("form-basic-password").setUserInput("111");
+      content.document.getElementById("form-basic-submit").click();
+    });
+    await processedPromise;
+
+    let notif = getCaptureDoorhanger("password-save");
+    ok(notif, "got notification popup");
+    ok(notif.dismissed, "notification popup was automatically dismissed");
+  });
+});
+