Bug 1550095 - Add Save Changes and Cancel button for LoginItem. r=MattN,Pike
☠☠ backed out by 1be22e2d35d1 ☠ ☠
authorJared Wein <jwein@mozilla.com>
Sat, 11 May 2019 02:04:52 +0000
changeset 535378 c5d450a431c8e1bea3ab2720cc4492c6926d12c6
parent 535377 93a0674140fc007a0f2d378a90cc4b53b77734e0
child 535379 b665ca03d2f6b9853383bec29db258ca3393f3e7
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, Pike
bugs1550095
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 1550095 - Add Save Changes and Cancel button for LoginItem. r=MattN,Pike Differential Revision: https://phabricator.services.mozilla.com/D30540
browser/components/BrowserGlue.jsm
browser/components/aboutlogins/AboutLoginsChild.jsm
browser/components/aboutlogins/AboutLoginsParent.jsm
browser/components/aboutlogins/content/aboutLogins.ftl
browser/components/aboutlogins/content/aboutLogins.html
browser/components/aboutlogins/content/aboutLogins.js
browser/components/aboutlogins/content/components/login-item.css
browser/components/aboutlogins/content/components/login-item.js
browser/components/aboutlogins/tests/browser/browser.ini
browser/components/aboutlogins/tests/browser/browser_loginChanges.js
browser/components/aboutlogins/tests/browser/browser_loginListChanges.js
browser/components/aboutlogins/tests/browser/browser_updateLogin.js
browser/components/aboutlogins/tests/mochitest/aboutlogins_common.js
browser/components/aboutlogins/tests/mochitest/test_login_item.html
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -31,16 +31,17 @@ let ACTORS = {
 
 let LEGACY_ACTORS = {
   AboutLogins: {
     child: {
       matches: ["about:logins"],
       module: "resource:///actors/AboutLoginsChild.jsm",
       events: {
         "AboutLoginsDeleteLogin": {wantUntrusted: true},
+        "AboutLoginsUpdateLogin": {wantUntrusted: true},
         "AboutLoginsInit": {wantUntrusted: true},
       },
       messages: [
         "AboutLogins:AllLogins",
         "AboutLogins:LoginAdded",
         "AboutLogins:LoginModified",
         "AboutLogins:LoginRemoved",
       ],
@@ -540,16 +541,17 @@ const listeners = {
     // PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN AsyncPrefs.init
 
     "webrtc:UpdateGlobalIndicators": ["webrtcUI"],
     "webrtc:UpdatingIndicators": ["webrtcUI"],
   },
 
   mm: {
     "AboutLogins:DeleteLogin": ["AboutLoginsParent"],
+    "AboutLogins:UpdateLogin": ["AboutLoginsParent"],
     "AboutLogins:Subscribe": ["AboutLoginsParent"],
     "Content:Click": ["ContentClick"],
     "ContentSearch": ["ContentSearch"],
     "FormValidation:ShowPopup": ["FormValidationHandler"],
     "FormValidation:HidePopup": ["FormValidationHandler"],
     "PictureInPicture:Request": ["PictureInPicture"],
     "PictureInPicture:Close": ["PictureInPicture"],
     "PictureInPicture:Playing": ["PictureInPicture"],
--- a/browser/components/aboutlogins/AboutLoginsChild.jsm
+++ b/browser/components/aboutlogins/AboutLoginsChild.jsm
@@ -25,16 +25,20 @@ class AboutLoginsChild extends ActorChil
           cloneFunctions: true,
         });
         break;
       }
       case "AboutLoginsDeleteLogin": {
         this.mm.sendAsyncMessage("AboutLogins:DeleteLogin", {login: event.detail});
         break;
       }
+      case "AboutLoginsUpdateLogin": {
+        this.mm.sendAsyncMessage("AboutLogins:UpdateLogin", {login: event.detail});
+        break;
+      }
     }
   }
 
   receiveMessage(message) {
     switch (message.name) {
       case "AboutLogins:AllLogins":
         this.sendToContent("AllLogins", message.data);
         break;
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -1,21 +1,26 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["AboutLoginsParent"];
 
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "LoginHelper",
                                "resource://gre/modules/LoginHelper.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  return LoginHelper.createLogger("AboutLoginsParent");
+});
+
 const ABOUT_LOGINS_ORIGIN = "about:logins";
 
 const isValidLogin = login => {
   return !(login.hostname || "").startsWith("chrome://");
 };
 
 const convertSubjectToLogin = subject => {
     subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
@@ -47,16 +52,35 @@ var AboutLoginsParent = {
           Services.obs.addObserver(this, "passwordmgr-storage-changed");
         }
         this._subscribers.add(message.target);
 
         let messageManager = message.target.messageManager;
         messageManager.sendAsyncMessage("AboutLogins:AllLogins", this.getAllLogins());
         break;
       }
