summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutlogins/tests/chrome/test_login_item.html
blob: a7946a06188a1acc83d59bda0bf44cbe1aa0faaa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
<!DOCTYPE HTML>
<html>
<!--
Test the login-item component
-->
<head>
  <meta charset="utf-8">
  <title>Test the login-item component</title>
  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
  <script type="module" src="chrome://browser/content/aboutlogins/components/login-item.mjs"></script>
  <script type="module" src="chrome://browser/content/aboutlogins/components/confirmation-dialog.mjs"></script>
  <script type="module" src="chrome://browser/content/aboutlogins/components/login-timeline.mjs"></script>
  <script src="aboutlogins_common.js"></script>

  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
  <p id="display">
  </p>
<div id="content" style="display: none">
  <iframe id="templateFrame" src="chrome://browser/content/aboutlogins/aboutLogins.html"
          sandbox="allow-same-origin"></iframe>
</div>
<pre id="test">
</pre>
<script type="module">

import { CONCEALED_PASSWORD_TEXT } from "chrome://browser/content/aboutlogins/aboutLoginsUtils.mjs";

/** Test the login-item component **/

let gLoginItem, gConfirmationDialog;
const TEST_LOGIN_1 = {
  guid: "123456789",
  origin: "https://example.com",
  username: "user1",
  password: "pass1",
  timeCreated: "1000",
  timePasswordChanged: "2000",
  timeLastUsed: "4000",
};

const TEST_LOGIN_2 = {
  guid: "987654321",
  origin: "https://example.com",
  username: "user2",
  password: "pass2",
  timeCreated: "2000",
  timePasswordChanged: "4000",
  timeLastUsed: "8000",
};

const TEST_BREACH = {
  Name: "Test-Breach",
  breachAlertURL: "https://monitor.firefox.com/breach-details/Test-Breach",
};

const TEST_BREACHES_MAP = new Map();
TEST_BREACHES_MAP.set(TEST_LOGIN_1.guid, TEST_BREACH);

const TEST_VULNERABLE_MAP = new Map();
TEST_VULNERABLE_MAP.set(TEST_LOGIN_2.guid, true);

const getLoginTimeline = loginItem =>
  loginItem.shadowRoot.querySelector("login-timeline");

const verifyTimelineActions = (actions, expectedActions) => {
  is(
    actions.length,
    expectedActions.length,
    `Number timeline actions length is correct. Actual: ${actions.length}. Expected: ${expectedActions.length}`
  );

  actions.forEach((point, index) => {
    let actionId = document.l10n.getAttributes(point).id;
    let expectedAction = expectedActions[index];

    is(
      actionId,
      expectedAction,
      `Rendered action is correct. Actual: ${actionId}. Expected: ${expectedAction}`
    );
  });
};

add_setup(async () => {
  let templateFrame = document.getElementById("templateFrame");
  let displayEl = document.getElementById("display");
  await importDependencies(templateFrame, displayEl);

  gLoginItem = document.createElement("login-item");
  displayEl.appendChild(gLoginItem);

  gConfirmationDialog = document.createElement("confirmation-dialog");
  gConfirmationDialog.hidden = true;
  displayEl.appendChild(gConfirmationDialog);
});

add_task(async function test_empty_item() {
  ok(gLoginItem, "loginItem exists");
  is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), "", "origin should be blank");
  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be blank");
  is(gLoginItem._passwordInput.value, "", "password should be blank");
  ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected");
  is(gLoginItem._passwordDisplayInput.value, "", "password display should be blank");
  ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display input should be visible")
  ok(isHidden(getLoginTimeline(gLoginItem)), "Timeline should be hidden");
});

