Bug 1576199 - Avoid sending redundant messages from LMC to trigger notifications from the parent. r=MattN a=lizzard
authorSam Foster <sfoster@mozilla.com>
Fri, 20 Sep 2019 00:34:40 +0000
changeset 555228 ff9d611b5c723c071d39362b9693dda0089bb264
parent 555227 d855eaf4028bdd96b0a74600ede3e7509ac0b663
child 555229 8ab819881b9166ec53fc9f72dd08b7daad1b4769
push id2165
push userffxbld-merge
push dateMon, 14 Oct 2019 16:30:58 +0000
treeherdermozilla-release@0eae18af659f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, lizzard
bugs1576199
milestone70.0
Bug 1576199 - Avoid sending redundant messages from LMC to trigger notifications from the parent. r=MattN a=lizzard Differential Revision: https://phabricator.services.mozilla.com/D45878
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
toolkit/components/passwordmgr/test/browser/browser_generated_password_doorhangers.js
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -355,16 +355,56 @@ this.LoginManagerContent = {
     messageManager.sendAsyncMessage(name, messageData);
 
     let deferred = PromiseUtils.defer();
     requestData.promise = deferred;
     this._requests.set(requestId, requestData);
     return deferred.promise;
   },
 
+  _compareAndUpdatePreviouslySentValues(
+    formLikeRoot,
+    usernameValue,
+    passwordValue,
+    dismissed = false
+  ) {
+    let state = this.stateForDocument(formLikeRoot.ownerDocument);
+    const lastSentValues = state.lastSubmittedValuesByRootElement.get(
+      formLikeRoot
+    );
+    if (lastSentValues) {
+      if (dismissed && !lastSentValues.dismissed) {
+        // preserve previous dismissed value if it was false (i.e. shown/open)
+        dismissed = false;
+      }
+      if (
+        lastSentValues.username == usernameValue &&
+        lastSentValues.password == passwordValue &&
+        lastSentValues.dismissed == dismissed
+      ) {
+        log(
+          "_compareAndUpdatePreviouslySentValues: values are equivalent, returning true"
+        );
+        return true;
+      }
+    }
+
+    // Save the last submitted values so we don't prompt twice for the same values using
+    // different capture methods e.g. a form submit event and upon navigation.
+    state.lastSubmittedValuesByRootElement.set(formLikeRoot, {
+      username: usernameValue,
+      password: passwordValue,
+      dismissed,
+    });
+    log(
+      "_compareAndUpdatePreviouslySentValues: values not equivalent, returning false"
+    );
+    return false;
+  },
+
   _onKeyDown(event) {
     let focusedElement = LoginManagerContent._formFillService.focusedInput;
     if (
       event.keyCode != event.DOM_VK_RETURN ||
       focusedElement != event.target
     ) {
       this._keyDownEnterForInput = null;
       return;
@@ -1464,60 +1504,51 @@ this.LoginManagerContent = {
       value: newPasswordField.value,
     };
     let mockOldPassword = oldPasswordField
       ? { name: oldPasswordField.name, value: oldPasswordField.value }
       : null;
 
     let usernameValue = usernameField ? usernameField.value : null;
     let formLikeRoot = FormLikeFactory.findRootForField(newPasswordField);
-    let state = this.stateForDocument(doc);
-    let lastSubmittedValues = state.lastSubmittedValuesByRootElement.get(
-      formLikeRoot
-    );
-    if (lastSubmittedValues) {
-      if (
-        lastSubmittedValues.username == usernameValue &&
-        lastSubmittedValues.password == newPasswordField.value
-      ) {
-        log(
-          "(form submission ignored -- already submitted with the same username and password)"
-        );
-        return;
-      }
-    }
-
-    // Save the last submitted values so we don't prompt twice for the same values using
-    // different capture methods e.g. a form submit event and upon navigation.
-    state.lastSubmittedValuesByRootElement.set(formLikeRoot, {
-      username: usernameValue,
-      password: newPasswordField.value,
-    });
-
-    // 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;
     }
 
+    if (
+      this._compareAndUpdatePreviouslySentValues(
+        formLikeRoot,
+        usernameValue,
+        newPasswordField.value,
+        dismissedPrompt
+      )
+    ) {
+      log(
+        "(form submission ignored -- already submitted with the same username and password)"
+      );
+      return;
+    }
+
+    // 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;
+    }
+
     let autoFilledLogin = this.stateForDocument(doc).fillsByRootElement.get(
       form.rootElement
     );
     messageManager.sendAsyncMessage("PasswordManager:onFormSubmit", {
       origin,
       formActionOrigin,
       autoFilledLoginGuid: autoFilledLogin && autoFilledLogin.guid,
       usernameField: mockUsername,
@@ -1564,16 +1595,17 @@ this.LoginManagerContent = {
 
     if (!LoginHelper.enabled) {
       throw new Error(
         "A generated password was filled while the password manager was disabled."
       );
     }
 
     let win = passwordField.ownerGlobal;
+    let formLikeRoot = FormLikeFactory.findRootForField(passwordField);
 
     this._highlightFilledField(passwordField);
 
     // change: Listen for changes to the field filled with the generated password so we can preserve edits.
     // input: Listen for the field getting blanked (without blurring) or a paste
     for (let eventType of ["blur", "change", "focus", "input"]) {
       passwordField.addEventListener(eventType, observer, {
         capture: true,
@@ -1592,29 +1624,46 @@ this.LoginManagerContent = {
 
     let loginForm = LoginFormFactory.createFromField(passwordField);
     let formActionOrigin = LoginHelper.getFormActionOrigin(loginForm);
     let origin = LoginHelper.getLoginOrigin(
       passwordField.ownerDocument.documentURI
     );
     let recipes = LoginRecipesContent.getRecipes(origin, win);
     let [usernameField] = this._getFormFields(loginForm, false, recipes);
+    let username = (usernameField && usernameField.value) || "";
+    // Avoid prompting twice for the same value,
+    // e.g. context menu fill followed by change (blur) event
+    if (
+      this._compareAndUpdatePreviouslySentValues(
+        formLikeRoot,
+        username,
+        passwordField.value,
+        true // dismissed
+      )
+    ) {
+      log(
+        "(generatedPasswordFilledOrEdited ignored -- already messaged with the same password value)"
+      );
+      return;
+    }
+
     let openerTopWindowID = null;
     if (win.opener) {
       openerTopWindowID = win.opener.top.windowUtils.outerWindowID;
     }
     let messageManager = win.docShell.messageManager;
     messageManager.sendAsyncMessage(
       "PasswordManager:onGeneratedPasswordFilledOrEdited",
       {
         browsingContextId: win.docShell.browsingContext.id,
         formActionOrigin,
         openerTopWindowID,
         password: passwordField.value,
-        username: (usernameField && usernameField.value) || "",
+        username,
       }
     );
   },
 
   _togglePasswordFieldMasking(passwordField, unmask) {
     let { editor } = passwordField;
 
     if (passwordField.type != "password") {
--- a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
@@ -888,11 +888,8 @@ add_task(async function test_showPasswor
     is(
       passwordVisiblityToggle.hidden,
       true,
       "Check that the Show Password field is Hidden"
     );
     await cleanupDoorhanger(notif);
   });
 });
-
-// TODO:
-// * existing login test, form has different password --> change password, no save prompt
--- a/toolkit/components/passwordmgr/test/browser/browser_generated_password_doorhangers.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_generated_password_doorhangers.js
@@ -860,205 +860,189 @@ add_task(async function contextmenu_pass
       // Check no new doorhanger was shown
       notif = getCaptureDoorhanger("password-change");
       ok(!notif, "No new doorhanger should be shown");
       await cleanupDoorhanger(); // cleanup for next test
     }
   );
 });
 
-add_task(async function autosaved_login_updated_to_existing_login() {
-  // test when filling with a generated password and editing the username in the
-  // doorhanger to match an existing login:
-  // * the matching login should be updated
-  // * the auto-saved login should be deleted
-  // * the metadata for the matching login should be updated
-  // * the by-origin cache for the password should point at the updated login
-  await setup_withOneLogin("user1", "xyzpassword");
-  await LoginTestUtils.addLogin({ username: "user2", password: "abcpassword" });
-  await openFormInNewTab(
-    TEST_ORIGIN + FORM_PAGE_PATH,
-    {
-      password: {
-        selector: passwordInputSelector,
-        expectedValue: "",
-      },
-      username: {
-        selector: usernameInputSelector,
-        expectedValue: "",
+add_task(
+  async function autosaved_login_updated_to_existing_login_via_doorhanger() {
+    // test when filling with a generated password and editing the username in the
+    // doorhanger to match an existing login:
+    // * the matching login should be updated
+    // * the auto-saved login should be deleted
+    // * the metadata for the matching login should be updated
+    // * the by-origin cache for the password should point at the updated login
+    await setup_withOneLogin("user1", "xyzpassword");
+    await LoginTestUtils.addLogin({
+      username: "user2",
+      password: "abcpassword",
+    });
+    await openFormInNewTab(
+      TEST_ORIGIN + FORM_PAGE_PATH,
+      {
+        password: {
+          selector: passwordInputSelector,
+          expectedValue: "",
+        },
+        username: {
+          selector: usernameInputSelector,
+          expectedValue: "",
+        },
       },
-    },
-    async function taskFn(browser) {
-      await SimpleTest.promiseFocus(browser.ownerGlobal);
+      async function taskFn(browser) {
+        await SimpleTest.promiseFocus(browser.ownerGlobal);
 
-      let storageChangedPromise = TestUtils.topicObserved(
-        "passwordmgr-storage-changed",
-        (_, data) => data == "addLogin"
-      );
-      let confirmationHint = document.getElementById("confirmation-hint");
-      let hintPromiseShown = BrowserTestUtils.waitForEvent(
-        confirmationHint,
-        "popupshown"
-      );
+        let storageChangedPromise = TestUtils.topicObserved(
+          "passwordmgr-storage-changed",
+          (_, data) => data == "addLogin"
+        );
+        let confirmationHint = document.getElementById("confirmation-hint");
+        let hintPromiseShown = BrowserTestUtils.waitForEvent(
+          confirmationHint,
+          "popupshown"
+        );
 
-      info("waiting to fill generated password using context menu");
-      await doFillGeneratedPasswordContextMenuItem(
-        browser,
-        passwordInputSelector
-      );
+        info("waiting to fill generated password using context menu");
+        await doFillGeneratedPasswordContextMenuItem(
+          browser,
+          passwordInputSelector
+        );
 
-      info("waiting for dismissed password-change notification");
-      await waitForDoorhanger(browser, "password-change");
-      // Make sure confirmation hint was shown
-      await hintPromiseShown;
-      await verifyConfirmationHint(confirmationHint);
+        info("waiting for dismissed password-change notification");
+        await waitForDoorhanger(browser, "password-change");
+        // Make sure confirmation hint was shown
+        await hintPromiseShown;
+        await verifyConfirmationHint(confirmationHint);
 
-      info("waiting for addLogin");
-      await storageChangedPromise;
-      info("addLogin promise resolved");
-      // Check properties of the newly auto-saved login
-      let [user1LoginSnapshot, unused, autoSavedLogin] = verifyLogins([
-        null, // ignore the first one
-        null, // ignore the 2nd one
-        {
-          timesUsed: 1,
-          username: "",
-          passwordLength: LoginTestUtils.generation.LENGTH,
-        },
-      ]);
-      info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
-      info("unused, guid: " + unused.guid);
-      info("autoSavedLogin, guid: " + autoSavedLogin.guid);
+        info("waiting for addLogin");
+        await storageChangedPromise;
+        info("addLogin promise resolved");
+        // Check properties of the newly auto-saved login
+        let [user1LoginSnapshot, unused, autoSavedLogin] = verifyLogins([
+          null, // ignore the first one
+          null, // ignore the 2nd one
+          {
+            timesUsed: 1,
+            username: "",
+            passwordLength: LoginTestUtils.generation.LENGTH,
+          },
+        ]);
+        info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
+        info("unused, guid: " + unused.guid);
+        info("autoSavedLogin, guid: " + autoSavedLogin.guid);
 
-      info("verifyLogins ok");
-      let passwordCacheEntry = LoginManagerParent._generatedPasswordsByPrincipalOrigin.get(
-        "https://example.com"
-      );
+        info("verifyLogins ok");
+        let passwordCacheEntry = LoginManagerParent._generatedPasswordsByPrincipalOrigin.get(
+          "https://example.com"
+        );
 
-      ok(
-        passwordCacheEntry,
-        "Got the cached generated password entry for https://example.com"
-      );
-      is(
-        passwordCacheEntry.value,
-        autoSavedLogin.password,
-        "Cached password matches the auto-saved login password"
-      );
-      is(
-        passwordCacheEntry.storageGUID,
-        autoSavedLogin.guid,
-        "Cached password guid matches the auto-saved login guid"
-      );
+        ok(
+          passwordCacheEntry,
+          "Got the cached generated password entry for https://example.com"
+        );
+        is(
+          passwordCacheEntry.value,
+          autoSavedLogin.password,
+          "Cached password matches the auto-saved login password"
+        );
+        is(
+          passwordCacheEntry.storageGUID,
+          autoSavedLogin.guid,
+          "Cached password guid matches the auto-saved login guid"
+        );
 
-      let messagePromise = new Promise(resolve => {
-        const eventName = "PasswordManager:onGeneratedPasswordFilledOrEdited";
-        browser.messageManager.addMessageListener(
-          eventName,
-          function mgsHandler(msg) {
-            if (msg.target != browser) {
-              return;
+        info("Waiting to openAndVerifyDoorhanger");
+        // also moves focus, producing another onGeneratedPasswordFilledOrEdited message from content
+        let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+          dismissed: true,
+          anchorExtraAttr: "attention",
+          usernameValue: "",
+          password: autoSavedLogin.password,
+        });
+        ok(notif, "Got password-change notification");
+
+        info("Calling updateDoorhangerInputValues");
+        await updateDoorhangerInputValues({
+          username: "user1",
+        });
+        info("doorhanger inputs updated");
+
+        let loginModifiedPromise = TestUtils.topicObserved(
+          "passwordmgr-storage-changed",
+          (subject, data) => {
+            if (data == "modifyLogin") {
+              info("passwordmgr-storage-changed, action: " + data);
+              info("subject: " + JSON.stringify(subject));
+              return true;
             }
-            browser.messageManager.removeMessageListener(eventName, mgsHandler);
-            info("Got onGeneratedPasswordFilledOrEdited, resolving");
-            // allow LMP to handle the message, then resolve
-            SimpleTest.executeSoon(resolve);
+            return false;
           }
         );
-      });
-
-      info("Waiting to openAndVerifyDoorhanger");
-      // also moves focus, producing another onGeneratedPasswordFilledOrEdited message from content
-      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
-        dismissed: true,
-        anchorExtraAttr: "attention",
-        usernameValue: "",
-        password: autoSavedLogin.password,
-      });
-      ok(notif, "Got password-change notification");
-
-      // content sends a 2nd message when we blur the password field,
-      // wait for that before interacting with doorhanger
-      info("waiting for messagePromise");
-      await messagePromise;
-
-      info("Calling updateDoorhangerInputValues");
-      await updateDoorhangerInputValues({
-        username: "user1",
-      });
-      info("doorhanger inputs updated");
+        let loginRemovedPromise = TestUtils.topicObserved(
+          "passwordmgr-storage-changed",
+          (subject, data) => {
+            if (data == "removeLogin") {
+              info("passwordmgr-storage-changed, action: " + data);
+              info("subject: " + JSON.stringify(subject));
+              return true;
+            }
+            return false;
+          }
+        );
 
-      let loginModifiedPromise = TestUtils.topicObserved(
-        "passwordmgr-storage-changed",
-        (_, data) => {
-          if (data == "modifyLogin") {
-            info("passwordmgr-storage-changed, action: " + data);
-            info("subject: " + JSON.stringify(_));
-            return true;
-          }
-          return false;
-        }
-      );
-      let loginRemovedPromise = TestUtils.topicObserved(
-        "passwordmgr-storage-changed",
-        (_, data) => {
-          if (data == "removeLogin") {
-            info("passwordmgr-storage-changed, action: " + data);
-            info("subject: " + JSON.stringify(_));
-            return true;
-          }
-          return false;
-        }
-      );
+        let promiseHidden = BrowserTestUtils.waitForEvent(
+          PopupNotifications.panel,
+          "popuphidden"
+        );
+        info("clicking change button");
+        clickDoorhangerButton(notif, CHANGE_BUTTON);
+        await promiseHidden;
+
+        info("Waiting for modifyLogin promise");
+        await loginModifiedPromise;
+
+        info("Waiting for removeLogin promise");
+        await loginRemovedPromise;
 
-      let promiseHidden = BrowserTestUtils.waitForEvent(
-        PopupNotifications.panel,
-        "popuphidden"
-      );
-      info("clicking change button");
-      clickDoorhangerButton(notif, CHANGE_BUTTON);
-      await promiseHidden;
-
-      info("Waiting for modifyLogin promise");
-      await loginModifiedPromise;
-
-      info("Waiting for removeLogin promise");
-      await loginRemovedPromise;
+        info("storage-change promises resolved");
+        // Check the auto-saved login was removed and the original login updated
+        verifyLogins([
+          {
+            username: "user1",
+            password: autoSavedLogin.password,
+            timeCreated: user1LoginSnapshot.timeCreated,
+            timeLastUsed: user1LoginSnapshot.timeLastUsed,
+            passwordChangedSince: autoSavedLogin.timePasswordChanged,
+          },
+          null, // ignore user2
+        ]);
 
-      info("storage-change promises resolved");
-      // Check the auto-saved login was removed and the original login updated
-      verifyLogins([
-        {
-          username: "user1",
-          password: autoSavedLogin.password,
-          timeCreated: user1LoginSnapshot.timeCreated,
-          timeLastUsed: user1LoginSnapshot.timeLastUsed,
-          passwordChangedSince: autoSavedLogin.timePasswordChanged,
-        },
-        null, // ignore user2
-      ]);
+        // Check we have no notifications at this point
+        ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
+        ok(
+          !PopupNotifications.getNotification("password", browser),
+          "No notifications"
+        );
 
-      // Check we have no notifications at this point
-      ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
-      ok(
-        !PopupNotifications.getNotification("password", browser),
-        "No notifications"
-      );
+        // make sure the cache entry was removed with the removal of the auto-saved login
+        ok(
+          !LoginManagerParent._generatedPasswordsByPrincipalOrigin.has(
+            "https://example.com"
+          ),
+          "Generated password cache entry has been removed"
+        );
+      }
+    );
+  }
+);
 
-      // make sure the cache entry was removed with the removal of the auto-saved login
-      ok(
-        !LoginManagerParent._generatedPasswordsByPrincipalOrigin.has(
-          "https://example.com"
-        ),
-        "Generated password cache entry has been removed"
-      );
-    }
-  );
-});
-
-add_task(async function autosaved_login_updated_to_existing_login() {
+add_task(async function autosaved_login_updated_to_existing_login_onsubmit() {
   // test when selecting auto-saved generated password in a form filled with an
   // existing login and submitting the form:
   // * the matching login should be updated
   // * the auto-saved login should be deleted
   // * the metadata for the matching login should be updated
   // * the by-origin cache for the password should point at the updated login
 
   // clear both fields which should be autofilled with our single login
@@ -1133,79 +1117,25 @@ add_task(async function autosaved_login_
         "Cached password matches the auto-saved login password"
       );
       is(
         passwordCacheEntry.storageGUID,
         autoSavedLogin.guid,
         "Cached password guid matches the auto-saved login guid"
       );
 
-      let messagePromise = new Promise(resolve => {
-        const eventName = "PasswordManager:onGeneratedPasswordFilledOrEdited";
-        browser.messageManager.addMessageListener(
-          eventName,
-          function mgsHandler(msg) {
-            if (msg.target != browser) {
-              return;
-            }
-            browser.messageManager.removeMessageListener(eventName, mgsHandler);
-            info("Got onGeneratedPasswordFilledOrEdited, resolving");
-            // allow LMP to handle the message, then resolve
-            SimpleTest.executeSoon(resolve);
-          }
-        );
-      });
-
-      info("Waiting to openAndVerifyDoorhanger");
-      // also moves focus, producing another onGeneratedPasswordFilledOrEdited message from content
       let notif = await openAndVerifyDoorhanger(browser, "password-change", {
         dismissed: true,
         anchorExtraAttr: "attention",
         usernameValue: "",
         password: autoSavedLogin.password,
       });
-      ok(notif, "Got password-change notification");
-
-      // content sends a 2nd message when we blur the password field,
-      // wait for that before interacting with doorhanger
-      info("waiting for messagePromise");
-      await messagePromise;
-
-      info("Hiding popup.");
       await cleanupDoorhanger(notif);
-      info("/Hiding popup.");
 
       // now submit the form with the user1 username and the generated password
-      let doorhangerShown = waitForDoorhanger(browser, "password-change");
-
-      // check and ok the password-change doorhanger
-      let loginModifiedPromise = TestUtils.topicObserved(
-        "passwordmgr-storage-changed",
-        (_, data) => {
-          if (data == "modifyLogin") {
-            info("passwordmgr-storage-changed, action: " + data);
-            info("subject: " + JSON.stringify(_));
-            return true;
-          }
-          return false;
-        }
-      );
-      let loginRemovedPromise = TestUtils.topicObserved(
-        "passwordmgr-storage-changed",
-        (_, data) => {
-          if (data == "removeLogin") {
-            info("passwordmgr-storage-changed, action: " + data);
-            info("subject: " + JSON.stringify(_));
-            return true;
-          }
-          return false;
-        }
-      );
-
-      // submit the form with the generated password and username set to user1
       info(`submitting form`);
       let submitResults = await submitFormAndGetResults(
         browser,
         "formsubmit.sjs",
         {
           "#form-basic-username": "user1",
         }
       );
@@ -1220,28 +1150,51 @@ add_task(async function autosaved_login_
         "Form submitted with expected password"
       );
       info(
         `form was submitted, got username/password ${submitResults.username}/${
           submitResults.password
         }`
       );
 
-      await doorhangerShown;
+      await waitForDoorhanger(browser, "password-change");
       notif = await openAndVerifyDoorhanger(browser, "password-change", {
         dismissed: false,
         anchorExtraAttr: "",
         usernameValue: "user1",
         password: autoSavedLogin.password,
       });
 
       let promiseHidden = BrowserTestUtils.waitForEvent(
         PopupNotifications.panel,
         "popuphidden"
       );
+      let loginModifiedPromise = TestUtils.topicObserved(
+        "passwordmgr-storage-changed",
+        (_, data) => {
+          if (data == "modifyLogin") {
+            info("passwordmgr-storage-changed, action: " + data);
+            info("subject: " + JSON.stringify(_));
+            return true;
+          }
+          return false;
+        }
+      );
+      let loginRemovedPromise = TestUtils.topicObserved(
+        "passwordmgr-storage-changed",
+        (_, data) => {
+          if (data == "removeLogin") {
+            info("passwordmgr-storage-changed, action: " + data);
+            info("subject: " + JSON.stringify(_));
+            return true;
+          }
+          return false;
+        }
+      );
+
       info("clicking change button");
       clickDoorhangerButton(notif, CHANGE_BUTTON);
       await promiseHidden;
 
       info("Waiting for modifyLogin promise");
       await loginModifiedPromise;
 
       info("Waiting for removeLogin promise");