test_login_item.html (28528B)
1 <!DOCTYPE HTML> 2 <html> 3 <!-- 4 Test the login-item component 5 --> 6 <head> 7 <meta charset="utf-8"> 8 <title>Test the login-item 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-item.mjs"></script> 12 <script type="module" src="chrome://browser/content/aboutlogins/components/login-command-button.mjs"></script> 13 <script type="module" src="chrome://browser/content/aboutlogins/components/confirmation-dialog.mjs"></script> 14 <script type="module" src="chrome://browser/content/aboutlogins/components/login-timeline.mjs"></script> 15 <script type="module" src="chrome://browser/content/aboutlogins/components/login-alert.mjs"></script> 16 <script src="aboutlogins_common.js"></script> 17 18 <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> 19 </head> 20 <body> 21 <p id="display"> 22 </p> 23 <div id="content" style="display: none"> 24 <iframe id="templateFrame" src="chrome://browser/content/aboutlogins/aboutLogins.html" 25 sandbox="allow-same-origin"></iframe> 26 </div> 27 <pre id="test"> 28 </pre> 29 <script type="module"> 30 31 import { CONCEALED_PASSWORD_TEXT } from "chrome://browser/content/aboutlogins/aboutLoginsUtils.mjs"; 32 33 /** Test the login-item component */ 34 35 let gLoginItem, gConfirmationDialog; 36 const TEST_LOGIN_1 = { 37 guid: "123456789", 38 origin: "https://example.com", 39 username: "user1", 40 password: "pass1", 41 timeCreated: "1000", 42 timePasswordChanged: "2000", 43 timeLastUsed: "4000", 44 }; 45 46 const TEST_LOGIN_2 = { 47 guid: "987654321", 48 origin: "https://example.com", 49 username: "user2", 50 password: "pass2", 51 timeCreated: "2000", 52 timePasswordChanged: "4000", 53 timeLastUsed: "8000", 54 }; 55 56 const TEST_LOGIN_3 = { 57 guid: "987654321", 58 origin: "https://example.com", 59 username: "user2", 60 password: "pass2", 61 timeCreated: "4000", 62 timePasswordChanged: "4000", 63 timeLastUsed: "4000", 64 }; 65 66 const TEST_BREACH = { 67 Name: "Test-Breach", 68 breachAlertURL: "https://monitor.firefox.com/breach-details/Test-Breach", 69 }; 70 71 const TEST_BREACHES_MAP = new Map(); 72 TEST_BREACHES_MAP.set(TEST_LOGIN_1.guid, TEST_BREACH); 73 74 const TEST_VULNERABLE_MAP = new Map(); 75 TEST_VULNERABLE_MAP.set(TEST_LOGIN_2.guid, true); 76 77 const getLoginTimeline = loginItem => 78 loginItem.shadowRoot.querySelector("login-timeline"); 79 80 const verifyTimelineActions = (actions, expectedActions) => { 81 is( 82 actions.length, 83 expectedActions.length, 84 `Number timeline actions length is correct. Actual: ${actions.length}. Expected: ${expectedActions.length}` 85 ); 86 87 actions.forEach((point, index) => { 88 let actionId = document.l10n.getAttributes(point).id; 89 let expectedAction = expectedActions[index]; 90 91 is( 92 actionId, 93 expectedAction, 94 `Rendered action is correct. Actual: ${actionId}. Expected: ${expectedAction}` 95 ); 96 }); 97 }; 98 99 add_setup(async () => { 100 let templateFrame = document.getElementById("templateFrame"); 101 let displayEl = document.getElementById("display"); 102 await importDependencies(templateFrame, displayEl); 103 104 gLoginItem = document.createElement("login-item"); 105 displayEl.appendChild(gLoginItem); 106 107 gConfirmationDialog = document.createElement("confirmation-dialog"); 108 gConfirmationDialog.hidden = true; 109 displayEl.appendChild(gConfirmationDialog); 110 }); 111 112 add_task(async function test_empty_item() { 113 ok(gLoginItem, "loginItem exists"); 114 is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), "", "origin should be blank"); 115 is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be blank"); 116 is(gLoginItem._passwordInput.value, "", "password should be blank"); 117 ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected"); 118 is(gLoginItem._passwordDisplayInput.value, "", "password display should be blank"); 119 ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display input should be visible") 120 ok(isHidden(getLoginTimeline(gLoginItem)), "Timeline should be hidden"); 121 }); 122 123 add_task(async function test_set_login() { 124 gLoginItem.setLogin(TEST_LOGIN_1); 125 await asyncElementRendered(); 126 127 ok(!gLoginItem.dataset.editing, "loginItem should not be in 'edit' mode"); 128 ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode"); 129 ok(isHidden(gLoginItem._originInput), "Origin input should be hidden when not in edit mode"); 130 ok(!isHidden(gLoginItem._originDisplayInput), "Origin display link should be visible when not in edit mode"); 131 let originLink = gLoginItem.shadowRoot.querySelector("a[name='origin']"); 132 is(originLink.getAttribute("href"), TEST_LOGIN_1.origin, "origin should be populated"); 133 let usernameInput = gLoginItem.shadowRoot.querySelector("input[name='username']"); 134 is(usernameInput.value, TEST_LOGIN_1.username, "username should be populated"); 135 is(document.l10n.getAttributes(usernameInput).id, "about-logins-login-item-username", "username field should have default placeholder when not editing"); 136 137 let passwordInput = gLoginItem._passwordInput; 138 is(passwordInput.value, TEST_LOGIN_1.password, "password should be populated"); 139 ok(!passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value"); 140 ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected"); 141 let passwordDisplayInput = gLoginItem._passwordDisplayInput; 142 is(passwordDisplayInput.value, CONCEALED_PASSWORD_TEXT, "password display should be populated"); 143 ok(!isHidden(passwordDisplayInput), "Password display input should be visible"); 144 145 let copyButtons = [...gLoginItem.shadowRoot.querySelectorAll(".copy-button")]; 146 ok(copyButtons.every(button => !isHidden(button)), "The copy buttons should be visible when viewing a login"); 147 148 let loginNoUsername = Object.assign({}, TEST_LOGIN_1, {username: ""}); 149 gLoginItem.setLogin(loginNoUsername); 150 await asyncElementRendered(); 151 152 ok(!gLoginItem.dataset.editing, "loginItem should not be in 'edit' mode"); 153 is(document.l10n.getAttributes(usernameInput).id, "about-logins-login-item-username", "username field should have default placeholder when username is not present and not editing"); 154 let copyUsernameButton = gLoginItem.shadowRoot.querySelector("copy-username-button"); 155 ok(copyUsernameButton.disabled, "The copy-username-button should be disabled if there is no username"); 156 157 usernameInput.placeholder = "dummy placeholder"; 158 gLoginItem.shadowRoot.querySelector("edit-button").click(); 159 await asyncElementRendered(); 160 is( 161 document.l10n.getAttributes(usernameInput).id, 162 null, 163 "there should be no placeholder id on the username input in edit mode" 164 ); 165 is(usernameInput.placeholder, "", "there should be no placeholder on the username input in edit mode"); 166 }); 167 168 add_task(async function test_update_breaches() { 169 gLoginItem.setLogin(TEST_LOGIN_1); 170 gLoginItem.setBreaches(TEST_BREACHES_MAP); 171 await asyncElementRendered(); 172 173 let breachAlert = gLoginItem.shadowRoot.querySelector("login-breach-alert"); 174 ok(!isHidden(breachAlert), "Breach alert should be visible"); 175 is(breachAlert.hostname, TEST_LOGIN_1.origin, "Link in the text should point to the login origin"); 176 let vulernableAlert = gLoginItem.shadowRoot.querySelector("login-vulnerable-password-alert"); 177 ok(isHidden(vulernableAlert), "Vulnerable alert should not be visible on a non-vulnerable login."); 178 }); 179 180 add_task(async function test_breach_alert_is_correctly_hidden() { 181 gLoginItem.setLogin(TEST_LOGIN_2); 182 gLoginItem.setBreaches(TEST_BREACHES_MAP); 183 await asyncElementRendered(); 184 185 let breachAlert = gLoginItem.shadowRoot.querySelector("login-breach-alert"); 186 ok(isHidden(breachAlert), "Breach alert should not be visible on login without an associated breach."); 187 let vulernableAlert = gLoginItem.shadowRoot.querySelector("login-vulnerable-password-alert"); 188 ok(isHidden(vulernableAlert), "Vulnerable alert should not be visible on a non-vulnerable login."); 189 }); 190 191 add_task(async function test_update_vulnerable() { 192 gLoginItem.setLogin(TEST_LOGIN_2); 193 gLoginItem.setVulnerableLogins(TEST_VULNERABLE_MAP); 194 await asyncElementRendered(); 195 196 let breachAlert = gLoginItem.shadowRoot.querySelector("login-breach-alert"); 197 ok(isHidden(breachAlert), "Breach alert should not be visible on login without an associated breach."); 198 let vulernableAlert = gLoginItem.shadowRoot.querySelector("login-vulnerable-password-alert"); 199 ok(!isHidden(vulernableAlert), "Vulnerable alert should be visible"); 200 is(vulernableAlert.shadowRoot.querySelector(".alert-link").href, TEST_LOGIN_2.origin + "/", "Link in the text should point to the login origin"); 201 }); 202 203 add_task(async function test_vulnerable_alert_is_correctly_hidden() { 204 gLoginItem.setLogin(TEST_LOGIN_1); 205 gLoginItem.setVulnerableLogins(TEST_VULNERABLE_MAP); 206 gLoginItem.setBreaches(new Map()); 207 await asyncElementRendered(); 208 209 let breachAlert = gLoginItem.shadowRoot.querySelector("login-breach-alert"); 210 ok(isHidden(breachAlert), "Breach alert should not be visible on login without an associated breach."); 211 let vulernableAlert = gLoginItem.shadowRoot.querySelector("login-vulnerable-password-alert"); 212 ok(isHidden(vulernableAlert), "Vulnerable alert should not be visible on a non-vulnerable login."); 213 }); 214 215 add_task(async function test_edit_login() { 216 gLoginItem.setLogin(TEST_LOGIN_1); 217 let usernameInput = gLoginItem.shadowRoot.querySelector("input[name='username']"); 218 usernameInput.placeholder = "dummy placeholder"; 219 gLoginItem.shadowRoot.querySelector("edit-button").click(); 220 await asyncElementRendered(); 221 await asyncElementRendered(); 222 223 ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode"); 224 ok(isHidden(gLoginItem.shadowRoot.querySelector("edit-button")), "edit button should be hidden in 'edit' mode"); 225 ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode"); 226 let deleteButton = gLoginItem.shadowRoot.querySelector("delete-button"); 227 ok(!deleteButton.disabled, "Delete button should be enabled when editing a login"); 228 ok(isHidden(gLoginItem._originInput), "Origin input should be hidden in edit mode"); 229 ok(!isHidden(gLoginItem._originDisplayInput), "Origin display link should be visible in edit mode"); 230 is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), TEST_LOGIN_1.origin, "origin should be populated"); 231 is(usernameInput.value, TEST_LOGIN_1.username, "username should be populated"); 232 is(usernameInput, document.activeElement?.shadowRoot?.activeElement, "username is focused"); 233 is(usernameInput.selectionStart, 0, "username value is selected from start"); 234 is(usernameInput.selectionEnd, usernameInput.value.length, "username value is selected to the end"); 235 is( 236 document.l10n.getAttributes(usernameInput).id, 237 null, 238 "there should be no placeholder id on the username input in edit mode" 239 ); 240 is(usernameInput.placeholder, "", "there should be no placeholder on the username input in edit mode"); 241 is(gLoginItem._passwordInput.value, TEST_LOGIN_1.password, "password should be populated"); 242 is(gLoginItem._passwordDisplayInput.value, CONCEALED_PASSWORD_TEXT, "password display should be populated"); 243 244 let timeline = getLoginTimeline(gLoginItem); 245 ok(!isHidden(timeline), "Timeline should be visible"); 246 247 let copyButtons = [...gLoginItem.shadowRoot.querySelectorAll(".copy-button")]; 248 ok(copyButtons.every(button => isHidden(button)), "The copy buttons should be hidden when editing a login"); 249 250 usernameInput.value = "newUsername"; 251 gLoginItem._passwordInput.value = "newPassword"; 252 253 let updateEventDispatched = false; 254 document.addEventListener("AboutLoginsUpdateLogin", event => { 255 is(event.detail.guid, TEST_LOGIN_1.guid, "event should include guid"); 256 is(event.detail.origin, TEST_LOGIN_1.origin, "event should include origin"); 257 is(event.detail.username, "newUsername", "event should include new username"); 258 is(event.detail.password, "newPassword", "event should include new password"); 259 updateEventDispatched = true; 260 }, {once: true}); 261 gLoginItem.shadowRoot.querySelector(".save-changes-button").click(); 262 ok(updateEventDispatched, "Clicking the .save-changes-button should dispatch the AboutLoginsUpdateLogin event"); 263 }); 264 265 add_task(async function test_edit_login_keyboard_shortcut() { 266 gLoginItem.setLogin(TEST_LOGIN_1); 267 let usernameInput = gLoginItem.shadowRoot.querySelector("input[name='username']"); 268 usernameInput.placeholder = "dummy placeholder"; 269 const ev = new KeyboardEvent("keydown", { altKey:true, key:"Enter" }); 270 window.dispatchEvent(ev); 271 await asyncElementRendered(); 272 273 ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode"); 274 ok(isHidden(gLoginItem.shadowRoot.querySelector("edit-button")), "edit button should be hidden in 'edit' mode"); 275 ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode"); 276 let deleteButton = gLoginItem.shadowRoot.querySelector("delete-button"); 277 ok(!deleteButton.disabled, "Delete button should be enabled when editing a login"); 278 ok(isHidden(gLoginItem._originInput), "Origin input should be hidden in edit mode"); 279 ok(!isHidden(gLoginItem._originDisplayInput), "Origin display link should be visible in edit mode"); 280 is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), TEST_LOGIN_1.origin, "origin should be populated"); 281 is(usernameInput.value, TEST_LOGIN_1.username, "username should be populated"); 282 is(usernameInput, document.activeElement?.shadowRoot?.activeElement, "username is focused"); 283 is(usernameInput.selectionStart, 0, "username value is selected from start"); 284 is(usernameInput.selectionEnd, usernameInput.value.length, "username value is selected to the end"); 285 is( 286 document.l10n.getAttributes(usernameInput).id, 287 null, 288 "there should be no placeholder id on the username input in edit mode" 289 ); 290 is(usernameInput.placeholder, "", "there should be no placeholder on the username input in edit mode"); 291 is(gLoginItem._passwordInput.value, TEST_LOGIN_1.password, "password should be populated"); 292 is(gLoginItem._passwordDisplayInput.value, CONCEALED_PASSWORD_TEXT, "password display should be populated"); 293 294 let timeline = getLoginTimeline(gLoginItem); 295 ok(!isHidden(timeline), "Timeline should be visible"); 296 297 let copyButtons = [...gLoginItem.shadowRoot.querySelectorAll(".copy-button")]; 298 ok(copyButtons.every(button => isHidden(button)), "The copy buttons should be hidden when editing a login"); 299 300 usernameInput.value = "newUsername"; 301 gLoginItem._passwordInput.value = "newPassword"; 302 303 let updateEventDispatched = false; 304 document.addEventListener("AboutLoginsUpdateLogin", event => { 305 is(event.detail.guid, TEST_LOGIN_1.guid, "event should include guid"); 306 is(event.detail.origin, TEST_LOGIN_1.origin, "event should include origin"); 307 is(event.detail.username, "newUsername", "event should include new username"); 308 is(event.detail.password, "newPassword", "event should include new password"); 309 updateEventDispatched = true; 310 }, {once: true}); 311 gLoginItem.shadowRoot.querySelector(".save-changes-button").click(); 312 ok(updateEventDispatched, "Clicking the .save-changes-button should dispatch the AboutLoginsUpdateLogin event"); 313 }) 314 315 add_task(async function test_edit_login_cancel() { 316 gLoginItem.setLogin(TEST_LOGIN_1); 317 gLoginItem.shadowRoot.querySelector("edit-button").click(); 318 await asyncElementRendered(); 319 320 ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode"); 321 is(!!gLoginItem.dataset.isNewLogin, false, 322 "loginItem should not be in 'isNewLogin' mode"); 323 324 gLoginItem.shadowRoot.querySelector(".cancel-button").click(); 325 gConfirmationDialog.shadowRoot.querySelector(".confirm-button").click(); 326 327 await SimpleTest.promiseWaitForCondition( 328 () => gConfirmationDialog.hidden, 329 "waiting for confirmation dialog to hide" 330 ); 331 332 ok(!gLoginItem.dataset.editing, "loginItem should not be in 'edit' mode"); 333 ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode"); 334 }); 335 336 add_task(async function test_edit_login_cancel_keyboard_shortcut() { 337 gLoginItem.setLogin(TEST_LOGIN_1); 338 gLoginItem.shadowRoot.querySelector("edit-button").click(); 339 await asyncElementRendered(); 340 341 ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode"); 342 is(!!gLoginItem.dataset.isNewLogin, false, 343 "loginItem should not be in 'isNewLogin' mode"); 344 345 const ev = new KeyboardEvent("keydown", { key: "Escape" }); 346 window.dispatchEvent(ev); 347 348 gConfirmationDialog.shadowRoot.querySelector(".confirm-button").click(); 349 350 await SimpleTest.promiseWaitForCondition( 351 () => gConfirmationDialog.hidden, 352 "waiting for confirmation dialog to hide" 353 ); 354 355 ok(!gLoginItem.dataset.editing, "loginItem should not be in 'edit' mode"); 356 ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode"); 357 }); 358 359 add_task(async function test_reveal_password_change_selected_login() { 360 gLoginItem.setLogin(TEST_LOGIN_1); 361 let revealCheckbox = gLoginItem.shadowRoot.querySelector(".reveal-password-checkbox"); 362 let passwordInput = gLoginItem._passwordInput; 363 364 ok(!revealCheckbox.checked, "reveal-checkbox should not be checked by default"); 365 is(passwordInput.type, "password", "Password should be masked by default"); 366 revealCheckbox.click(); 367 ok(revealCheckbox.checked, "reveal-checkbox should be checked after clicking"); 368 await SimpleTest.promiseWaitForCondition(() => passwordInput.type == "text", 369 "waiting for password input type to change after checking for primary password"); 370 is(passwordInput.type, "text", "Password should be unmasked when checkbox is clicked"); 371 ok(!isHidden(passwordInput), "Password input should be visible"); 372 373 let editButton = gLoginItem.shadowRoot.querySelector("edit-button"); 374 editButton.click(); 375 await asyncElementRendered(); 376 ok(!isHidden(passwordInput), "Password input should still be visible"); 377 ok(revealCheckbox.checked, "reveal-checkbox should remain checked when entering 'edit' mode"); 378 gLoginItem.shadowRoot.querySelector(".cancel-button").click(); 379 ok(!revealCheckbox.checked, "reveal-checkbox should be unchecked after canceling 'edit' mode"); 380 revealCheckbox.click(); 381 ok(isHidden(passwordInput), "Password input should be hidden"); 382 ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible"); 383 gLoginItem.setLogin(TEST_LOGIN_2); 384 ok(!revealCheckbox.checked, "reveal-checkbox should be unchecked when changing logins"); 385 is(passwordInput.type, "password", "Password should be masked by default when switching logins"); 386 ok(isHidden(passwordInput), "Password input should be hidden"); 387 ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible"); 388 }); 389 390 add_task(async function test_set_login_empty() { 391 gLoginItem.setLogin({}); 392 await asyncElementRendered(); 393 394 ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode"); 395 ok(isHidden(gLoginItem.shadowRoot.querySelector("edit-button")), "edit button should be hidden in 'edit' mode"); 396 ok(gLoginItem.dataset.isNewLogin, "loginItem should be in 'isNewLogin' mode"); 397 let deleteButton = gLoginItem.shadowRoot.querySelector("delete-button"); 398 ok(deleteButton.disabled, "Delete button should be disabled when creating a login"); 399 ok(!isHidden(gLoginItem._originInput), "Origin input should be visible in new login edit mode"); 400 ok(isHidden(gLoginItem._originDisplayInput), "Origin display should be hidden in new login edit mode"); 401 is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be empty"); 402 is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be empty"); 403 is(gLoginItem._passwordInput.value, "", "password should be empty"); 404 ok(!isHidden(gLoginItem._passwordInput), "Real password input should be visible in edit mode"); 405 ok(isHidden(gLoginItem._passwordDisplayInput), "Password display should be hidden in edit mode"); 406 ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value"); 407 408 let timeline = getLoginTimeline(gLoginItem); 409 ok(isHidden(timeline), "Timeline should be visible"); 410 411 let copyButtons = [...gLoginItem.shadowRoot.querySelectorAll(".copy-button")]; 412 ok(copyButtons.every(button => isHidden(button)), "The copy buttons should be hidden when creating a login"); 413 414 let createEventDispatched = false; 415 document.addEventListener("AboutLoginsCreateLogin", (_e) => { 416 createEventDispatched = true; 417 }, {once: true}); 418 gLoginItem.shadowRoot.querySelector(".save-changes-button").click(); 419 ok(!createEventDispatched, "Clicking the .save-changes-button shouldn't dispatch the event when fields are invalid"); 420 let originInput = gLoginItem.shadowRoot.querySelector("input[name='origin']"); 421 ok(originInput.matches(":invalid"), "origin value is required"); 422 is(originInput.value, "", "origin input should be blank at start"); 423 424 for (let originTuple of [ 425 ["ftp://ftp.example.com/", "ftp://ftp.example.com/"], 426 ["https://example.com/", "https://example.com/"], 427 ["http://example.com/", "http://example.com/"], 428 ["www.example.com/bar", "https://www.example.com/bar"], 429 ["example.com/foo", "https://example.com/foo"], 430 ]) { 431 originInput.value = originTuple[0]; 432 sendKey("TAB"); 433 is(originInput.value, originTuple[1], 434 "origin input should have https:// prefix when not provided by user"); 435 // Return focus back to the origin input 436 synthesizeKey("VK_TAB", { shiftKey: true }); 437 } 438 439 gLoginItem.shadowRoot.querySelector("input[name='username']").value = "user1"; 440 gLoginItem._passwordInput.value = "pass1"; 441 442 document.addEventListener("AboutLoginsCreateLogin", event => { 443 is(event.detail.guid, undefined, "event should not include guid"); 444 is(event.detail.origin, "https://example.com/foo", "event should include origin"); 445 is(event.detail.username, "user1", "event should include new username"); 446 is(event.detail.password, "pass1", "event should include new password"); 447 createEventDispatched = true; 448 }, {once: true}); 449 gLoginItem.shadowRoot.querySelector(".save-changes-button").click(); 450 ok(createEventDispatched, "Clicking the .save-changes-button should dispatch the AboutLoginsCreateLogin event"); 451 }); 452 453 add_task(async function test_different_login_modified() { 454 gLoginItem.setLogin(TEST_LOGIN_1); 455 let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"}); 456 gLoginItem.loginModified(otherLogin); 457 await asyncElementRendered(); 458 459 is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), TEST_LOGIN_1.origin, "origin should be unchanged"); 460 is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged"); 461 is(gLoginItem._passwordInput.value, TEST_LOGIN_1.password, "password should be unchanged"); 462 ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value"); 463 ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected in masked non-edit mode"); 464 ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible in masked non-edit mode"); 465 }); 466 467 add_task(async function test_different_login_removed() { 468 gLoginItem.setLogin(TEST_LOGIN_1); 469 let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"}); 470 gLoginItem.loginRemoved(otherLogin); 471 await asyncElementRendered(); 472 473 is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), TEST_LOGIN_1.origin, "origin should be unchanged"); 474 is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged"); 475 is(gLoginItem._passwordInput.value, TEST_LOGIN_1.password, "password should be unchanged"); 476 ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value"); 477 ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected in masked non-edit mode"); 478 ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible in masked non-edit mode"); 479 }); 480 481 add_task(async function test_login_modified() { 482 gLoginItem.setLogin(TEST_LOGIN_1); 483 let modifiedLogin = Object.assign({}, TEST_LOGIN_1, {username: "updateduser"}); 484 gLoginItem.loginModified(modifiedLogin); 485 await asyncElementRendered(); 486 487 is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), modifiedLogin.origin, "origin should be updated"); 488 is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, modifiedLogin.username, "username should be updated"); 489 is(gLoginItem._passwordInput.value, modifiedLogin.password, "password should be updated"); 490 ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value"); 491 ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected in masked non-edit mode"); 492 ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible in masked non-edit mode"); 493 }); 494 495 add_task(async function test_login_removed() { 496 gLoginItem.setLogin(TEST_LOGIN_1); 497 gLoginItem.loginRemoved(TEST_LOGIN_1); 498 await asyncElementRendered(); 499 500 is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be cleared"); 501 is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be cleared"); 502 is(gLoginItem._passwordInput.value, "", "password should be cleared"); 503 ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value"); 504 505 let timeline = getLoginTimeline(gLoginItem); 506 ok(isHidden(timeline), "Timeline should be visible"); 507 }); 508 509 add_task(async function test_login_long_username_scrollLeft_reset() { 510 let loginLongUsername = Object.assign({}, TEST_LOGIN_1, {username: "user2longnamelongnamelongnamelongnamelongname"}); 511 gLoginItem.setLogin(loginLongUsername); 512 gLoginItem.shadowRoot.querySelector("edit-button").click(); 513 await asyncElementRendered(); 514 await asyncElementRendered(); 515 let usernameInput = gLoginItem.shadowRoot.querySelector("input[name='username']"); 516 usernameInput.scrollLeft = usernameInput.scrollLeftMax; 517 gLoginItem.shadowRoot.querySelector(".cancel-button").click(); 518 is(usernameInput.scrollLeft, 0, "username input should be scrolled horizontally to the beginning"); 519 }); 520 521 add_task(async function test_copy_button_state() { 522 gLoginItem.setLogin(TEST_LOGIN_1); 523 await asyncElementRendered(); 524 525 let copyUsernameButton = gLoginItem.shadowRoot.querySelector("copy-username-button"); 526 ok(!copyUsernameButton.disabled, "The copy-username-button should be enabled"); 527 528 let copyPasswordButton = gLoginItem.shadowRoot.querySelector("copy-password-button"); 529 ok(!copyPasswordButton.disabled, "The copy-password-button should be enabled"); 530 531 copyUsernameButton.click(); 532 await asyncElementRendered(); 533 534 copyPasswordButton.click(); 535 await asyncElementRendered(); 536 537 let loginNoUsername = Object.assign({}, TEST_LOGIN_2, {username: ""}); 538 gLoginItem.setLogin(loginNoUsername); 539 await asyncElementRendered(); 540 541 ok(copyUsernameButton.disabled, "The copy-username-button should be disabled when the username is empty"); 542 ok(!copyPasswordButton.disabled, "The copy-password-button should be enabled"); 543 544 copyPasswordButton.click(); 545 }); 546 547 add_task(async function test_login_timeline_state() { 548 gLoginItem.setLogin(TEST_LOGIN_1); 549 await asyncElementRendered(); 550 551 let timeline = getLoginTimeline(gLoginItem); 552 ok(!isHidden(timeline), "Timeline should be visible"); 553 is(timeline.history.length, 3, "All 3 timestamps (created, updated, used) must be shown") 554 let actions = timeline.shadowRoot.querySelectorAll(".action"); 555 verifyTimelineActions(actions, [ 556 "login-item-timeline-action-created", 557 "login-item-timeline-action-updated", 558 "login-item-timeline-action-used", 559 ]); 560 561 gLoginItem.setLogin(TEST_LOGIN_3); 562 await asyncElementRendered(); 563 564 timeline = getLoginTimeline(gLoginItem); 565 ok(!isHidden(timeline), "Timeline should be visible"); 566 is(timeline.history.length, 2, "Created and Last Used must be shown only") 567 actions = timeline.shadowRoot.querySelectorAll(".action"); 568 verifyTimelineActions(actions, [ 569 "login-item-timeline-action-created", 570 "login-item-timeline-action-used", 571 ]); 572 }); 573 574 </script> 575 576 </body> 577 </html>