tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_login_list.html (35780B)


      1 <!DOCTYPE HTML>
      2 <html>
      3 <!--
      4 Test the login-list component
      5 -->
      6 <head>
      7  <meta charset="utf-8">
      8  <title>Test the login-list component</title>
      9  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
     10  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
     11  <script type="module" src="chrome://browser/content/aboutlogins/components/login-list.mjs"></script>
     12  <script type="module" src="chrome://browser/content/aboutlogins/components/login-command-button.mjs"></script>
     13  <script src="aboutlogins_common.js"></script>
     14 
     15  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
     16 </head>
     17 <body>
     18  <p id="display">
     19  </p>
     20 <div id="content" style="display: none">
     21  <iframe id="templateFrame" src="chrome://browser/content/aboutlogins/aboutLogins.html"
     22          sandbox="allow-same-origin"></iframe>
     23 </div>
     24 <pre id="test">
     25 </pre>
     26 <script>
     27 /** Test the login-list component */
     28 
     29 let gLoginList;
     30 const TEST_LOGIN_1 = {
     31  guid: "123456789",
     32  origin: "https://abc.example.com",
     33  httpRealm: null,
     34  username: "user1",
     35  password: "pass1",
     36  title: "abc.example.com",
     37  // new Date("December 13, 2018").getTime()
     38  timeLastUsed: 1544677200000,
     39  timePasswordChanged: 1544677200000,
     40 };
     41 const TEST_LOGIN_2 = {
     42  guid: "987654321",
     43  origin: "https://example.com",
     44  httpRealm: null,
     45  username: "user2",
     46  password: "pass2",
     47  title: "example.com",
     48  // new Date("June 1, 2019").getTime()
     49  timeLastUsed: 1559361600000,
     50  timePasswordChanged: 1559361600000,
     51 };
     52 const TEST_LOGIN_3 = {
     53  guid: "1111122222",
     54  origin: "https://def.example.com",
     55  httpRealm: null,
     56  username: "",
     57  password: "pass3",
     58  title: "def.example.com",
     59  // new Date("June 1, 2019").getTime()
     60  timeLastUsed: 1559361600000,
     61  timePasswordChanged: 1559361600000,
     62 };
     63 const TEST_HTTP_AUTH_LOGIN_1 = {
     64  guid: "8675309",
     65  origin: "https://httpauth.example.com",
     66  httpRealm: "My Realm",
     67  username: "http_auth_user",
     68  password: "pass4",
     69  title: "httpauth.example.com (My Realm)",
     70  // new Date("June 1, 2019").getTime()
     71  timeLastUsed: 1559361600000,
     72  timePasswordChanged: 1559361600000,
     73 };
     74 
     75 const TEST_BREACH = {
     76  AddedDate: "2018-12-20T23:56:26Z",
     77  BreachDate: "2018-12-11",
     78  Domain: "abc.example.com",
     79  Name: "ABC Example",
     80  PwnCount: 1643100,
     81  DataClasses: ["Usernames", "Passwords"],
     82  _status: "synced",
     83  id: "047940fe-d2fd-4314-b636-b4a952ee0043",
     84  last_modified: "1541615610052",
     85  schema: "1541615609018",
     86  breachAlertURL: "https://monitor.firefox.com/breach-details/ABC-Example",
     87 };
     88 
     89 
     90 const TEST_BREACHES_MAP = new Map();
     91 TEST_BREACHES_MAP.set(TEST_LOGIN_1.guid, TEST_BREACH);
     92 
     93 add_setup(async () => {
     94  let templateFrame = document.getElementById("templateFrame");
     95  let displayEl = document.getElementById("display");
     96  await importDependencies(templateFrame, displayEl);
     97 
     98  gLoginList = document.createElement("login-list");
     99  displayEl.appendChild(gLoginList);
    100 });
    101 
    102 add_task(async function test_empty_list() {
    103  ok(gLoginList, "loginList exists");
    104  is(gLoginList.textContent, "", "Initially empty");
    105  gLoginList.classList.add("no-logins");
    106  let loginListBox = gLoginList.shadowRoot.querySelector("ol");
    107  let introText = gLoginList.shadowRoot.querySelector(".intro");
    108  let emptySearchText = gLoginList.shadowRoot.querySelector(".empty-search-message");
    109  ok(isHidden(loginListBox), "The login-list ol should be hidden when there are no logins");
    110  ok(!isHidden(introText), "The intro text should be visible when the list is empty");
    111  ok(isHidden(emptySearchText), "The empty-search text should be hidden when there are no logins");
    112 
    113  gLoginList.classList.add("create-login-selected");
    114  ok(!isHidden(loginListBox), "The login-list ol should be visible when the create-login mode is active");
    115  ok(isHidden(introText), "The intro text should be hidden when the create-login mode is active");
    116  ok(isHidden(emptySearchText), "The empty-search text should be hidden when the create-login mode is active");
    117  gLoginList.classList.remove("create-login-selected");
    118 
    119  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    120    bubbles: true,
    121    detail: "foo",
    122  }));
    123  ok(isHidden(loginListBox), "The login-list ol should be hidden when there are no logins");
    124  ok(!isHidden(introText), "The intro text should be visible when the list is empty");
    125  ok(isHidden(emptySearchText), "The empty-search text should be hidden when there are no logins even if a filter is applied");
    126 
    127  // Clean up state for next test
    128  gLoginList.classList.remove("no-logins");
    129  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    130    bubbles: true,
    131    detail: "",
    132  }));
    133 });
    134 
    135 add_task(async function test_keyboard_navigation() {
    136  let logins = [];
    137  for (let i = 0; i < 20; i++) {
    138    let suffix = i % 2 ? "odd" : "even";
    139    logins.push(Object.assign({}, TEST_LOGIN_1, {
    140      guid: "" + i,
    141      username: `testuser-${suffix}-${i}`,
    142      password: `testpass-${suffix}-${i}`,
    143    }));
    144  }
    145  gLoginList.setLogins(logins);
    146  let ol = gLoginList.shadowRoot.querySelector("ol");
    147  is(ol.querySelectorAll("login-list-item[data-guid]").length, 20, "there should be 20 logins in the list");
    148  is(ol.querySelectorAll("login-list-item[data-guid]:not([hidden])").length, 20, "all logins should be visible");
    149  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    150    bubbles: true,
    151    detail: "odd",
    152  }));
    153  is(ol.querySelectorAll("login-list-item[data-guid]:not([hidden])").length, 10, "half of the logins in the list");
    154 
    155  while (document.activeElement != gLoginList &&
    156         gLoginList.shadowRoot.querySelector("#login-sort") != gLoginList.shadowRoot.activeElement) {
    157    sendKey("TAB");
    158    await new Promise(resolve => requestAnimationFrame(resolve));
    159  }
    160  sendKey("TAB");
    161  let loginSort = gLoginList.shadowRoot.querySelector("#login-sort");
    162  await SimpleTest.promiseWaitForCondition(() => loginSort == gLoginList.shadowRoot.activeElement,
    163    "waiting for login-sort to get focus");
    164  ok(loginSort == gLoginList.shadowRoot.activeElement, "#login-sort should be focused after tabbing to it");
    165 
    166  sendKey("TAB");
    167  await SimpleTest.promiseWaitForCondition(() => ol.matches(":focus"),
    168    "waiting for 'ol' to get focus");
    169  ok(ol.matches(":focus"), "'ol' should be focused after tabbing to it");
    170 
    171  let selectedGuid = ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[0].dataset.guid;
    172  let loginSelectedEvent = null;
    173  gLoginList.addEventListener("AboutLoginsLoginSelected", event => loginSelectedEvent = event, {once: true});
    174  sendKey("RETURN");
    175  is(ol.querySelector(".selected").dataset.guid, selectedGuid, "item should be marked as selected");
    176  ok(loginSelectedEvent, "AboutLoginsLoginSelected event should be dispatched on pressing Enter");
    177  is(loginSelectedEvent.detail.guid, selectedGuid, "event should have expected login attached");
    178 
    179  for (let [keyFwd, keyRev] of [["LEFT", "RIGHT"], ["DOWN", "UP"]]) {
    180    sendKey(keyFwd);
    181    await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[1].id,
    182      `waiting for second item in list to get focused (${keyFwd})`);
    183    ok(ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (${keyFwd})`);
    184 
    185    sendKey(keyRev);
    186    await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[0].id,
    187      `waiting for first item in list to get focused (${keyRev})`);
    188    ok(ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[0].classList.contains("keyboard-selected"), `first item should be marked as keyboard-selected (${keyRev})`);
    189  }
    190 
    191  sendKey("DOWN");
    192  await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[1].id,
    193    `waiting for second item in list to get focused (DOWN)`);
    194  ok(ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (DOWN)`);
    195  selectedGuid = ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[1].dataset.guid;
    196 
    197  synthesizeKey("VK_DOWN", { repeat: 5 });
    198  ok(ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[6].classList.contains("keyboard-selected"), `sixth item should be marked as keyboard-selected after 5 DOWN repeats`);
    199  synthesizeKey("VK_UP", { repeat: 5 });
    200  ok(ol.querySelectorAll("login-list-item[data-guid]:not([hidden])")[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected again after 5 UP repeats`);
    201 
    202  loginSelectedEvent = null;
    203  gLoginList.addEventListener("AboutLoginsLoginSelected", event => loginSelectedEvent = event, {once: true});
    204  sendKey("RETURN");
    205  is(ol.querySelector(".selected").dataset.guid, selectedGuid, "item should be marked as selected");
    206  ok(loginSelectedEvent, "AboutLoginsLoginSelected event should be dispatched on pressing Enter");
    207  is(loginSelectedEvent.detail.guid, selectedGuid, "event should have expected login attached");
    208 
    209  // Clean up state for next test
    210  gLoginList.classList.remove("no-logins");
    211  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    212    bubbles: true,
    213    detail: "",
    214  }));
    215 });
    216 
    217 add_task(async function test_empty_login_username_in_list() {
    218  // Clear the selection so the 'new' login will be in the list too.
    219  window.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
    220    detail: {},
    221  }));
    222 
    223  gLoginList.setLogins([TEST_LOGIN_3]);
    224  await asyncElementRendered();
    225 
    226  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    227  is(loginListItems.length, 1, "The one stored login should be displayed");
    228  is(loginListItems[0].dataset.guid, TEST_LOGIN_3.guid, "login-list-item should have correct guid attribute");
    229  let loginUsername = loginListItems[0].shadowRoot.querySelector(".subtitle");
    230  is(loginUsername.getAttribute("data-l10n-id"), "login-list-item-subtitle-missing-username", "login should show missing username text");
    231 });
    232 
    233 add_task(async function test_populated_list() {
    234  gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
    235  await asyncElementRendered();
    236  await asyncElementRendered();
    237 
    238  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    239  is(loginListItems.length, 2, "The two stored logins should be displayed");
    240  is(loginListItems[0].shadowRoot.querySelector("list-item").shadowRoot.querySelector(".list-item").getAttribute("role"), "option", "Each login-list-item should have role='option'");
    241  is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
    242  is(loginListItems[0].title, TEST_LOGIN_1.title,
    243     "login-list-item origin should match");
    244  is(loginListItems[0].username, TEST_LOGIN_1.username,
    245     "login-list-item username should match");
    246  ok(loginListItems[0].classList.contains("selected"), "The first item should be selected by default");
    247  ok(!loginListItems[1].classList.contains("selected"), "The second item should not be selected by default");
    248  loginListItems[0].click();
    249  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    250  is(loginListItems.length, 2, "After selecting one, only the two stored logins should be displayed");
    251  ok(loginListItems[0].classList.contains("selected"), "The first item should be selected");
    252  ok(!loginListItems[1].classList.contains("selected"), "The second item should still not be selected");
    253 });
    254 
    255 add_task(async function test_breach_indicator() {
    256  gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2, Object.assign({}, TEST_LOGIN_3, {password: TEST_LOGIN_1.password})]);
    257  gLoginList.setBreaches(TEST_BREACHES_MAP);
    258  let vulnerableLogins = new Map();
    259  vulnerableLogins.set(TEST_LOGIN_1.guid, true);
    260  vulnerableLogins.set(TEST_LOGIN_3.guid, true);
    261  gLoginList.setVulnerableLogins(vulnerableLogins);
    262  await asyncElementRendered();
    263  await asyncElementRendered();
    264 
    265  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    266  let alertIcon = loginListItems[0].shadowRoot.querySelector(".alert-icon");
    267  is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "The first login should be TEST_LOGIN_1");
    268  ok(loginListItems[0].notificationIcon !== "vulnerable", "The first login should not have the .vulnerable class");
    269  ok(loginListItems[0].notificationIcon === "breached", "The first login should have the .breached class.");
    270  is(alertIcon.src, "chrome://browser/content/aboutlogins/icons/breached-website.svg", "The alert icon should be the breach warning icon");
    271  is(loginListItems[1].dataset.guid, TEST_LOGIN_3.guid, "The second login should be TEST_LOGIN_3");
    272  ok(loginListItems[1].notificationIcon === "vulnerable", "The second login should have the .vulnerable class");
    273  ok(loginListItems[1].notificationIcon !== ("breached"), "The second login should not have the .breached class");
    274  alertIcon = loginListItems[1].shadowRoot.querySelector(".alert-icon");;
    275  is(alertIcon.src, "chrome://browser/content/aboutlogins/icons/vulnerable-password.svg", "The alert icon should be the vulnerable password icon");
    276  is(loginListItems[2].dataset.guid, TEST_LOGIN_2.guid, "The third login should be TEST_LOGIN_2");
    277  alertIcon = loginListItems[2].shadowRoot.querySelector(".alert-icon");;
    278  ok(loginListItems[2].notificationIcon !== "vulnerable", "The third login should not have the .vulnerable class");
    279  ok(loginListItems[2].notificationIcon !== "breached", "The third login should not have the .breached class");
    280  is(alertIcon.src, "chrome://mochitests/content/chrome/browser/components/aboutlogins/tests/chrome/test_login_list.html",  "The alert icon src should be empty");
    281 });
    282 
    283 function assertCount({ count, total }) {
    284  const countSpan = gLoginList.shadowRoot.querySelector(".count");
    285  const actual = JSON.parse(countSpan.getAttribute("data-l10n-args"));
    286  isDeeply(actual, { count, total }, "Login count updated");
    287 }
    288 
    289 add_task(async function test_filtered_list() {
    290  await asyncElementRendered();
    291 
    292  function findItemFromUsername(list, username) {
    293    for (let item of list) {
    294      if ((item._cachedUsername || (item._cachedUsername = item.username)) == username) {
    295        return item;
    296      }
    297    }
    298    ok(false, `The ${username} wasn't in the list of logins.`)
    299    return list[0];
    300  }
    301  gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
    302  await asyncElementRendered();
    303 
    304  let emptySearchText = gLoginList.shadowRoot.querySelector(".empty-search-message");
    305  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
    306  is(gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])").length, 2, "Both logins should be visible");
    307 
    308  assertCount({ count: 2, total: 2 });
    309  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    310    bubbles: true,
    311    detail: "user1",
    312  }));
    313  assertCount({ count: 1, total: 2 });
    314  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
    315  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]");
    316  is(loginListItems[0].username, "user1", "user1 is expected first");
    317  ok(!loginListItems[0].hidden, "user1 should remain visible");
    318  ok(loginListItems[1].hidden, "user2 should be hidden");
    319  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    320    bubbles: true,
    321    detail: "user2",
    322  }));
    323  assertCount({ count: 1, total: 2 });
    324  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
    325  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]");
    326  ok(findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be hidden");
    327  ok(!findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be visible");
    328  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    329    bubbles: true,
    330    detail: "user",
    331  }));
    332  assertCount({ count: 2, total: 2 });
    333  ok(!gLoginList._sortSelect.disabled, "The sort should be enabled when there are visible logins in the list");
    334  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
    335  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]");
    336  ok(!findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be visible");
    337  ok(!findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be visible");
    338  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    339    bubbles: true,
    340    detail: "foo",
    341  }));
    342  assertCount({ count: 0, total: 2 });
    343  ok(gLoginList._sortSelect.disabled, "The sort should be disabled when there are no visible logins in the list");
    344  ok(!isHidden(emptySearchText), "The empty search text should be visible when there are no results in the list");
    345  isnot(gLoginList.shadowRoot.querySelector(".container > ol").getAttribute("aria-activedescendant"), "new-login-list-item", "new-login-list-item shouldn't be the active descendant");
    346  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]");
    347  ok(findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be hidden");
    348  ok(findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be hidden");
    349  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    350    bubbles: true,
    351    detail: "",
    352  }));
    353  ok(!gLoginList._sortSelect.disabled, "The sort should be re-enabled when there are visible logins in the list");
    354  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
    355  assertCount({ count: 2, total: 2 });
    356  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]");
    357  ok(!findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be visible");
    358  ok(!findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be visible");
    359 
    360  info("Add an HTTP Auth login");
    361  gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2, TEST_HTTP_AUTH_LOGIN_1]);
    362  await asyncElementRendered();
    363  assertCount({ count: 3, total: 3 });
    364  info("Filter by httpRealm");
    365  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    366    bubbles: true,
    367    detail: "realm",
    368  }));
    369  assertCount({ count: 1, total: 3 });
    370  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]");
    371  ok(findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be hidden");
    372  ok(findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be hidden");
    373  ok(!findItemFromUsername(loginListItems, 'http_auth_user').hidden, "http_auth_user should be visible");
    374 
    375  gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
    376  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    377    bubbles: true,
    378    detail: "",
    379  }));
    380  await asyncElementRendered();
    381 });
    382 
    383 add_task(async function test_initial_empty_results() {
    384  // Create a new instance to reset state
    385  gLoginList.remove();
    386  gLoginList = document.createElement("login-list");
    387  document.getElementById("display").appendChild(gLoginList);
    388  await asyncElementRendered();
    389 
    390  let emptySearchText = gLoginList.shadowRoot.querySelector(".empty-search-message");
    391 
    392  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    393    bubbles: true,
    394    detail: "foo",
    395  }));
    396  assertCount({ count: 0, total: 0 });
    397  ok(gLoginList._sortSelect.disabled, "The sort should be disabled when there are no visible logins in the list");
    398  ok(!isHidden(emptySearchText), "The empty search text should be visible when there are no results in the list");
    399  isnot(gLoginList.shadowRoot.querySelector(".container > ol").getAttribute("aria-activedescendant"), "new-login-list-item", "new-login-list-item shouldn't be the active descendant");
    400  ok(gLoginList.shadowRoot.querySelector("new-list-item").hidden, "new-login-list-item should be @hidden");
    401 
    402  gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
    403  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    404    bubbles: true,
    405    detail: "",
    406  }));
    407  await asyncElementRendered();
    408 });
    409 
    410 add_task(async function test_login_modified() {
    411  let modifiedLogin = Object.assign(TEST_LOGIN_1, {username: "user11"});
    412  gLoginList.loginModified(modifiedLogin);
    413  await asyncElementRendered();
    414 
    415  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]:not([hidden])");
    416  is(loginListItems.length, 2, "Both logins should be displayed");
    417  is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
    418  is(loginListItems[0].title, TEST_LOGIN_1.title,
    419     "login-list-item origin should match");
    420  is(loginListItems[0].username, modifiedLogin.username,
    421     "login-list-item username should have been updated");
    422  is(loginListItems[1].username, TEST_LOGIN_2.username,
    423     "login-list-item2 username should remain unchanged");
    424 });
    425 
    426 add_task(async function test_login_added() {
    427  info("selected sort: " + gLoginList.shadowRoot.getElementById("login-sort").selectedIndex);
    428 
    429  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    430  is(loginListItems.length, 2, "Should have two logins at start of test");
    431  let newLogin = Object.assign({}, TEST_LOGIN_1, {title: "example2.example.com", guid: "111222"});
    432  gLoginList.loginAdded(newLogin);
    433  await asyncElementRendered();
    434 
    435  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    436  is(loginListItems.length, 3, "New login should be added to the list");
    437  is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
    438  is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
    439  is(loginListItems[2].dataset.guid, newLogin.guid, "login-list-item3 should have correct guid attribute");
    440  is(loginListItems[2].title, newLogin.title,
    441     "login-list-item origin should match");
    442  is(loginListItems[2].username, newLogin.username,
    443     "login-list-item username should have been updated");
    444 });
    445 
    446 add_task(async function test_login_removed() {
    447  gLoginList.loginRemoved({guid: "111222"});
    448  await asyncElementRendered();
    449  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    450  is(loginListItems.length, 2, "New login should be removed from the list");
    451  is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
    452  is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
    453 });
    454 
    455 add_task(async function test_login_added_filtered() {
    456  assertCount({ count: 2, total: 2 });
    457  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    458    detail: "user1",
    459  }));
    460  assertCount({ count: 1, total: 2 });
    461 
    462  let newLogin = Object.assign({}, TEST_LOGIN_1, {title: "example2.example.com", username: "user22", guid: "111222"});
    463  gLoginList.loginAdded(newLogin);
    464  await asyncElementRendered();
    465  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]");
    466  is(loginListItems.length, 3, "New login should be added to the list");
    467  is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
    468  is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
    469  is(loginListItems[2].dataset.guid, newLogin.guid, "login-list-item3 should have correct guid attribute");
    470  ok(!loginListItems[0].hidden, "login-list-item1 should be visible");
    471  ok(loginListItems[1].hidden, "login-list-item2 should be hidden");
    472  ok(loginListItems[2].hidden, "login-list-item3 should be hidden");
    473  assertCount({ count: 1, total: 3 });
    474 });
    475 
    476 add_task(async function test_sorted_list() {
    477  function dispatchChangeEvent(target) {
    478    let event = document.createEvent("UIEvent");
    479    event.initEvent("change", true, true);
    480    target.dispatchEvent(event);
    481  }
    482 
    483  // Clear the filter
    484  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
    485    detail: "",
    486  }));
    487 
    488  // Clear the selection so the 'new' login will be in the list too.
    489  window.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
    490    detail: {},
    491  }));
    492 
    493  // make sure that the logins have distinct orderings based on sort order
    494  let [guid1, guid2, guid3] = gLoginList._loginGuidsSortedOrder;
    495  gLoginList._logins[guid1].login.timeLastUsed = 0;
    496  gLoginList._logins[guid2].login.timeLastUsed = 1;
    497  gLoginList._logins[guid3].login.timeLastUsed = 2;
    498  gLoginList._logins[guid1].login.title = "a";
    499  gLoginList._logins[guid2].login.title = "b";
    500  gLoginList._logins[guid3].login.title = "c";
    501  gLoginList._logins[guid1].login.username = "a";
    502  gLoginList._logins[guid2].login.username = "b";
    503  gLoginList._logins[guid3].login.username = "c";
    504  gLoginList._logins[guid1].login.timePasswordChanged = 1;
    505  gLoginList._logins[guid2].login.timePasswordChanged = 2;
    506  gLoginList._logins[guid3].login.timePasswordChanged = 0;
    507 
    508  // sort by last used
    509  let loginSort = gLoginList.shadowRoot.getElementById("login-sort");
    510  loginSort.value = "last-used";
    511  dispatchChangeEvent(loginSort);
    512  let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    513  is(loginListItems.length, 3, "The list should contain the three stored logins");
    514  let timeUsed1 = gLoginList._logins[loginListItems[0].dataset.guid].login.timeLastUsed;
    515  let timeUsed2 = gLoginList._logins[loginListItems[1].dataset.guid].login.timeLastUsed;
    516  let timeUsed3 = gLoginList._logins[loginListItems[2].dataset.guid].login.timeLastUsed;
    517  is(timeUsed1 > timeUsed2, true, "Logins sorted by timeLastUsed. First: " + timeUsed1 + "; Second: " + timeUsed2);
    518  is(timeUsed2 > timeUsed3, true, "Logins sorted by timeLastUsed. Second: " + timeUsed2 + "; Third: " + timeUsed3);
    519 
    520  // sort by title
    521  loginSort.value = "name";
    522  dispatchChangeEvent(loginSort);
    523  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    524  let title1 = gLoginList._logins[loginListItems[0].dataset.guid].login.title;
    525  let title2 = gLoginList._logins[loginListItems[1].dataset.guid].login.title;
    526  let title3 = gLoginList._logins[loginListItems[2].dataset.guid].login.title;
    527  is(title1.localeCompare(title2), -1, "Logins sorted by title. First: " + title1 + "; Second: " + title2);
    528  is(title2.localeCompare(title3), -1, "Logins sorted by title. Second: " + title2 + "; Third: " + title3);
    529 
    530  // sort by title in reverse alphabetical order
    531  loginSort.value = "name-reverse";
    532  dispatchChangeEvent(loginSort);
    533  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    534  title1 = gLoginList._logins[loginListItems[0].dataset.guid].login.title;
    535  title2 = gLoginList._logins[loginListItems[1].dataset.guid].login.title;
    536  title3 = gLoginList._logins[loginListItems[2].dataset.guid].login.title;
    537  let testDescription = "Logins sorted by title in reverse alphabetical order."
    538  is(title1.localeCompare(title2), 1, `${testDescription} First: ${title2}; Second: ${title1}`);
    539  is(title2.localeCompare(title3), 1, `${testDescription} Second: ${title3}; Third: ${title2}`);
    540 
    541  // sort by last changed
    542  loginSort.value = "last-changed";
    543  dispatchChangeEvent(loginSort);
    544  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    545  let pwChanged1 = gLoginList._logins[loginListItems[0].dataset.guid].login.timePasswordChanged;
    546  let pwChanged2 = gLoginList._logins[loginListItems[1].dataset.guid].login.timePasswordChanged;
    547  let pwChanged3 = gLoginList._logins[loginListItems[2].dataset.guid].login.timePasswordChanged;
    548  is(pwChanged1 > pwChanged2, true, "Logins sorted by timePasswordChanged. First: " + pwChanged1 + "; Second: " + pwChanged2);
    549  is(pwChanged2 > pwChanged3, true, "Logins sorted by timePasswordChanged. Second: " + pwChanged2 + "; Third: " + pwChanged3);
    550 
    551  // sort by breached when there are breached logins
    552  gLoginList.setBreaches(TEST_BREACHES_MAP);
    553  loginSort.value = "alerts";
    554  let vulnerableLogins = new Map();
    555  gLoginList.setVulnerableLogins(vulnerableLogins);
    556  dispatchChangeEvent(loginSort);
    557  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    558  is(loginListItems[0].notificationIcon === "breached", true, "Breached login should be displayed at top of list");
    559  is(loginListItems[1].notificationIcon !== "breached", true, "Non-breached login should be displayed below breached");
    560 
    561  // sort by username
    562  loginSort.value = "username";
    563  dispatchChangeEvent(loginSort);
    564  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    565  let username1 = gLoginList._logins[loginListItems[0].dataset.guid].login.username;
    566  let username2 = gLoginList._logins[loginListItems[1].dataset.guid].login.username;
    567  let username3 = gLoginList._logins[loginListItems[2].dataset.guid].login.username;
    568  is(username1.localeCompare(username2), -1, "Logins sorted by username. First: " + username1 + "; Second: " + username2);
    569  is(username2.localeCompare(username3), -1, "Logins sorted by username. Second: " + username2 + "; Third: " + username3);
    570 
    571  // sort by username in reverse alphabetical order
    572  loginSort.value = "username-reverse";
    573  dispatchChangeEvent(loginSort);
    574  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    575  username1 = gLoginList._logins[loginListItems[0].dataset.guid].login.username;
    576  username2 = gLoginList._logins[loginListItems[1].dataset.guid].login.username;
    577  username3 = gLoginList._logins[loginListItems[2].dataset.guid].login.username;
    578  testDescription = "Logins sorted by username in reverse alphabetical order.";
    579  is(username3.localeCompare(username2), -1, `${testDescription} First: ${username3} Second: ${username2}`);
    580  is(username2.localeCompare(username1), -1, `${testDescription} Second: ${username2} Third: ${username1}`);
    581 
    582  // sort by name when there are no breached logins
    583  gLoginList.setBreaches(new Map());
    584  loginSort.value = "alerts";
    585  dispatchChangeEvent(loginSort);
    586  loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item:not(#new-login-list-item, [hidden])");
    587  title1 = gLoginList._logins[loginListItems[0].dataset.guid].login.title;
    588  title2 = gLoginList._logins[loginListItems[1].dataset.guid].login.title;
    589  is(title1.localeCompare(title2), -1, "Logins should be sorted alphabetically by hostname");
    590 });
    591 
    592 add_task(async function test_login_list_item_removed_next_selected() {
    593  let logins = [];
    594  for (let i = 0; i < 12; i++) {
    595    let group = i % 2 ? "BB" : "AA";
    596    // Create logins of the form `jared0AAa@example.com`,
    597    // `jared1BBb@example.com`, `jared2AAc@example.com`, etc.
    598    logins.push({
    599      guid: `${i}`,
    600      username: `jared${i}${group}${String.fromCharCode(97 + i)}@example.com`,
    601      password: "omgsecret!!1",
    602      origin: "https://www.example.com",
    603    });
    604  }
    605 
    606  gLoginList.setLogins(logins);
    607  let visibleLogins = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]:not([hidden])");
    608  await SimpleTest.promiseWaitForCondition(() => {
    609    return visibleLogins.length == 12;
    610  }, "Waiting for all logins to be visible");
    611  is(gLoginList._selectedGuid, logins[0].guid, "login0 should be selected by default");
    612 
    613  window.dispatchEvent(
    614    new CustomEvent("AboutLoginsFilterLogins", {
    615      bubbles: true,
    616      detail: "BB",
    617    })
    618  );
    619 
    620  await SimpleTest.promiseWaitForCondition(() => {
    621    visibleLogins = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]:not([hidden])");
    622    return visibleLogins.length == 6;
    623  }, "Only logins with BB in the username should be visible, visible count: " + visibleLogins.length);
    624 
    625  is(gLoginList._selectedGuid, logins[0].guid, "login0 should still be selected after filtering");
    626 
    627  gLoginList.loginRemoved({guid: logins[0].guid});
    628 
    629  await SimpleTest.promiseWaitForCondition(() => {
    630    return gLoginList._loginGuidsSortedOrder.length == 11;
    631  }, "Waiting for login to get removed");
    632 
    633  await SimpleTest.promiseWaitForCondition(() => {
    634    visibleLogins = gLoginList.shadowRoot.querySelectorAll("login-list-item[data-guid]:not([hidden])");
    635    return visibleLogins.length == 6;
    636  }, "the number of visible logins should not change, got " + visibleLogins.length);
    637  is(gLoginList._selectedGuid, logins[1].guid,
    638     "login1 should be selected after delete since the deleted login was not visible and login1 was the first in the list");
    639 
    640  let loginToSwitchTo = gLoginList._logins[visibleLogins[1].dataset.guid].login;
    641  window.dispatchEvent(
    642    new CustomEvent("AboutLoginsLoginSelected", {
    643      bubbles: true,
    644      detail: loginToSwitchTo,
    645    })
    646  );
    647  is(gLoginList._selectedGuid, loginToSwitchTo.guid, "login3 should be selected");
    648 
    649  gLoginList.loginRemoved({guid: logins[3].guid});
    650 
    651  await SimpleTest.promiseWaitForCondition(() => {
    652    return gLoginList._loginGuidsSortedOrder.length == 10;
    653  }, "Waiting for login to get removed");
    654 
    655  await SimpleTest.promiseWaitForCondition(() => {
    656    visibleLogins = gLoginList.shadowRoot.querySelectorAll(
    657      "login-list-item[data-guid]:not([hidden])"
    658    );
    659    return visibleLogins.length == 5;
    660  }, "the number of filtered logins should decrease by 1");
    661  is(gLoginList._selectedGuid, visibleLogins[0].dataset.guid, "the first login should now be selected");
    662 
    663  gLoginList.loginRemoved({guid: logins[1].guid});
    664 
    665  await SimpleTest.promiseWaitForCondition(() => {
    666    return gLoginList._loginGuidsSortedOrder.length == 9;
    667  }, "Waiting for login to get removed");
    668 
    669  await SimpleTest.promiseWaitForCondition(() => {
    670    visibleLogins = gLoginList.shadowRoot.querySelectorAll(
    671      "login-list-item[data-guid]:not([hidden])"
    672    );
    673    return visibleLogins.length == 4;
    674  }, "the number of filtered logins should decrease by 1");
    675  is(gLoginList._selectedGuid, visibleLogins[0].dataset.guid, "the first login should now still be selected");
    676 
    677  loginToSwitchTo = gLoginList._logins[visibleLogins[3].dataset.guid].login;
    678  window.dispatchEvent(
    679    new CustomEvent("AboutLoginsLoginSelected", {
    680      bubbles: true,
    681      detail: loginToSwitchTo,
    682    })
    683  );
    684  is(gLoginList._selectedGuid, visibleLogins[3].dataset.guid, "the last login should now still be selected");
    685 
    686  gLoginList.loginRemoved({guid: logins[10].guid});
    687 
    688  await SimpleTest.promiseWaitForCondition(() => {
    689    return gLoginList._loginGuidsSortedOrder.length == 8;
    690  }, "Waiting for login to get removed");
    691 
    692  await SimpleTest.promiseWaitForCondition(() => {
    693    visibleLogins = gLoginList.shadowRoot.querySelectorAll(
    694      "login-list-item[data-guid]:not([hidden])"
    695    );
    696    return visibleLogins.length == 4;
    697  }, "the number of filtered logins should decrease by 1");
    698  is(gLoginList._selectedGuid, visibleLogins[3].dataset.guid, "the last login should now be selected");
    699 
    700  loginToSwitchTo = gLoginList._logins[visibleLogins[2].dataset.guid].login;
    701  window.dispatchEvent(
    702    new CustomEvent("AboutLoginsLoginSelected", {
    703      bubbles: true,
    704      detail: loginToSwitchTo,
    705    })
    706  );
    707  is(gLoginList._selectedGuid, visibleLogins[2].dataset.guid, "the last login should now still be selected");
    708 });
    709 </script>
    710 
    711 </body>
    712 </html>