summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutlogins/tests/chrome/test_login_list.html
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/aboutlogins/tests/chrome/test_login_list.html')
-rw-r--r--browser/components/aboutlogins/tests/chrome/test_login_list.html697
1 files changed, 697 insertions, 0 deletions
diff --git a/browser/components/aboutlogins/tests/chrome/test_login_list.html b/browser/components/aboutlogins/tests/chrome/test_login_list.html
new file mode 100644
index 0000000000..98342978fb
--- /dev/null
+++ b/browser/components/aboutlogins/tests/chrome/test_login_list.html
@@ -0,0 +1,697 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the login-list component
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test the login-list 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-list.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>
+/** Test the login-list component **/
+
+let gLoginList;
+const TEST_LOGIN_1 = {
+ guid: "123456789",
+ origin: "https://abc.example.com",
+ httpRealm: null,
+ username: "user1",
+ password: "pass1",
+ title: "abc.example.com",
+ // new Date("December 13, 2018").getTime()
+ timeLastUsed: 1544677200000,
+ timePasswordChanged: 1544677200000,
+};
+const TEST_LOGIN_2 = {
+ guid: "987654321",
+ origin: "https://example.com",
+ httpRealm: null,
+ username: "user2",
+ password: "pass2",
+ title: "example.com",
+ // new Date("June 1, 2019").getTime()
+ timeLastUsed: 1559361600000,
+ timePasswordChanged: 1559361600000,
+};
+const TEST_LOGIN_3 = {
+ guid: "1111122222",
+ origin: "https://def.example.com",
+ httpRealm: null,
+ username: "",
+ password: "pass3",
+ title: "def.example.com",
+ // new Date("June 1, 2019").getTime()
+ timeLastUsed: 1559361600000,
+ timePasswordChanged: 1559361600000,
+};
+const TEST_HTTP_AUTH_LOGIN_1 = {
+ guid: "8675309",
+ origin: "https://httpauth.example.com",
+ httpRealm: "My Realm",
+ username: "http_auth_user",
+ password: "pass4",
+ title: "httpauth.example.com (My Realm)",
+ // new Date("June 1, 2019").getTime()
+ timeLastUsed: 1559361600000,
+ timePasswordChanged: 1559361600000,
+};
+
+const TEST_BREACH = {
+ AddedDate: "2018-12-20T23:56:26Z",
+ BreachDate: "2018-12-11",
+ Domain: "abc.example.com",
+ Name: "ABC Example",
+ PwnCount: 1643100,
+ DataClasses: ["Usernames", "Passwords"],
+ _status: "synced",
+ id: "047940fe-d2fd-4314-b636-b4a952ee0043",
+ last_modified: "1541615610052",
+ schema: "1541615609018",
+ breachAlertURL: "https://monitor.firefox.com/breach-details/ABC-Example",
+};
+
+
+const TEST_BREACHES_MAP = new Map();
+TEST_BREACHES_MAP.set(TEST_LOGIN_1.guid, TEST_BREACH);
+
+add_setup(async () => {
+ let templateFrame = document.getElementById("templateFrame");
+ let displayEl = document.getElementById("display");
+ await importDependencies(templateFrame, displayEl);
+
+ gLoginList = document.createElement("login-list");
+ displayEl.appendChild(gLoginList);
+});
+
+add_task(async function test_empty_list() {
+ ok(gLoginList, "loginList exists");
+ is(gLoginList.textContent, "", "Initially empty");
+ gLoginList.classList.add("no-logins");
+ let loginListBox = gLoginList.shadowRoot.querySelector("ol");
+ let introText = gLoginList.shadowRoot.querySelector(".intro");
+ let emptySearchText = gLoginList.shadowRoot.querySelector(".empty-search-message");
+ ok(isHidden(loginListBox), "The login-list ol should be hidden when there are no logins");
+ ok(!isHidden(introText), "The intro text should be visible when the list is empty");
+ ok(isHidden(emptySearchText), "The empty-search text should be hidden when there are no logins");
+
+ gLoginList.classList.add("create-login-selected");
+ ok(!isHidden(loginListBox), "The login-list ol should be visible when the create-login mode is active");
+ ok(isHidden(introText), "The intro text should be hidden when the create-login mode is active");
+ ok(isHidden(emptySearchText), "The empty-search text should be hidden when the create-login mode is active");
+ gLoginList.classList.remove("create-login-selected");
+
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "foo",
+ }));
+ ok(isHidden(loginListBox), "The login-list ol should be hidden when there are no logins");
+ ok(!isHidden(introText), "The intro text should be visible when the list is empty");
+ ok(isHidden(emptySearchText), "The empty-search text should be hidden when there are no logins even if a filter is applied");
+
+ // Clean up state for next test
+ gLoginList.classList.remove("no-logins");
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "",
+ }));
+});
+
+add_task(async function test_keyboard_navigation() {
+ let logins = [];
+ for (let i = 0; i < 20; i++) {
+ let suffix = i % 2 ? "odd" : "even";
+ logins.push(Object.assign({}, TEST_LOGIN_1, {
+ guid: "" + i,
+ username: `testuser-${suffix}-${i}`,
+ password: `testpass-${suffix}-${i}`,
+ }));
+ }
+ gLoginList.setLogins(logins);
+ let ol = gLoginList.shadowRoot.querySelector("ol");
+ is(ol.querySelectorAll(".login-list-item[data-guid]").length, 20, "there should be 20 logins in the list");
+ is(ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])").length, 20, "all logins should be visible");
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "odd",
+ }));
+ is(ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])").length, 10, "half of the logins in the list");
+
+ while (document.activeElement != gLoginList &&
+ gLoginList.shadowRoot.querySelector("#login-sort") != gLoginList.shadowRoot.activeElement) {
+ sendKey("TAB");
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ }
+ sendKey("TAB");
+ let loginSort = gLoginList.shadowRoot.querySelector("#login-sort");
+ await SimpleTest.promiseWaitForCondition(() => loginSort == gLoginList.shadowRoot.activeElement,
+ "waiting for login-sort to get focus");
+ ok(loginSort == gLoginList.shadowRoot.activeElement, "#login-sort should be focused after tabbing to it");
+
+ sendKey("TAB");
+ await SimpleTest.promiseWaitForCondition(() => ol.matches(":focus"),
+ "waiting for 'ol' to get focus");
+ ok(ol.matches(":focus"), "'ol' should be focused after tabbing to it");
+
+ let selectedGuid = ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])")[0].dataset.guid;
+ let loginSelectedEvent = null;
+ gLoginList.addEventListener("AboutLoginsLoginSelected", event => loginSelectedEvent = event, {once: true});
+ sendKey("RETURN");
+ is(ol.querySelector(".selected").dataset.guid, selectedGuid, "item should be marked as selected");
+ ok(loginSelectedEvent, "AboutLoginsLoginSelected event should be dispatched on pressing Enter");
+ is(loginSelectedEvent.detail.guid, selectedGuid, "event should have expected login attached");
+
+ for (let [keyFwd, keyRev] of [["LEFT", "RIGHT"], ["DOWN", "UP"]]) {
+ sendKey(keyFwd);
+ await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])")[1].id,
+ `waiting for second item in list to get focused (${keyFwd})`);
+ ok(ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])")[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (${keyFwd})`);
+
+ sendKey(keyRev);
+ await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])")[0].id,
+ `waiting for first item in list to get focused (${keyRev})`);
+ ok(ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])")[0].classList.contains("keyboard-selected"), `first item should be marked as keyboard-selected (${keyRev})`);
+ }
+
+ sendKey("DOWN");
+ await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])")[1].id,
+ `waiting for second item in list to get focused (DOWN)`);
+ ok(ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])")[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (DOWN)`);
+ selectedGuid = ol.querySelectorAll(".login-list-item[data-guid]:not([hidden])")[1].dataset.guid;
+
+ synthesizeKey("VK_DOWN", { repeat: 5 });
+ 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`);
+ synthesizeKey("VK_UP", { repeat: 5 });
+ 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`);
+
+ loginSelectedEvent = null;
+ gLoginList.addEventListener("AboutLoginsLoginSelected", event => loginSelectedEvent = event, {once: true});
+ sendKey("RETURN");
+ is(ol.querySelector(".selected").dataset.guid, selectedGuid, "item should be marked as selected");
+ ok(loginSelectedEvent, "AboutLoginsLoginSelected event should be dispatched on pressing Enter");
+ is(loginSelectedEvent.detail.guid, selectedGuid, "event should have expected login attached");
+
+ // Clean up state for next test
+ gLoginList.classList.remove("no-logins");
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "",
+ }));
+});
+
+add_task(async function test_empty_login_username_in_list() {
+ // Clear the selection so the 'new' login will be in the list too.
+ window.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
+ detail: {},
+ }));
+
+ gLoginList.setLogins([TEST_LOGIN_3]);
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ is(loginListItems.length, 1, "The one stored login should be displayed");
+ is(loginListItems[0].dataset.guid, TEST_LOGIN_3.guid, "login-list-item should have correct guid attribute");
+ let loginUsername = loginListItems[0].querySelector(".username");
+ is(loginUsername.getAttribute("data-l10n-id"), "login-list-item-subtitle-missing-username", "login should show missing username text");
+});
+
+add_task(async function test_populated_list() {
+ gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ is(loginListItems.length, 2, "The two stored logins should be displayed");
+ is(loginListItems[0].getAttribute("role"), "option", "Each login-list-item should have role='option'");
+ is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
+ is(loginListItems[0].querySelector(".title").textContent, TEST_LOGIN_1.title,
+ "login-list-item origin should match");
+ is(loginListItems[0].querySelector(".username").textContent, TEST_LOGIN_1.username,
+ "login-list-item username should match");
+ ok(loginListItems[0].classList.contains("selected"), "The first item should be selected by default");
+ ok(!loginListItems[1].classList.contains("selected"), "The second item should not be selected by default");
+ loginListItems[0].click();
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ is(loginListItems.length, 2, "After selecting one, only the two stored logins should be displayed");
+ ok(loginListItems[0].classList.contains("selected"), "The first item should be selected");
+ ok(!loginListItems[1].classList.contains("selected"), "The second item should still not be selected");
+});
+
+add_task(async function test_breach_indicator() {
+ gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2, Object.assign({}, TEST_LOGIN_3, {password: TEST_LOGIN_1.password})]);
+ gLoginList.setBreaches(TEST_BREACHES_MAP);
+ let vulnerableLogins = new Map();
+ vulnerableLogins.set(TEST_LOGIN_1.guid, true);
+ vulnerableLogins.set(TEST_LOGIN_3.guid, true);
+ gLoginList.setVulnerableLogins(vulnerableLogins);
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ let alertIcon = loginListItems[0].querySelector(".alert-icon");
+ is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "The first login should be TEST_LOGIN_1");
+ ok(!loginListItems[0].classList.contains("vulnerable"), "The first login should not have the .vulnerable class");
+ ok(loginListItems[0].classList.contains("breached"), "The first login should have the .breached class.");
+ is(alertIcon.src, "chrome://browser/content/aboutlogins/icons/breached-website.svg", "The alert icon should be the breach warning icon");
+ is(loginListItems[1].dataset.guid, TEST_LOGIN_3.guid, "The second login should be TEST_LOGIN_3");
+ ok(loginListItems[1].classList.contains("vulnerable"), "The second login should have the .vulnerable class");
+ ok(!loginListItems[1].classList.contains("breached"), "The second login should not have the .breached class");
+ alertIcon = loginListItems[1].querySelector(".alert-icon");
+ is(alertIcon.src, "chrome://browser/content/aboutlogins/icons/vulnerable-password.svg", "The alert icon should be the vulnerable password icon");
+ is(loginListItems[2].dataset.guid, TEST_LOGIN_2.guid, "The third login should be TEST_LOGIN_2");
+ alertIcon = loginListItems[2].querySelector(".alert-icon");
+ ok(!loginListItems[2].classList.contains("vulnerable"), "The third login should not have the .vulnerable class");
+ ok(!loginListItems[2].classList.contains("breached"), "The third login should not have the .breached class");
+ is(alertIcon.src, "chrome://mochitests/content/chrome/browser/components/aboutlogins/tests/chrome/test_login_list.html", "The alert icon src should be empty");
+});
+
+function assertCount({ count, total }) {
+ const countSpan = gLoginList.shadowRoot.querySelector(".count");
+ const actual = JSON.parse(countSpan.getAttribute("data-l10n-args"));
+ isDeeply(actual, { count, total }, "Login count updated");
+}
+
+add_task(async function test_filtered_list() {
+ function findItemFromUsername(list, username) {
+ for (let item of list) {
+ if ((item._cachedUsername || (item._cachedUsername = item.querySelector('.username').textContent)) == username) {
+ return item;
+ }
+ }
+ ok(false, `The ${username} wasn't in the list of logins.`)
+ return list[0];
+ }
+ gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
+ let emptySearchText = gLoginList.shadowRoot.querySelector(".empty-search-message");
+ ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
+ is(gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])").length, 2, "Both logins should be visible");
+
+ assertCount({ count: 2, total: 2 });
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "user1",
+ }));
+ assertCount({ count: 1, total: 2 });
+ ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
+ is(loginListItems[0].querySelector(".username").textContent, "user1", "user1 is expected first");
+ ok(!loginListItems[0].hidden, "user1 should remain visible");
+ ok(loginListItems[1].hidden, "user2 should be hidden");
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "user2",
+ }));
+ assertCount({ count: 1, total: 2 });
+ ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
+ ok(findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be hidden");
+ ok(!findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be visible");
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "user",
+ }));
+ assertCount({ count: 2, total: 2 });
+ ok(!gLoginList._sortSelect.disabled, "The sort should be enabled when there are visible logins in the list");
+ ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
+ ok(!findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be visible");
+ ok(!findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be visible");
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "foo",
+ }));
+ assertCount({ count: 0, total: 2 });
+ ok(gLoginList._sortSelect.disabled, "The sort should be disabled when there are no visible logins in the list");
+ ok(!isHidden(emptySearchText), "The empty search text should be visible when there are no results in the list");
+ isnot(gLoginList.shadowRoot.querySelector(".container > ol").getAttribute("aria-activedescendant"), "new-login-list-item", "new-login-list-item shouldn't be the active descendant");
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
+ ok(findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be hidden");
+ ok(findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be hidden");
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "",
+ }));
+ ok(!gLoginList._sortSelect.disabled, "The sort should be re-enabled when there are visible logins in the list");
+ ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
+ assertCount({ count: 2, total: 2 });
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
+ ok(!findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be visible");
+ ok(!findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be visible");
+
+ info("Add an HTTP Auth login");
+ gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2, TEST_HTTP_AUTH_LOGIN_1]);
+ await asyncElementRendered();
+ assertCount({ count: 3, total: 3 });
+ info("Filter by httpRealm");
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "realm",
+ }));
+ assertCount({ count: 1, total: 3 });
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
+ ok(findItemFromUsername(loginListItems, 'user1').hidden, "user1 should be hidden");
+ ok(findItemFromUsername(loginListItems, 'user2').hidden, "user2 should be hidden");
+ ok(!findItemFromUsername(loginListItems, 'http_auth_user').hidden, "http_auth_user should be visible");
+
+ gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "",
+ }));
+ await asyncElementRendered();
+});
+
+add_task(async function test_initial_empty_results() {
+ // Create a new instance to reset state
+ gLoginList.remove();
+ gLoginList = document.createElement("login-list");
+ document.getElementById("display").appendChild(gLoginList);
+ await asyncElementRendered();
+
+ let emptySearchText = gLoginList.shadowRoot.querySelector(".empty-search-message");
+
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "foo",
+ }));
+ assertCount({ count: 0, total: 0 });
+ ok(gLoginList._sortSelect.disabled, "The sort should be disabled when there are no visible logins in the list");
+ ok(!isHidden(emptySearchText), "The empty search text should be visible when there are no results in the list");
+ isnot(gLoginList.shadowRoot.querySelector(".container > ol").getAttribute("aria-activedescendant"), "new-login-list-item", "new-login-list-item shouldn't be the active descendant");
+ ok(gLoginList.shadowRoot.querySelector("#new-login-list-item").hidden, "new-login-list-item should be @hidden");
+
+ gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "",
+ }));
+ await asyncElementRendered();
+});
+
+add_task(async function test_login_modified() {
+ let modifiedLogin = Object.assign(TEST_LOGIN_1, {username: "user11"});
+ gLoginList.loginModified(modifiedLogin);
+ await asyncElementRendered();
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]:not([hidden])");
+ is(loginListItems.length, 2, "Both logins should be displayed");
+ is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
+ is(loginListItems[0].querySelector(".title").textContent, TEST_LOGIN_1.title,
+ "login-list-item origin should match");
+ is(loginListItems[0].querySelector(".username").textContent, modifiedLogin.username,
+ "login-list-item username should have been updated");
+ is(loginListItems[1].querySelector(".username").textContent, TEST_LOGIN_2.username,
+ "login-list-item2 username should remain unchanged");
+});
+
+add_task(async function test_login_added() {
+ info("selected sort: " + gLoginList.shadowRoot.getElementById("login-sort").selectedIndex);
+
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ is(loginListItems.length, 2, "Should have two logins at start of test");
+ let newLogin = Object.assign({}, TEST_LOGIN_1, {title: "example2.example.com", guid: "111222"});
+ gLoginList.loginAdded(newLogin);
+ await asyncElementRendered();
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ is(loginListItems.length, 3, "New login should be added to the list");
+ is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
+ is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
+ is(loginListItems[2].dataset.guid, newLogin.guid, "login-list-item3 should have correct guid attribute");
+ is(loginListItems[2].querySelector(".title").textContent, newLogin.title,
+ "login-list-item origin should match");
+ is(loginListItems[2].querySelector(".username").textContent, newLogin.username,
+ "login-list-item username should have been updated");
+});
+
+add_task(async function test_login_removed() {
+ gLoginList.loginRemoved({guid: "111222"});
+ await asyncElementRendered();
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ is(loginListItems.length, 2, "New login should be removed from the list");
+ is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
+ is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
+});
+
+add_task(async function test_login_added_filtered() {
+ assertCount({ count: 2, total: 2 });
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ detail: "user1",
+ }));
+ assertCount({ count: 1, total: 2 });
+
+ let newLogin = Object.assign({}, TEST_LOGIN_1, {title: "example2.example.com", username: "user22", guid: "111222"});
+ gLoginList.loginAdded(newLogin);
+ await asyncElementRendered();
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
+ is(loginListItems.length, 3, "New login should be added to the list");
+ is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
+ is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
+ is(loginListItems[2].dataset.guid, newLogin.guid, "login-list-item3 should have correct guid attribute");
+ ok(!loginListItems[0].hidden, "login-list-item1 should be visible");
+ ok(loginListItems[1].hidden, "login-list-item2 should be hidden");
+ ok(loginListItems[2].hidden, "login-list-item3 should be hidden");
+ assertCount({ count: 1, total: 3 });
+});
+
+add_task(async function test_sorted_list() {
+ function dispatchChangeEvent(target) {
+ let event = document.createEvent("UIEvent");
+ event.initEvent("change", true, true);
+ target.dispatchEvent(event);
+ }
+
+ // Clear the filter
+ window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+ detail: "",
+ }));
+
+ // Clear the selection so the 'new' login will be in the list too.
+ window.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
+ detail: {},
+ }));
+
+ // make sure that the logins have distinct orderings based on sort order
+ let [guid1, guid2, guid3] = gLoginList._loginGuidsSortedOrder;
+ gLoginList._logins[guid1].login.timeLastUsed = 0;
+ gLoginList._logins[guid2].login.timeLastUsed = 1;
+ gLoginList._logins[guid3].login.timeLastUsed = 2;
+ gLoginList._logins[guid1].login.title = "a";
+ gLoginList._logins[guid2].login.title = "b";
+ gLoginList._logins[guid3].login.title = "c";
+ gLoginList._logins[guid1].login.username = "a";
+ gLoginList._logins[guid2].login.username = "b";
+ gLoginList._logins[guid3].login.username = "c";
+ gLoginList._logins[guid1].login.timePasswordChanged = 1;
+ gLoginList._logins[guid2].login.timePasswordChanged = 2;
+ gLoginList._logins[guid3].login.timePasswordChanged = 0;
+
+ // sort by last used
+ let loginSort = gLoginList.shadowRoot.getElementById("login-sort");
+ loginSort.value = "last-used";
+ dispatchChangeEvent(loginSort);
+ let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ is(loginListItems.length, 3, "The list should contain the three stored logins");
+ let timeUsed1 = gLoginList._logins[loginListItems[0].dataset.guid].login.timeLastUsed;
+ let timeUsed2 = gLoginList._logins[loginListItems[1].dataset.guid].login.timeLastUsed;
+ let timeUsed3 = gLoginList._logins[loginListItems[2].dataset.guid].login.timeLastUsed;
+ is(timeUsed1 > timeUsed2, true, "Logins sorted by timeLastUsed. First: " + timeUsed1 + "; Second: " + timeUsed2);
+ is(timeUsed2 > timeUsed3, true, "Logins sorted by timeLastUsed. Second: " + timeUsed2 + "; Third: " + timeUsed3);
+
+ // sort by title
+ loginSort.value = "name";
+ dispatchChangeEvent(loginSort);
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ let title1 = gLoginList._logins[loginListItems[0].dataset.guid].login.title;
+ let title2 = gLoginList._logins[loginListItems[1].dataset.guid].login.title;
+ let title3 = gLoginList._logins[loginListItems[2].dataset.guid].login.title;
+ is(title1.localeCompare(title2), -1, "Logins sorted by title. First: " + title1 + "; Second: " + title2);
+ is(title2.localeCompare(title3), -1, "Logins sorted by title. Second: " + title2 + "; Third: " + title3);
+
+ // sort by title in reverse alphabetical order
+ loginSort.value = "name-reverse";
+ dispatchChangeEvent(loginSort);
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ title1 = gLoginList._logins[loginListItems[0].dataset.guid].login.title;
+ title2 = gLoginList._logins[loginListItems[1].dataset.guid].login.title;
+ title3 = gLoginList._logins[loginListItems[2].dataset.guid].login.title;
+ let testDescription = "Logins sorted by title in reverse alphabetical order."
+ is(title1.localeCompare(title2), 1, `${testDescription} First: ${title2}; Second: ${title1}`);
+ is(title2.localeCompare(title3), 1, `${testDescription} Second: ${title3}; Third: ${title2}`);
+
+ // sort by last changed
+ loginSort.value = "last-changed";
+ dispatchChangeEvent(loginSort);
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ let pwChanged1 = gLoginList._logins[loginListItems[0].dataset.guid].login.timePasswordChanged;
+ let pwChanged2 = gLoginList._logins[loginListItems[1].dataset.guid].login.timePasswordChanged;
+ let pwChanged3 = gLoginList._logins[loginListItems[2].dataset.guid].login.timePasswordChanged;
+ is(pwChanged1 > pwChanged2, true, "Logins sorted by timePasswordChanged. First: " + pwChanged1 + "; Second: " + pwChanged2);
+ is(pwChanged2 > pwChanged3, true, "Logins sorted by timePasswordChanged. Second: " + pwChanged2 + "; Third: " + pwChanged3);
+
+ // sort by breached when there are breached logins
+ gLoginList.setBreaches(TEST_BREACHES_MAP);
+ loginSort.value = "alerts";
+ let vulnerableLogins = new Map();
+ gLoginList.setVulnerableLogins(vulnerableLogins);
+ dispatchChangeEvent(loginSort);
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ is(loginListItems[0].classList.contains("breached"), true, "Breached login should be displayed at top of list");
+ is(!loginListItems[1].classList.contains("breached"), true, "Non-breached login should be displayed below breached");
+
+ // sort by username
+ loginSort.value = "username";
+ dispatchChangeEvent(loginSort);
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ let username1 = gLoginList._logins[loginListItems[0].dataset.guid].login.username;
+ let username2 = gLoginList._logins[loginListItems[1].dataset.guid].login.username;
+ let username3 = gLoginList._logins[loginListItems[2].dataset.guid].login.username;
+ is(username1.localeCompare(username2), -1, "Logins sorted by username. First: " + username1 + "; Second: " + username2);
+ is(username2.localeCompare(username3), -1, "Logins sorted by username. Second: " + username2 + "; Third: " + username3);
+
+ // sort by username in reverse alphabetical order
+ loginSort.value = "username-reverse";
+ dispatchChangeEvent(loginSort);
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ username1 = gLoginList._logins[loginListItems[0].dataset.guid].login.username;
+ username2 = gLoginList._logins[loginListItems[1].dataset.guid].login.username;
+ username3 = gLoginList._logins[loginListItems[2].dataset.guid].login.username;
+ testDescription = "Logins sorted by username in reverse alphabetical order.";
+ is(username3.localeCompare(username2), -1, `${testDescription} First: ${username3} Second: ${username2}`);
+ is(username2.localeCompare(username1), -1, `${testDescription} Second: ${username2} Third: ${username1}`);
+
+ // sort by name when there are no breached logins
+ gLoginList.setBreaches(new Map());
+ loginSort.value = "alerts";
+ dispatchChangeEvent(loginSort);
+ loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item, [hidden])");
+ title1 = gLoginList._logins[loginListItems[0].dataset.guid].login.title;
+ title2 = gLoginList._logins[loginListItems[1].dataset.guid].login.title;
+ is(title1.localeCompare(title2), -1, "Logins should be sorted alphabetically by hostname");
+});
+
+add_task(async function test_login_list_item_removed_next_selected() {
+ let logins = [];
+ for (let i = 0; i < 12; i++) {
+ let group = i % 2 ? "BB" : "AA";
+ // Create logins of the form `jared0AAa@example.com`,
+ // `jared1BBb@example.com`, `jared2AAc@example.com`, etc.
+ logins.push({
+ guid: `${i}`,
+ username: `jared${i}${group}${String.fromCharCode(97 + i)}@example.com`,
+ password: "omgsecret!!1",
+ origin: "https://www.example.com",
+ });
+ }
+
+ gLoginList.setLogins(logins);
+ let visibleLogins = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]:not([hidden])");
+ await SimpleTest.promiseWaitForCondition(() => {
+ return visibleLogins.length == 12;
+ }, "Waiting for all logins to be visible");
+ is(gLoginList._selectedGuid, logins[0].guid, "login0 should be selected by default");
+
+ window.dispatchEvent(
+ new CustomEvent("AboutLoginsFilterLogins", {
+ bubbles: true,
+ detail: "BB",
+ })
+ );
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ visibleLogins = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]:not([hidden])");
+ return visibleLogins.length == 6;
+ }, "Only logins with BB in the username should be visible, visible count: " + visibleLogins.length);
+
+ is(gLoginList._selectedGuid, logins[0].guid, "login0 should still be selected after filtering");
+
+ gLoginList.loginRemoved({guid: logins[0].guid});
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ return gLoginList._loginGuidsSortedOrder.length == 11;
+ }, "Waiting for login to get removed");
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ visibleLogins = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]:not([hidden])");
+ return visibleLogins.length == 6;
+ }, "the number of visible logins should not change, got " + visibleLogins.length);
+ is(gLoginList._selectedGuid, logins[1].guid,
+ "login1 should be selected after delete since the deleted login was not visible and login1 was the first in the list");
+
+ let loginToSwitchTo = gLoginList._logins[visibleLogins[1].dataset.guid].login;
+ window.dispatchEvent(
+ new CustomEvent("AboutLoginsLoginSelected", {
+ bubbles: true,
+ detail: loginToSwitchTo,
+ })
+ );
+ is(gLoginList._selectedGuid, loginToSwitchTo.guid, "login3 should be selected");
+
+ gLoginList.loginRemoved({guid: logins[3].guid});
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ return gLoginList._loginGuidsSortedOrder.length == 10;
+ }, "Waiting for login to get removed");
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ visibleLogins = gLoginList.shadowRoot.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ );
+ return visibleLogins.length == 5;
+ }, "the number of filtered logins should decrease by 1");
+ is(gLoginList._selectedGuid, visibleLogins[0].dataset.guid, "the first login should now be selected");
+
+ gLoginList.loginRemoved({guid: logins[1].guid});
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ return gLoginList._loginGuidsSortedOrder.length == 9;
+ }, "Waiting for login to get removed");
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ visibleLogins = gLoginList.shadowRoot.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ );
+ return visibleLogins.length == 4;
+ }, "the number of filtered logins should decrease by 1");
+ is(gLoginList._selectedGuid, visibleLogins[0].dataset.guid, "the first login should now still be selected");
+
+ loginToSwitchTo = gLoginList._logins[visibleLogins[3].dataset.guid].login;
+ window.dispatchEvent(
+ new CustomEvent("AboutLoginsLoginSelected", {
+ bubbles: true,
+ detail: loginToSwitchTo,
+ })
+ );
+ is(gLoginList._selectedGuid, visibleLogins[3].dataset.guid, "the last login should now still be selected");
+
+ gLoginList.loginRemoved({guid: logins[10].guid});
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ return gLoginList._loginGuidsSortedOrder.length == 8;
+ }, "Waiting for login to get removed");
+
+ await SimpleTest.promiseWaitForCondition(() => {
+ visibleLogins = gLoginList.shadowRoot.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ );
+ return visibleLogins.length == 4;
+ }, "the number of filtered logins should decrease by 1");
+ is(gLoginList._selectedGuid, visibleLogins[3].dataset.guid, "the last login should now be selected");
+
+ loginToSwitchTo = gLoginList._logins[visibleLogins[2].dataset.guid].login;
+ window.dispatchEvent(
+ new CustomEvent("AboutLoginsLoginSelected", {
+ bubbles: true,
+ detail: loginToSwitchTo,
+ })
+ );
+ is(gLoginList._selectedGuid, visibleLogins[2].dataset.guid, "the last login should now still be selected");
+});
+</script>
+
+</body>
+</html>