+      case "AboutLogins:UpdateLogin": {
+        let loginUpdates = message.data.login;
+        let logins = LoginHelper.searchLoginsWithObject({guid: loginUpdates.guid});
+        if (!logins || logins.length != 1) {
+          log.warn(`AboutLogins:UpdateLogin: expected to find a login for guid: ${loginUpdates.guid} but found ${(logins || []).length}`);
+          return;
+        }
+
+        let modifiedLogin = logins[0].clone();
+        if (loginUpdates.hasOwnProperty("username")) {
+          modifiedLogin.username = loginUpdates.username;
+        }
+        if (loginUpdates.hasOwnProperty("password")) {
+          modifiedLogin.password = loginUpdates.password;
+        }
+
+        Services.logins.modifyLogin(logins[0], modifiedLogin);
+        break;
+      }
     }
   },
 
   observe(subject, topic, type) {
     if (!ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers).length) {
       Services.obs.removeObserver(this, "passwordmgr-storage-changed");
       return;
     }
@@ -90,17 +114,18 @@ var AboutLoginsParent = {
         break;
       }
     }
   },
 
   messageSubscribers(name, details) {
     let subscribers = ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers);
     for (let subscriber of subscribers) {
-      if (subscriber.contentPrincipal.originNoSuffix != ABOUT_LOGINS_ORIGIN) {
+      if (!subscriber.contentPrincipal ||
+          subscriber.contentPrincipal.originNoSuffix != ABOUT_LOGINS_ORIGIN) {
         this._subscribers.delete(subscriber);
         continue;
       }
       try {
         subscriber.messageManager.sendAsyncMessage(name, details);
       } catch (ex) {}
     }
   },
--- a/browser/components/aboutlogins/content/aboutLogins.ftl
+++ b/browser/components/aboutlogins/content/aboutLogins.ftl
@@ -11,13 +11,17 @@
 ### descendant after translation.
 
 about-logins-page-title = Login Manager
 
 login-list =
   .login-list-header = Logins
 
 login-item =
+  .cancel-button = Cancel
   .delete-button = Delete
-  .hostname-label = Hostname
+  .hostname-label = Website Address
   .password-label = Password
-  .time-created-label = Time Created
+  .save-changes-button = Save Changes
+  .time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
+  .time-changed = Last changed: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
+  .time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
   .username-label = Username
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -13,20 +13,25 @@
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list-item.js"></script>
     <script defer="defer" src="chrome://browser/content/aboutlogins/aboutLogins.js"></script>
   </head>
   <body>
     <login-list data-l10n-id="login-list"
                 data-l10n-attrs="login-list-header"></login-list>
     <login-item data-l10n-id="login-item"
-                data-l10n-attrs="delete-button,
+                data-l10n-args='{"timeCreated": 0, "timeChanged": 0, "timeUsed": 0}'
+                data-l10n-attrs="cancel-button,
+                                 delete-button,
                                  hostname-label,
                                  password-label,
-                                 time-created-label,
+                                 save-changes-button,
+                                 time-created,
+                                 time-changed,
+                                 time-used,
                                  username-label"></login-item>
 
     <template id="login-list-template">
       <h2></h2>
       <pre>
       </pre>
     </template>
 
@@ -38,28 +43,29 @@
       </style>
       <span class="login-list-item-hostname"></span>
       <span class="login-list-item-username"></span>
     </template>
 
     <template id="login-item-template">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-item.css">
       <h2 class="header"></h2>
+      <button class="delete-button"></button>
       <label>
-        <span class="hostname-label"></span>
-        <input name="hostname"/>
+        <span class="hostname-label field-label"></span>
+        <span class="hostname"/>
       </label>
       <label>
-        <span class="username-label"></span>
+        <span class="username-label field-label"></span>
         <input name="username"/>
       </label>
       <label>
-        <span class="password-label"></span>
+        <span class="password-label field-label"></span>
         <input type="password" name="password"/>
       </label>
-      <p>
-        <span class="time-created-label"></span>
-        <span class="time-created"></span>
-      </p>
-      <button class="delete-button"></button>
+      <p class="time-created meta-info"></p>
+      <p class="time-changed meta-info"></p>
+      <p class="time-used meta-info"></p>
+      <button class="save-changes-button"></button>
+      <button class="cancel-button"></button>
     </template>
   </body>
 </html>