add_task(async function test_set_login() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  await asyncElementRendered();

  ok(!gLoginItem.dataset.editing, "loginItem should not be in 'edit' mode");
  ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode");
  ok(isHidden(gLoginItem._originInput), "Origin input should be hidden when not in edit mode");
  ok(!isHidden(gLoginItem._originDisplayInput), "Origin display link should be visible when not in edit mode");
  let originLink = gLoginItem.shadowRoot.querySelector("a[name='origin']");
  is(originLink.getAttribute("href"), TEST_LOGIN_1.origin, "origin should be populated");
  let usernameInput = gLoginItem.shadowRoot.querySelector("input[name='username']");
  is(usernameInput.value, TEST_LOGIN_1.username, "username should be populated");
  is(document.l10n.getAttributes(usernameInput).id, "about-logins-login-item-username", "username field should have default placeholder when not editing");

  let passwordInput = gLoginItem._passwordInput;
  is(passwordInput.value, TEST_LOGIN_1.password, "password should be populated");
  ok(!passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value");
  ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected");
  let passwordDisplayInput = gLoginItem._passwordDisplayInput;
  is(passwordDisplayInput.value, CONCEALED_PASSWORD_TEXT, "password display should be populated");
  ok(!isHidden(passwordDisplayInput), "Password display input should be visible");

  let timeline = getLoginTimeline(gLoginItem);
  ok(!isHidden(timeline), "Timeline should be visible");
  let actions = timeline.shadowRoot.querySelectorAll(".action");
  verifyTimelineActions(actions, [
    "login-item-timeline-action-created",
    "login-item-timeline-action-updated",
    "login-item-timeline-action-used",
  ]);

  let copyButtons = [...gLoginItem.shadowRoot.querySelectorAll(".copy-button")];
  ok(copyButtons.every(button => !isHidden(button)), "The copy buttons should be visible when viewing a login");

  let loginNoUsername = Object.assign({}, TEST_LOGIN_1, {username: ""});
  gLoginItem.setLogin(loginNoUsername);
  ok(!gLoginItem.dataset.editing, "loginItem should not be in 'edit' mode");
  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");
  let copyUsernameButton = gLoginItem.shadowRoot.querySelector(".copy-username-button");
  ok(copyUsernameButton.disabled, "The copy-username-button should be disabled if there is no username");

  usernameInput.placeholder = "dummy placeholder";
  gLoginItem.shadowRoot.querySelector(".edit-button").click();
  await asyncElementRendered();
  is(
    document.l10n.getAttributes(usernameInput).id,
    null,
    "there should be no placeholder id on the username input in edit mode"
  );
  is(usernameInput.placeholder, "", "there should be no placeholder on the username input in edit mode");
});

add_task(async function test_update_breaches() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  gLoginItem.setBreaches(TEST_BREACHES_MAP);
  await asyncElementRendered();

  let breachAlert = gLoginItem.shadowRoot.querySelector(".breach-alert");
  ok(!isHidden(breachAlert), "Breach alert should be visible");
  is(breachAlert.querySelector(".alert-link").href, TEST_LOGIN_1.origin + "/", "Link in the text should point to the login origin");
  let vulernableAlert = gLoginItem.shadowRoot.querySelector(".vulnerable-alert");
  ok(isHidden(vulernableAlert), "Vulnerable alert should not be visible on a non-vulnerable login.");
});

add_task(async function test_breach_alert_is_correctly_hidden() {
  gLoginItem.setLogin(TEST_LOGIN_2);
  gLoginItem.setBreaches(TEST_BREACHES_MAP);
  await asyncElementRendered();

  let breachAlert = gLoginItem.shadowRoot.querySelector(".breach-alert");
  ok(isHidden(breachAlert), "Breach alert should not be visible on login without an associated breach.");
  let vulernableAlert = gLoginItem.shadowRoot.querySelector(".vulnerable-alert");
  ok(isHidden(vulernableAlert), "Vulnerable alert should not be visible on a non-vulnerable login.");
});

add_task(async function test_update_vulnerable() {
  gLoginItem.setLogin(TEST_LOGIN_2);
  gLoginItem.setVulnerableLogins(TEST_VULNERABLE_MAP);
  await asyncElementRendered();

  let breachAlert = gLoginItem.shadowRoot.querySelector(".breach-alert");
  ok(isHidden(breachAlert), "Breach alert should not be visible on login without an associated breach.");
  let vulernableAlert = gLoginItem.shadowRoot.querySelector(".vulnerable-alert");
  ok(!isHidden(vulernableAlert), "Vulnerable alert should be visible");
  is(vulernableAlert.querySelector(".alert-link").href, TEST_LOGIN_2.origin + "/", "Link in the text should point to the login origin");
});

add_task(async function test_vulnerable_alert_is_correctly_hidden() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  gLoginItem.setVulnerableLogins(TEST_VULNERABLE_MAP);
  gLoginItem.setBreaches(new Map());
  await asyncElementRendered();

  let breachAlert = gLoginItem.shadowRoot.querySelector(".breach-alert");
  ok(isHidden(breachAlert), "Breach alert should not be visible on login without an associated breach.");
  let vulernableAlert = gLoginItem.shadowRoot.querySelector(".vulnerable-alert");
  ok(isHidden(vulernableAlert), "Vulnerable alert should not be visible on a non-vulnerable login.");
});

