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>