tor-browser

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

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>