add_task(async function test_edit_login() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  let usernameInput = gLoginItem.shadowRoot.querySelector("input[name='username']");
  usernameInput.placeholder = "dummy placeholder";
  gLoginItem.shadowRoot.querySelector(".edit-button").click();
  await asyncElementRendered();
  await asyncElementRendered();

  ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode");
  ok(isHidden(gLoginItem.shadowRoot.querySelector(".edit-button")), "edit button should be hidden in 'edit' mode");
  ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode");
  let deleteButton = gLoginItem.shadowRoot.querySelector(".delete-button");
  ok(!deleteButton.disabled, "Delete button should be enabled when editing a login");
  ok(isHidden(gLoginItem._originInput), "Origin input should be hidden in edit mode");
  ok(!isHidden(gLoginItem._originDisplayInput), "Origin display link should be visible in edit mode");
  is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), TEST_LOGIN_1.origin, "origin should be populated");
  is(usernameInput.value, TEST_LOGIN_1.username, "username should be populated");
  is(usernameInput, document.activeElement?.shadowRoot?.activeElement, "username is focused");
  is(usernameInput.selectionStart, 0, "username value is selected from start");
  is(usernameInput.selectionEnd, usernameInput.value.length, "username value is selected to the end");
  is(
    document.l10n.getAttributes(usernameInput).id,
    null,
    "there should be no placeholder id on the username input in edit mode"
  );
  is(usernameInput.placeholder, "", "there should be no placeholder on the username input in edit mode");
  is(gLoginItem._passwordInput.value, TEST_LOGIN_1.password, "password should be populated");
  is(gLoginItem._passwordDisplayInput.value, CONCEALED_PASSWORD_TEXT, "password display should be populated");

  let timeline = getLoginTimeline(gLoginItem);
  ok(!isHidden(timeline), "Timeline should be visible");

  let copyButtons = [...gLoginItem.shadowRoot.querySelectorAll(".copy-button")];
  ok(copyButtons.every(button => isHidden(button)), "The copy buttons should be hidden when editing a login");

  usernameInput.value = "newUsername";
  gLoginItem._passwordInput.value = "newPassword";

  let updateEventDispatched = false;
  document.addEventListener("AboutLoginsUpdateLogin", event => {
    is(event.detail.guid, TEST_LOGIN_1.guid, "event should include guid");
    is(event.detail.origin, TEST_LOGIN_1.origin, "event should include origin");
    is(event.detail.username, "newUsername", "event should include new username");
    is(event.detail.password, "newPassword", "event should include new password");
    updateEventDispatched = true;
  }, {once: true});
  gLoginItem.shadowRoot.querySelector(".save-changes-button").click();
  ok(updateEventDispatched, "Clicking the .save-changes-button should dispatch the AboutLoginsUpdateLogin event");
});

add_task(async function test_edit_login_cancel() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  gLoginItem.shadowRoot.querySelector(".edit-button").click();
  await asyncElementRendered();

  ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode");
  is(!!gLoginItem.dataset.isNewLogin, false,
     "loginItem should not be in 'isNewLogin' mode");

  gLoginItem.shadowRoot.querySelector(".cancel-button").click();
  gConfirmationDialog.shadowRoot.querySelector(".confirm-button").click();

  await SimpleTest.promiseWaitForCondition(
    () => gConfirmationDialog.hidden,
    "waiting for confirmation dialog to hide"
  );

  ok(!gLoginItem.dataset.editing, "loginItem should not be in 'edit' mode");
  ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode");
});