--- a/browser/components/aboutlogins/content/aboutLogins.js
+++ b/browser/components/aboutlogins/content/aboutLogins.js
@@ -14,17 +14,16 @@ document.addEventListener("DOMContentLoa
 window.addEventListener("AboutLoginsChromeToContent", event => {
   switch (event.detail.messageType) {
     case "AllLogins": {
       gElements.loginList.setLogins(event.detail.value);
       break;
     }
     case "LoginAdded": {
       gElements.loginList.loginAdded(event.detail.value);
-      gElements.loginItem.loginAdded(event.detail.value);
       break;
     }
     case "LoginModified": {
       gElements.loginList.loginModified(event.detail.value);
       gElements.loginItem.loginModified(event.detail.value);
       break;
     }
     case "LoginRemoved": {
--- a/browser/components/aboutlogins/content/components/login-item.css
+++ b/browser/components/aboutlogins/content/components/login-item.css
@@ -4,8 +4,28 @@
 
 :host {
   --grey-30: #d7d7db;
 }
 
 h2 {
   border-bottom: 1px solid var(--grey-30);
 }
+
+.field-label {
+  display: block;
+}
+
+.meta-info {
+  font-size: smaller;
+}
+
+.meta-info:not(:first-of-type) {
+  margin-top: 0;
+}
+
+.meta-info:not(:last-of-type) {
+  margin-bottom: 0;
+}
+
+.meta-info:first-of-type {
+  border-top: 1px solid var(--grey-30);
+}
--- a/browser/components/aboutlogins/content/components/login-item.js
+++ b/browser/components/aboutlogins/content/components/login-item.js
@@ -13,30 +13,40 @@ class LoginItem extends HTMLElement {
       this.render();
       return;
     }
 
     let loginItemTemplate = document.querySelector("#login-item-template");
     this.attachShadow({mode: "open"})
         .appendChild(loginItemTemplate.content.cloneNode(true));
 
-    let deleteButton = this.shadowRoot.querySelector(".delete-button");
-    deleteButton.addEventListener("click", this);
+    for (let selector of [
+      ".delete-button",
+      ".save-changes-button",
+      ".cancel-button",
+    ]) {
+      let button = this.shadowRoot.querySelector(selector);
+      button.addEventListener("click", this);
+    }
 
     window.addEventListener("AboutLoginsLoginSelected", this);
 
     this.render();
   }
 
   static get observedAttributes() {
     return [
+      "cancel-button",
       "delete-button",
       "hostname-label",
       "password-label",
-      "time-created-label",
+      "save-changes-button",
+      "time-created",
+      "time-changed",
+      "time-used",
       "username-label",
     ];
   }
 
   /* Fluent doesn't handle localizing into Shadow DOM yet so strings
      need to get reflected in to their targeted element. */
   attributeChangedCallback(attr, oldValue, newValue) {
     if (!this.shadowRoot) {
@@ -45,66 +55,74 @@ class LoginItem extends HTMLElement {
 
     // Strings that are reflected to their shadowed element are assigned
     // to an attribute name that matches a className on the element.
     let shadowedElement = this.shadowRoot.querySelector("." + attr);
     shadowedElement.textContent = newValue;
   }
 
   render() {
-    this.shadowRoot.querySelector("input[name='hostname']").value = this._login.hostname || "";
+    let l10nArgs = {
+      timeCreated: this._login.timeCreated || "",
+      timeChanged: this._login.timePasswordChanged || "",
+      timeUsed: this._login.timeLastUsed || "",
+    };
+    document.l10n.setAttributes(this, "login-item", l10nArgs);
+    let hostnameNoScheme = this._login.hostname && new URL(this._login.hostname).hostname;
+    this.shadowRoot.querySelector(".header").textContent = hostnameNoScheme || "";
+    this.shadowRoot.querySelector(".hostname").textContent = this._login.hostname || "";
     this.shadowRoot.querySelector("input[name='username']").value = this._login.username || "";
     this.shadowRoot.querySelector("input[name='password']").value = this._login.password || "";
-    this.shadowRoot.querySelector(".time-created").textContent = this._login.timeCreated || "";
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "AboutLoginsLoginSelected": {
         this.setLogin(event.detail);
         break;
       }
       case "click": {
         if (event.target.classList.contains("delete-button")) {
           document.dispatchEvent(new CustomEvent("AboutLoginsDeleteLogin", {
             bubbles: true,
             detail: this._login,
           }));
+          return;
+        }
+        if (event.target.classList.contains("save-changes-button")) {
+          let loginUpdates = {
+            guid: this._login.guid,
+          };
+          let formUsername = this.shadowRoot.querySelector("input[name='username']").value.trim();
+          if (formUsername != this._login.username) {
+            loginUpdates.username = formUsername;
+          }
+          let formPassword = this.shadowRoot.querySelector("input[name='password']").value.trim();
+          if (formPassword != this._login.password) {
+            loginUpdates.password = formPassword;
+          }
+          document.dispatchEvent(new CustomEvent("AboutLoginsUpdateLogin", {
+            bubbles: true,
+            detail: loginUpdates,
+          }));
+          return;
+        }
+        if (event.target.classList.contains("cancel-button")) {
+          this.render();
         }
         break;
       }
     }
   }
 
   setLogin(login) {
     this._login = login;
     this.render();
   }
 
-  loginAdded(login) {
-    if (!this._login.guid) {
-      let tempLogin = {
-        username: this.shadowRoot.querySelector("input[name='username']").value,
-        formSubmitURL: "", // Use the wildcard since the user doesn't supply it.
-        hostname: this.shadowRoot.querySelector("input[name='hostname']").value,
-        password: this.shadowRoot.querySelector("input[name='password']").value,
-      };
-      // Need to use LoginHelper.doLoginsMatch() to see if the login
-      // that was added is the login that was being edited, so we
-      // can update time-created, etc.
-      if (window.AboutLoginsUtils.doLoginsMatch(tempLogin, login)) {
-        this._login = login;
-        this.render();
-      }
-    } else if (login.guid == this._login.guid) {
-      this._login = login;
-      this.render();
-    }
-  }
-
   loginModified(login) {
     if (login.guid != this._login.guid) {
       return;
     }
 
     this._login = login;
     this.render();
   }
--- a/browser/components/aboutlogins/tests/browser/browser.ini
+++ b/browser/components/aboutlogins/tests/browser/browser.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 prefs =
   signon.management.page.enabled=true
 
 [browser_deleteLogin.js]
-[browser_loginChanges.js]
+[browser_loginListChanges.js]
+[browser_updateLogin.js]
rename from browser/components/aboutlogins/tests/browser/browser_loginChanges.js
rename to browser/components/aboutlogins/tests/browser/browser_loginListChanges.js
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+                                             Ci.nsILoginInfo, "init");
+const LOGIN_URL = "https://www.example.com";
+let TEST_LOGIN1 = new nsLoginInfo(LOGIN_URL, LOGIN_URL, null, "user1", "pass1", "username", "password");
+
+add_task(async function setup() {
+  TEST_LOGIN1 = Services.logins.addLogin(TEST_LOGIN1);
+  await BrowserTestUtils.openNewForegroundTab({gBrowser, url: "about:logins"});
+  registerCleanupFunction(() => {
+    BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    Services.logins.removeAllLogins();
+  });
+});
+
+add_task(async function test_show_logins() {
+  let browser = gBrowser.selectedBrowser;
+  await ContentTask.spawn(browser, TEST_LOGIN1.guid, async (loginGuid) => {
+    let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+    let loginFound = await ContentTaskUtils.waitForCondition(() => {
+      return loginList._logins.length == 1 &&
+             loginList._logins[0].guid == loginGuid;
+    }, "Waiting for login to be displayed");
+    ok(loginFound, "Stored logins should be displayed upon loading the page");
+  });
+});
+
+add_task(async function test_login_item() {
+  let browser = gBrowser.selectedBrowser;
+  await ContentTask.spawn(browser, LoginHelper.loginToVanillaObject(TEST_LOGIN1), async (login) => {
+    let loginList = content.document.querySelector("login-list");
+    let loginListItem = Cu.waiveXrays(loginList.shadowRoot.querySelector("login-list-item"));
+    loginListItem.click();
+
+    let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+    let loginItemPopulated = await ContentTaskUtils.waitForCondition(() => {
+      return loginItem._login.guid == loginListItem.getAttribute("guid") &&
+             loginItem._login.guid == login.guid;
+    }, "Waiting for login item to get populated");
+    ok(loginItemPopulated, "The login item should get populated");
+
+    let usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
+    let passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
+
+    usernameInput.value += "-undome";
+    passwordInput.value += "-undome";
+
+    let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
+    cancelButton.click();
+    await Promise.resolve();
+    is(usernameInput.value, login.username, "Username change should be reverted");
+    is(passwordInput.value, login.password, "Password change should be reverted");
+
+    usernameInput.value += "-saveme";
+    passwordInput.value += "-saveme";
+
+    let saveChangesButton = loginItem.shadowRoot.querySelector(".save-changes-button");
+    saveChangesButton.click();
+
+    await ContentTaskUtils.waitForCondition(() => {
+      return loginListItem._login.username == usernameInput.value &&
+             loginListItem._login.password == passwordInput.value;
+    }, "Waiting for corresponding login in login list to update");
+  });
+});
--- a/browser/components/aboutlogins/tests/mochitest/aboutlogins_common.js
+++ b/browser/components/aboutlogins/tests/mochitest/aboutlogins_common.js
@@ -1,11 +1,11 @@
 "use strict";
 
-/* exported asyncElementRendered, importDependencies */
+/* exported asyncElementRendered, importDependencies, stubFluentL10n */
 
 /**
  * A helper to await on while waiting for an asynchronous rendering of a Custom
  * Element.
  * @returns {Promise}
  */
 function asyncElementRendered() {
   return Promise.resolve();
@@ -19,8 +19,20 @@ function asyncElementRendered() {
 function importDependencies(templateFrame, destinationEl) {
   let templates = templateFrame.contentDocument.querySelectorAll("template");
   isnot(templates, null, "Check some templates found");
   for (let template of templates) {
     let imported = document.importNode(template, true);
     destinationEl.appendChild(imported);
   }
 }
+
+function stubFluentL10n(argsMap) {
+  document.l10n = {
+    setAttributes(element, id, args) {
+      element.setAttribute("data-l10n-id", id);
+      for (let attrName of Object.keys(argsMap)) {
+        let varName = argsMap[attrName];
+        element.setAttribute(attrName, args[varName]);
+      }
+    },
+  };
+}
--- a/browser/components/aboutlogins/tests/mochitest/test_login_item.html
+++ b/browser/components/aboutlogins/tests/mochitest/test_login_item.html
@@ -26,84 +26,104 @@ Test the login-item component
 
 let gLoginItem;
 const TEST_LOGIN_1 = {
   guid: "123456789",
   hostname: "https://example.com",
   username: "user1",
   password: "pass1",
   timeCreated: "1000",
+  timePasswordChanged: "2000",
+  timeLastUsed: "4000",
 };
 
 add_task(async function setup() {
+  stubFluentL10n({
+    "time-created": "timeCreated",
+    "time-changed": "timeChanged",
+    "time-used": "timeUsed",
+  });
+
   let templateFrame = document.getElementById("templateFrame");
   let displayEl = document.getElementById("display");
   importDependencies(templateFrame, displayEl);
 
   gLoginItem = document.createElement("login-item");
   displayEl.appendChild(gLoginItem);
 });
 
 add_task(async function test_empty_item() {
   ok(gLoginItem, "loginItem exists");
-  is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, "", "hostname should be blank");
+  is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, "", "hostname should be blank");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be blank");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be blank");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank");
+  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank");
+  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank");
 });
 
 add_task(async function test_set_login() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   await asyncElementRendered();
 
-  is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, TEST_LOGIN_1.hostname, "hostname should be populated");
+  is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be populated");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be populated");
+  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
+  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
 });
 
 add_task(async function test_different_login_modified() {
   let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
   gLoginItem.loginModified(otherLogin);
   await asyncElementRendered();
 
-  is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, TEST_LOGIN_1.hostname, "hostname should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be unchanged");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
 });
 
 add_task(async function test_different_login_removed() {
   let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
   gLoginItem.loginRemoved(otherLogin);
   await asyncElementRendered();
 
-  is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, TEST_LOGIN_1.hostname, "hostname should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be unchanged");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
+  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
 });
 
 add_task(async function test_login_modified() {
   let modifiedLogin = Object.assign({}, TEST_LOGIN_1, {username: "updateduser"});
   gLoginItem.loginModified(modifiedLogin);
   await asyncElementRendered();
 
-  is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, modifiedLogin.hostname, "hostname should be updated");
+  is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, modifiedLogin.hostname, "hostname should be updated");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, modifiedLogin.username, "username should be updated");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, modifiedLogin.password, "password should be updated");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, modifiedLogin.timeCreated, "time-created should be updated");
+  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, modifiedLogin.timePasswordChanged, "time-changed should be updated");
+  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, modifiedLogin.timeLastUsed, "time-used should be updated");
 });
 
 add_task(async function test_login_removed() {
   gLoginItem.loginRemoved(TEST_LOGIN_1);
   await asyncElementRendered();
 
-  is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, "", "hostname should be cleared");
+  is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, "", "hostname should be cleared");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be cleared");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be cleared");
   is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be cleared");
+  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be cleared");
+  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be cleared");
 });
 
 </script>
 
 </body>
 </html>