add_task(async function test_reveal_password_change_selected_login() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  let revealCheckbox = gLoginItem.shadowRoot.querySelector(".reveal-password-checkbox");
  let passwordInput = gLoginItem._passwordInput;

  ok(!revealCheckbox.checked, "reveal-checkbox should not be checked by default");
  is(passwordInput.type, "password", "Password should be masked by default");
  revealCheckbox.click();
  ok(revealCheckbox.checked, "reveal-checkbox should be checked after clicking");
  await SimpleTest.promiseWaitForCondition(() => passwordInput.type == "text",
    "waiting for password input type to change after checking for primary password");
  is(passwordInput.type, "text", "Password should be unmasked when checkbox is clicked");
  ok(!isHidden(passwordInput), "Password input should be visible");

  let editButton = gLoginItem.shadowRoot.querySelector(".edit-button");
  editButton.click();
  await asyncElementRendered();
  ok(!isHidden(passwordInput), "Password input should still be visible");
  ok(revealCheckbox.checked, "reveal-checkbox should remain checked when entering 'edit' mode");
  gLoginItem.shadowRoot.querySelector(".cancel-button").click();
  ok(!revealCheckbox.checked, "reveal-checkbox should be unchecked after canceling 'edit' mode");
  revealCheckbox.click();
  ok(isHidden(passwordInput), "Password input should be hidden");
  ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible");
  gLoginItem.setLogin(TEST_LOGIN_2);
  ok(!revealCheckbox.checked, "reveal-checkbox should be unchecked when changing logins");
  is(passwordInput.type, "password", "Password should be masked by default when switching logins");
  ok(isHidden(passwordInput), "Password input should be hidden");
  ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible");
});

add_task(async function test_set_login_empty() {
  gLoginItem.setLogin({});
  await asyncElementRendered();

  ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode");
  ok(isHidden(gLoginItem.shadowRoot.querySelector(".edit-button")), "edit button should be hidden in 'edit' mode");
  ok(gLoginItem.dataset.isNewLogin, "loginItem should be in 'isNewLogin' mode");
  let deleteButton = gLoginItem.shadowRoot.querySelector(".delete-button");
  ok(deleteButton.disabled, "Delete button should be disabled when creating a login");
  ok(!isHidden(gLoginItem._originInput), "Origin input should be visible in new login edit mode");
  ok(isHidden(gLoginItem._originDisplayInput), "Origin display should be hidden in new login edit mode");
  is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be empty");
  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be empty");
  is(gLoginItem._passwordInput.value, "", "password should be empty");
  ok(!isHidden(gLoginItem._passwordInput), "Real password input should be visible in edit mode");
  ok(isHidden(gLoginItem._passwordDisplayInput), "Password display should be hidden in edit mode");
  ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value");

  let timeline = getLoginTimeline(gLoginItem);
  ok(isHidden(timeline), "Timeline should be visible");

  let copyButtons = [...gLoginItem.shadowRoot.querySelectorAll(".copy-button")];
  ok(copyButtons.every(button => isHidden(button)), "The copy buttons should be hidden when creating a login");

  let createEventDispatched = false;
  document.addEventListener("AboutLoginsCreateLogin", event => {
    createEventDispatched = true;
  }, {once: true});
  gLoginItem.shadowRoot.querySelector(".save-changes-button").click();
  ok(!createEventDispatched, "Clicking the .save-changes-button shouldn't dispatch the event when fields are invalid");
  let originInput = gLoginItem.shadowRoot.querySelector("input[name='origin']");
  ok(originInput.matches(":invalid"), "origin value is required");
  is(originInput.value, "", "origin input should be blank at start");

  for (let originTuple of [
    ["ftp://ftp.example.com/", "ftp://ftp.example.com/"],
    ["https://example.com/", "https://example.com/"],
    ["http://example.com/", "http://example.com/"],
    ["www.example.com/bar", "https://www.example.com/bar"],
    ["example.com/foo", "https://example.com/foo"],
  ]) {
    originInput.value = originTuple[0];
    sendKey("TAB");
    is(originInput.value, originTuple[1],
      "origin input should have https:// prefix when not provided by user");
    // Return focus back to the origin input
    synthesizeKey("VK_TAB", { shiftKey: true });
  }

  gLoginItem.shadowRoot.querySelector("input[name='username']").value = "user1";
  gLoginItem._passwordInput.value = "pass1";

  document.addEventListener("AboutLoginsCreateLogin", event => {
    is(event.detail.guid, undefined, "event should not include guid");
    is(event.detail.origin, "https://example.com/foo", "event should include origin");
    is(event.detail.username, "user1", "event should include new username");
    is(event.detail.password, "pass1", "event should include new password");
    createEventDispatched = true;
  }, {once: true});
  gLoginItem.shadowRoot.querySelector(".save-changes-button").click();
  ok(createEventDispatched, "Clicking the .save-changes-button should dispatch the AboutLoginsCreateLogin event");
});

add_task(async function test_different_login_modified() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
  gLoginItem.loginModified(otherLogin);
  await asyncElementRendered();

  is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), TEST_LOGIN_1.origin, "origin should be unchanged");
  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
  is(gLoginItem._passwordInput.value, TEST_LOGIN_1.password, "password should be unchanged");
  ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value");
  ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected in masked non-edit mode");
  ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible in masked non-edit mode");
});

add_task(async function test_different_login_removed() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
  gLoginItem.loginRemoved(otherLogin);
  await asyncElementRendered();

  is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), TEST_LOGIN_1.origin, "origin should be unchanged");
  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
  is(gLoginItem._passwordInput.value, TEST_LOGIN_1.password, "password should be unchanged");
  ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value");
  ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected in masked non-edit mode");
  ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible in masked non-edit mode");
});

add_task(async function test_login_modified() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  let modifiedLogin = Object.assign({}, TEST_LOGIN_1, {username: "updateduser"});
  gLoginItem.loginModified(modifiedLogin);
  await asyncElementRendered();

  is(gLoginItem.shadowRoot.querySelector("a[name='origin']").getAttribute("href"), modifiedLogin.origin, "origin should be updated");
  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, modifiedLogin.username, "username should be updated");
  is(gLoginItem._passwordInput.value, modifiedLogin.password, "password should be updated");
  ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value");
  ok(!gLoginItem._passwordInput.isConnected, "Real password input should be disconnected in masked non-edit mode");
  ok(!isHidden(gLoginItem._passwordDisplayInput), "Password display should be visible in masked non-edit mode");
});

add_task(async function test_login_removed() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  gLoginItem.loginRemoved(TEST_LOGIN_1);
  await asyncElementRendered();

  is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be cleared");
  is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be cleared");
  is(gLoginItem._passwordInput.value, "", "password should be cleared");
  ok(!gLoginItem._passwordInput.hasAttribute("value"), "Password shouldn't be exposed in @value");

  let timeline = getLoginTimeline(gLoginItem);
  ok(isHidden(timeline), "Timeline should be visible");
});

add_task(async function test_login_long_username_scrollLeft_reset() {
  let loginLongUsername = Object.assign({}, TEST_LOGIN_1, {username: "user2longnamelongnamelongnamelongnamelongname"});
  gLoginItem.setLogin(loginLongUsername);
  gLoginItem.shadowRoot.querySelector(".edit-button").click();
  await asyncElementRendered();
  await asyncElementRendered();
  let usernameInput = gLoginItem.shadowRoot.querySelector("input[name='username']");
  usernameInput.scrollLeft = usernameInput.scrollLeftMax;
  gLoginItem.shadowRoot.querySelector(".cancel-button").click();
  is(usernameInput.scrollLeft, 0, "username input should be scrolled horizontally to the beginning");
});

add_task(async function test_copy_button_state() {
  gLoginItem.setLogin(TEST_LOGIN_1);
  await asyncElementRendered();

  let copyUsernameButton = gLoginItem.shadowRoot.querySelector(".copy-username-button");
  ok(!copyUsernameButton.disabled, "The copy-username-button should be enabled");

  let copyPasswordButton = gLoginItem.shadowRoot.querySelector(".copy-password-button");
  ok(!copyPasswordButton.disabled, "The copy-passwoed-button should be enabled");

  copyUsernameButton.click();
  ok(copyUsernameButton.disabled, "The copy-username-button should be disabled when it is clicked");
  ok(!copyPasswordButton.disabled, "The copy-passwoed-button should be enabled when the copy-username-button is clicked");

  copyPasswordButton.click();
  await SimpleTest.promiseWaitForCondition(() => copyPasswordButton.disabled,
  "waiting for copy-password-button to become disabled after checking for primary password");

  ok(copyPasswordButton.disabled, "The copy-passwoed-button should be disabled when it is clicked");
  ok(!copyUsernameButton.disabled, "The copy-username-button should be enabled when the copy-password-button is clicked");

  let loginNoUsername = Object.assign({}, TEST_LOGIN_2, {username: ""});
  gLoginItem.setLogin(loginNoUsername);

  ok(copyUsernameButton.disabled, "The copy-username-button should be disabled when the username is empty");
  ok(!copyPasswordButton.disabled, "The copy-passwoed-button should be enabled");

  copyPasswordButton.click();
  await SimpleTest.promiseWaitForCondition(() => copyPasswordButton.disabled,
  "waiting for copy-password-button to become disabled after checking for primary password");

  ok(copyPasswordButton.disabled, "The copy-passwoed-button should be disabled when it is clicked");
  ok(copyUsernameButton.disabled, "The copy-username-button should still be disabled after clicking the password button when the username is empty");
});

</script>

</body>
</html>