summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutlogins/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/aboutlogins/tests/browser')
-rw-r--r--browser/components/aboutlogins/tests/browser/AboutLoginsTestUtils.sys.mjs107
-rw-r--r--browser/components/aboutlogins/tests/browser/browser.ini58
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js271
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js227
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_breachAlertShowingForAddedLogin.js123
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_confirmDeleteDialog.js128
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js185
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js118
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_createLogin.js535
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_deleteLogin.js182
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_fxAccounts.js96
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_loginFilter.js60
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js153
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_loginListChanges.js144
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_loginSortOrderRestored.js172
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_noLoginsView.js199
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openExport.js149
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openFiltered.js295
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openImport.js60
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openImportCSV.js411
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openPreferences.js82
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openPreferencesExternal.js65
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openSite.js94
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js165
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_primaryPassword.js282
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js555
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_sessionRestore.js62
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js276
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_updateLogin.js421
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_vulnerableLoginAddedInSecondaryWindow.js223
-rw-r--r--browser/components/aboutlogins/tests/browser/head.js225
31 files changed, 6123 insertions, 0 deletions
diff --git a/browser/components/aboutlogins/tests/browser/AboutLoginsTestUtils.sys.mjs b/browser/components/aboutlogins/tests/browser/AboutLoginsTestUtils.sys.mjs
new file mode 100644
index 0000000000..44a51b80ad
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/AboutLoginsTestUtils.sys.mjs
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * An utility class to help out with the about:logins and about:loginsimportreport DOM interaction for the tests.
+ *
+ */
+export class AboutLoginsTestUtils {
+ /**
+ * An utility method to fetch the data from the CSV import success dialog.
+ *
+ * @param {content} content
+ * The content object.
+ * @param {ContentTaskUtils} ContentTaskUtils
+ * The ContentTaskUtils object.
+ * @returns {Promise<Object>} A promise that contains added, modified, noChange and errors count.
+ */
+ static async getCsvImportSuccessDialogData(content, ContentTaskUtils) {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("import-summary-dialog")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => !dialog.hidden,
+ "Waiting for the dialog to be visible"
+ );
+
+ let added = dialog.shadowRoot.querySelector(
+ ".import-items-added .result-count"
+ ).textContent;
+ let modified = dialog.shadowRoot.querySelector(
+ ".import-items-modified .result-count"
+ ).textContent;
+ let noChange = dialog.shadowRoot.querySelector(
+ ".import-items-no-change .result-count"
+ ).textContent;
+ let errors = dialog.shadowRoot.querySelector(
+ ".import-items-errors .result-count"
+ ).textContent;
+ return {
+ added,
+ modified,
+ noChange,
+ errors,
+ l10nFocused: dialog.shadowRoot.activeElement.getAttribute("data-l10n-id"),
+ };
+ }
+
+ /**
+ * An utility method to fetch the data from the CSV import error dialog.
+ *
+ * @param {content} content
+ * The content object.
+ * @returns {Promise<Object>} A promise that contains the hidden state and l10n id for title, description and focused element.
+ */
+ static async getCsvImportErrorDialogData(content) {
+ const dialog = Cu.waiveXrays(
+ content.document.querySelector("import-error-dialog")
+ );
+ const l10nTitle = dialog._genericDialog
+ .querySelector(".error-title")
+ .getAttribute("data-l10n-id");
+ const l10nDescription = dialog._genericDialog
+ .querySelector(".error-description")
+ .getAttribute("data-l10n-id");
+ return {
+ hidden: dialog.hidden,
+ l10nFocused: dialog.shadowRoot.activeElement.getAttribute("data-l10n-id"),
+ l10nTitle,
+ l10nDescription,
+ };
+ }
+
+ /**
+ * An utility method to fetch data from the about:loginsimportreport page.
+ * It also cleans up the tab so you don't have to.
+ *
+ * @param {content} content
+ * The content object.
+ * @returns {Promise<Object>} A promise that contains the detailed report data like added, modified, noChange, errors and rows.
+ */
+ static async getCsvImportReportData(content) {
+ const rows = [];
+ for (let element of content.document.querySelectorAll(".row-details")) {
+ rows.push(element.getAttribute("data-l10n-id"));
+ }
+ const added = content.document.querySelector(
+ ".new-logins .result-count"
+ ).textContent;
+ const modified = content.document.querySelector(
+ ".exiting-logins .result-count"
+ ).textContent;
+ const noChange = content.document.querySelector(
+ ".duplicate-logins .result-count"
+ ).textContent;
+ const errors = content.document.querySelector(
+ ".errors-logins .result-count"
+ ).textContent;
+ return {
+ rows,
+ added,
+ modified,
+ noChange,
+ errors,
+ };
+ }
+}
diff --git a/browser/components/aboutlogins/tests/browser/browser.ini b/browser/components/aboutlogins/tests/browser/browser.ini
new file mode 100644
index 0000000000..06eb8ab92a
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser.ini
@@ -0,0 +1,58 @@
+[DEFAULT]
+support-files =
+ head.js
+prefs =
+ signon.management.page.vulnerable-passwords.enabled=true
+ signon.management.page.os-auth.enabled=true
+ # lower the interval for event telemetry in the content process to update the parent process
+ toolkit.telemetry.ipcBatchTimeout=10
+
+# Run first so content events from previous tests won't trickle in.
+# Skip ASAN and debug since waiting for content events is already slow.
+[browser_aaa_eventTelemetry_run_first.js]
+skip-if =
+ asan || tsan || ccov || debug || (os == "win" && !debug) # bug 1605494 is more prevalent on linux, Bug 1627419
+ os == 'linux' && bits == 64 && !debug # Bug 1648862
+[browser_alertDismissedAfterChangingPassword.js]
+skip-if =
+ os == "mac" && os_version == "10.15" && !debug # Bug 1684513
+[browser_breachAlertShowingForAddedLogin.js]
+[browser_confirmDeleteDialog.js]
+[browser_contextmenuFillLogins.js]
+skip-if = win10_2004 && debug # Bug 1723573
+[browser_copyToClipboardButton.js]
+[browser_createLogin.js]
+[browser_deleteLogin.js]
+[browser_fxAccounts.js]
+[browser_loginFilter.js]
+[browser_loginItemErrors.js]
+skip-if = debug # Bug 1577710
+[browser_loginListChanges.js]
+[browser_loginSortOrderRestored.js]
+skip-if = os == 'linux' && bits == 64 && os_version == '18.04' # Bug 1587625; Bug 1587626 for linux1804
+[browser_noLoginsView.js]
+[browser_openExport.js]
+[browser_openFiltered.js]
+[browser_openImport.js]
+skip-if =
+ os != "win" && os != "mac" # import is only available on Windows and macOS
+ os == "mac" && !debug # bug 1775753
+[browser_openImportCSV.js]
+[browser_openPreferences.js]
+[browser_openPreferencesExternal.js]
+[browser_openSite.js]
+skip-if =
+ os == "linux" && bits == 64 # Bug 1581889
+[browser_osAuthDialog.js]
+skip-if = (os == 'linux') # bug 1527745
+[browser_primaryPassword.js]
+skip-if =
+ (os == 'linux') # bug 1569789
+[browser_removeAllDialog.js]
+[browser_sessionRestore.js]
+skip-if =
+ tsan
+ debug # Bug 1576876
+[browser_tabKeyNav.js]
+[browser_updateLogin.js]
+[browser_vulnerableLoginAddedInSecondaryWindow.js]
diff --git a/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js b/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
new file mode 100644
index 0000000000..0ff77a2240
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
@@ -0,0 +1,271 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(2);
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+EXPECTED_BREACH = {
+ AddedDate: "2018-12-20T23:56:26Z",
+ BreachDate: "2018-12-16",
+ Domain: "breached.example.com",
+ Name: "Breached",
+ PwnCount: 1643100,
+ DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
+ _status: "synced",
+ id: "047940fe-d2fd-4314-b636-b4a952ee0043",
+ last_modified: "1541615610052",
+ schema: "1541615609018",
+};
+
+let VULNERABLE_TEST_LOGIN2 = new nsLoginInfo(
+ "https://2.example.com",
+ "https://2.example.com",
+ null,
+ "user2",
+ "pass3",
+ "username",
+ "password"
+);
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ VULNERABLE_TEST_LOGIN2 = await addLogin(VULNERABLE_TEST_LOGIN2);
+ TEST_LOGIN3 = await addLogin(TEST_LOGIN3);
+
+ await TestUtils.waitForCondition(() => {
+ Services.telemetry.clearEvents();
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ }, "Waiting for telemetry events to get cleared");
+
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_telemetry_events() {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginList = content.document.querySelector("login-list");
+ let loginListItem = loginList.shadowRoot.querySelector(
+ ".login-list-item.breached"
+ );
+ loginListItem.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(2);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let copyButton = loginItem.shadowRoot.querySelector(
+ ".copy-username-button"
+ );
+ copyButton.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(3);
+
+ if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let copyButton = loginItem.shadowRoot.querySelector(
+ ".copy-password-button"
+ );
+ copyButton.click();
+ });
+ await reauthObserved;
+ // When reauth is observed an extra telemetry event will be recorded
+ // for the reauth, hence the event count increasing by 2 here, and later
+ // in the test as well.
+ await LoginTestUtils.telemetry.waitForEventCount(5);
+ }
+ let nextTelemetryEventCount = OSKeyStoreTestUtils.canTestOSKeyStoreLogin()
+ ? 6
+ : 4;
+
+ let promiseNewTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_LOGIN3.origin + "/"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let originInput = loginItem.shadowRoot.querySelector(".origin-input");
+ originInput.click();
+ });
+ let newTab = await promiseNewTab;
+ Assert.ok(true, "New tab opened to " + TEST_LOGIN3.origin);
+ BrowserTestUtils.removeTab(newTab);
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ // Show the password
+ if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ let reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ nextTelemetryEventCount++; // An extra event is observed for the reauth event.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ });
+ await reauthObserved;
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ // Hide the password
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ // Don't force the auth timeout here to check that `auth_skipped: true` is set as
+ // in `extra`.
+ nextTelemetryEventCount++; // An extra event is observed for the reauth event.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let usernameField = loginItem.shadowRoot.querySelector(
+ 'input[name="username"]'
+ );
+ usernameField.value = "user1-modified";
+
+ let saveButton = loginItem.shadowRoot.querySelector(
+ ".save-changes-button"
+ );
+ saveButton.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+ }
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
+ deleteButton.click();
+ let confirmDeleteDialog = content.document.querySelector(
+ "confirmation-dialog"
+ );
+ let confirmDeleteButton =
+ confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+ confirmDeleteButton.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let newLoginButton = content.document
+ .querySelector("login-list")
+ .shadowRoot.querySelector(".create-login-button");
+ newLoginButton.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
+ cancelButton.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginList = content.document.querySelector("login-list");
+ let loginListItem = loginList.shadowRoot.querySelector(
+ ".login-list-item.vulnerable"
+ );
+ loginListItem.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let copyButton = loginItem.shadowRoot.querySelector(
+ ".copy-username-button"
+ );
+ copyButton.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
+ deleteButton.click();
+ let confirmDeleteDialog = content.document.querySelector(
+ "confirmation-dialog"
+ );
+ let confirmDeleteButton =
+ confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+ confirmDeleteButton.click();
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginSort = content.document
+ .querySelector("login-list")
+ .shadowRoot.querySelector("#login-sort");
+ loginSort.value = "last-used";
+ loginSort.dispatchEvent(new content.Event("change", { bubbles: true }));
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.management.page.sort");
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const loginList = content.document.querySelector("login-list");
+ const loginFilter = loginList.shadowRoot.querySelector("login-filter");
+ const input = loginFilter.shadowRoot.querySelector("input");
+ input.setUserInput("test");
+ });
+ await LoginTestUtils.telemetry.waitForEventCount(nextTelemetryEventCount++);
+
+ const testOSAuth = OSKeyStoreTestUtils.canTestOSKeyStoreLogin();
+ let expectedEvents = [
+ [true, "pwmgr", "open_management", "direct"],
+ [true, "pwmgr", "select", "existing_login", null, { breached: "true" }],
+ [true, "pwmgr", "copy", "username", null, { breached: "true" }],
+ [testOSAuth, "pwmgr", "reauthenticate", "os_auth", "success"],
+ [testOSAuth, "pwmgr", "copy", "password", null, { breached: "true" }],
+ [true, "pwmgr", "open_site", "existing_login", null, { breached: "true" }],
+ [testOSAuth, "pwmgr", "reauthenticate", "os_auth", "success"],
+ [testOSAuth, "pwmgr", "show", "password", null, { breached: "true" }],
+ [testOSAuth, "pwmgr", "hide", "password", null, { breached: "true" }],
+ [testOSAuth, "pwmgr", "reauthenticate", "os_auth", "success_no_prompt"],
+ [testOSAuth, "pwmgr", "edit", "existing_login", null, { breached: "true" }],
+ [testOSAuth, "pwmgr", "save", "existing_login", null, { breached: "true" }],
+ [true, "pwmgr", "delete", "existing_login", null, { breached: "true" }],
+ [true, "pwmgr", "new", "new_login"],
+ [true, "pwmgr", "cancel", "new_login"],
+ [true, "pwmgr", "select", "existing_login", null, { vulnerable: "true" }],
+ [true, "pwmgr", "copy", "username", null, { vulnerable: "true" }],
+ [true, "pwmgr", "delete", "existing_login", null, { vulnerable: "true" }],
+ [true, "pwmgr", "sort", "list"],
+ [true, "pwmgr", "filter", "list"],
+ ];
+ expectedEvents = expectedEvents
+ .filter(event => event[0])
+ .map(event => event.slice(1));
+
+ TelemetryTestUtils.assertEvents(
+ expectedEvents,
+ { category: "pwmgr" },
+ { clear: true, process: "content" }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js b/browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js
new file mode 100644
index 0000000000..623df38fcb
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+EXPECTED_BREACH = {
+ AddedDate: "2018-12-20T23:56:26Z",
+ BreachDate: "2018-12-16",
+ Domain: "breached.example.com",
+ Name: "Breached",
+ PwnCount: 1643100,
+ DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
+ _status: "synced",
+ id: "047940fe-d2fd-4314-b636-b4a952ee0043",
+ last_modified: "1541615610052",
+ schema: "1541615609018",
+};
+
+let VULNERABLE_TEST_LOGIN2 = new nsLoginInfo(
+ "https://2.example.com",
+ "https://2.example.com",
+ null,
+ "user2",
+ "pass3",
+ "username",
+ "password"
+);
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ VULNERABLE_TEST_LOGIN2 = await addLogin(VULNERABLE_TEST_LOGIN2);
+ TEST_LOGIN3 = await addLogin(TEST_LOGIN3);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_added_login_shows_breach_warning() {
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(
+ browser,
+ [[TEST_LOGIN1.guid, VULNERABLE_TEST_LOGIN2.guid, TEST_LOGIN3.guid]],
+ async ([regularLoginGuid, vulnerableLoginGuid, breachedLoginGuid]) => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginList.shadowRoot.querySelectorAll(".login-list-item").length,
+ "Waiting for login-list to get populated"
+ );
+ let { listItem: regularListItem } = loginList._logins[regularLoginGuid];
+ let { listItem: vulnerableListItem } =
+ loginList._logins[vulnerableLoginGuid];
+ let { listItem: breachedListItem } = loginList._logins[breachedLoginGuid];
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ !regularListItem.matches(".breached.vulnerable") &&
+ vulnerableListItem.matches(".vulnerable") &&
+ breachedListItem.matches(".breached")
+ );
+ }, `waiting for the list items to get their classes updated: ${regularListItem.className} / ${vulnerableListItem.className} / ${breachedListItem.className}`);
+ Assert.ok(
+ !regularListItem.classList.contains("breached") &&
+ !regularListItem.classList.contains("vulnerable"),
+ "regular login should not be marked breached or vulnerable: " +
+ regularLoginGuid.className
+ );
+ Assert.ok(
+ !vulnerableListItem.classList.contains("breached") &&
+ vulnerableListItem.classList.contains("vulnerable"),
+ "vulnerable login should be marked vulnerable: " +
+ vulnerableListItem.className
+ );
+ Assert.ok(
+ breachedListItem.classList.contains("breached") &&
+ !breachedListItem.classList.contains("vulnerable"),
+ "breached login should be marked breached: " +
+ breachedListItem.className
+ );
+
+ breachedListItem.click();
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return loginItem._login && loginItem._login.guid == breachedLoginGuid;
+ }, "waiting for breached login to get selected");
+ Assert.ok(
+ !ContentTaskUtils.is_hidden(
+ loginItem.shadowRoot.querySelector(".breach-alert")
+ ),
+ "the breach alert should be visible"
+ );
+ }
+ );
+
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ info(
+ "leaving test early since the remaining part of the test requires 'edit' mode which requires 'oskeystore' login"
+ );
+ return;
+ }
+
+ let reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ // Change the password on the breached login and check that the
+ // login is no longer marked as breached. The vulnerable login
+ // should still be marked as vulnerable afterwards.
+ await SpecialPowers.spawn(browser, [], () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ loginItem.shadowRoot.querySelector(".edit-button").click();
+ });
+ await reauthObserved;
+ await SpecialPowers.spawn(
+ browser,
+ [[TEST_LOGIN1.guid, VULNERABLE_TEST_LOGIN2.guid, TEST_LOGIN3.guid]],
+ async ([regularLoginGuid, vulnerableLoginGuid, breachedLoginGuid]) => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing == "true",
+ "waiting for login-item to enter edit mode"
+ );
+
+ // The password display field is in the DOM when password input is unfocused.
+ // To get the password input field, ensure it receives focus.
+ let passwordInput = loginItem.shadowRoot.querySelector(
+ "input[type='password']"
+ );
+ passwordInput.focus();
+ passwordInput = loginItem.shadowRoot.querySelector(
+ "input[name='password']"
+ );
+
+ const CHANGED_PASSWORD_VALUE = "changedPassword";
+ passwordInput.value = CHANGED_PASSWORD_VALUE;
+ let saveChangesButton = loginItem.shadowRoot.querySelector(
+ ".save-changes-button"
+ );
+ saveChangesButton.click();
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList._logins[breachedLoginGuid].login.password ==
+ CHANGED_PASSWORD_VALUE
+ );
+ }, "waiting for stored login to get updated");
+
+ Assert.ok(
+ ContentTaskUtils.is_hidden(
+ loginItem.shadowRoot.querySelector(".breach-alert")
+ ),
+ "the breach alert should be hidden now"
+ );
+
+ let { listItem: breachedListItem } = loginList._logins[breachedLoginGuid];
+ let { listItem: vulnerableListItem } =
+ loginList._logins[vulnerableLoginGuid];
+ Assert.ok(
+ !breachedListItem.classList.contains("breached") &&
+ !breachedListItem.classList.contains("vulnerable"),
+ "the originally breached login should no longer be marked as breached"
+ );
+ Assert.ok(
+ !vulnerableListItem.classList.contains("breached") &&
+ vulnerableListItem.classList.contains("vulnerable"),
+ "vulnerable login should still be marked vulnerable: " +
+ vulnerableListItem.className
+ );
+
+ // Change the password on the vulnerable login and check that the
+ // login is no longer marked as vulnerable.
+ vulnerableListItem.click();
+ await ContentTaskUtils.waitForCondition(() => {
+ return loginItem._login && loginItem._login.guid == vulnerableLoginGuid;
+ }, "waiting for vulnerable login to get selected");
+ Assert.ok(
+ !ContentTaskUtils.is_hidden(
+ loginItem.shadowRoot.querySelector(".vulnerable-alert")
+ ),
+ "the vulnerable alert should be visible"
+ );
+ loginItem.shadowRoot.querySelector(".edit-button").click();
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing == "true",
+ "waiting for login-item to enter edit mode"
+ );
+
+ passwordInput.value = CHANGED_PASSWORD_VALUE;
+ saveChangesButton.click();
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList._logins[vulnerableLoginGuid].login.password ==
+ CHANGED_PASSWORD_VALUE
+ );
+ }, "waiting for stored login to get updated");
+
+ Assert.ok(
+ ContentTaskUtils.is_hidden(
+ loginItem.shadowRoot.querySelector(".vulnerable-alert")
+ ),
+ "the vulnerable alert should be hidden now"
+ );
+ Assert.equal(
+ vulnerableListItem.querySelector(".alert-icon").src,
+ "",
+ ".alert-icon for the vulnerable list item should have its source removed"
+ );
+ vulnerableListItem = loginList._logins[vulnerableLoginGuid].listItem;
+ Assert.ok(
+ !vulnerableListItem.classList.contains("breached") &&
+ !vulnerableListItem.classList.contains("vulnerable"),
+ "vulnerable login should no longer be marked vulnerable: " +
+ vulnerableListItem.className
+ );
+ }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_breachAlertShowingForAddedLogin.js b/browser/components/aboutlogins/tests/browser/browser_breachAlertShowingForAddedLogin.js
new file mode 100644
index 0000000000..a5aef703fa
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_breachAlertShowingForAddedLogin.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+EXPECTED_BREACH = {
+ AddedDate: "2018-12-20T23:56:26Z",
+ BreachDate: "2018-12-16",
+ Domain: "breached.example.com",
+ Name: "Breached",
+ PwnCount: 1643100,
+ DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
+ _status: "synced",
+ id: "047940fe-d2fd-4314-b636-b4a952ee0043",
+ last_modified: "1541615610052",
+ schema: "1541615609018",
+};
+
+add_setup(async function () {
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_added_login_shows_breach_warning() {
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ Assert.equal(
+ loginList._loginGuidsSortedOrder.length,
+ 0,
+ "the login list should be empty"
+ );
+ });
+
+ TEST_LOGIN3 = await addLogin(TEST_LOGIN3);
+ await SpecialPowers.spawn(
+ browser,
+ [TEST_LOGIN3.guid],
+ async aTestLogin3Guid => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginList._loginGuidsSortedOrder.length == 1,
+ "waiting for login list count to equal one. count=" +
+ loginList._loginGuidsSortedOrder.length
+ );
+ Assert.equal(
+ loginList._loginGuidsSortedOrder.length,
+ 1,
+ "one login should be in the list"
+ );
+ let breachedLoginListItems;
+ await ContentTaskUtils.waitForCondition(() => {
+ breachedLoginListItems = loginList._list.querySelectorAll(
+ ".login-list-item[data-guid].breached"
+ );
+ return breachedLoginListItems.length == 1;
+ }, "waiting for the login to get marked as breached");
+ Assert.equal(
+ breachedLoginListItems[0].dataset.guid,
+ aTestLogin3Guid,
+ "the breached login should be login3"
+ );
+ }
+ );
+
+ info("adding a login that uses the same password as the breached login");
+ let vulnerableLogin = new nsLoginInfo(
+ "https://2.example.com",
+ "https://2.example.com",
+ null,
+ "user2",
+ "pass3",
+ "username",
+ "password"
+ );
+ vulnerableLogin = await addLogin(vulnerableLogin);
+ await SpecialPowers.spawn(
+ browser,
+ [[TEST_LOGIN3.guid, vulnerableLogin.guid]],
+ async ([aTestLogin3Guid, aVulnerableLoginGuid]) => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginList._loginGuidsSortedOrder.length == 2,
+ "waiting for login list count to equal two. count=" +
+ loginList._loginGuidsSortedOrder.length
+ );
+ Assert.equal(
+ loginList._loginGuidsSortedOrder.length,
+ 2,
+ "two logins should be in the list"
+ );
+ let breachedAndVulnerableLoginListItems;
+ await ContentTaskUtils.waitForCondition(() => {
+ breachedAndVulnerableLoginListItems = [
+ ...loginList._list.querySelectorAll(".breached, .vulnerable"),
+ ];
+ return breachedAndVulnerableLoginListItems.length == 2;
+ }, "waiting for the logins to get marked as breached and vulnerable");
+ Assert.ok(
+ !!breachedAndVulnerableLoginListItems.find(
+ listItem => listItem.dataset.guid == aTestLogin3Guid
+ ),
+ "the list should include the breached login: " +
+ breachedAndVulnerableLoginListItems.map(li => li.dataset.guid)
+ );
+ Assert.ok(
+ !!breachedAndVulnerableLoginListItems.find(
+ listItem => listItem.dataset.guid == aVulnerableLoginGuid
+ ),
+ "the list should include the vulnerable login: " +
+ breachedAndVulnerableLoginListItems.map(li => li.dataset.guid)
+ );
+ }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_confirmDeleteDialog.js b/browser/components/aboutlogins/tests/browser/browser_confirmDeleteDialog.js
new file mode 100644
index 0000000000..52c288c780
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_confirmDeleteDialog.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function test() {
+ let browser = gBrowser.selectedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+
+ let showPromise = loginItem.showConfirmationDialog("delete");
+
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("confirmation-dialog")
+ );
+ let cancelButton = dialog.shadowRoot.querySelector(".cancel-button");
+ let confirmDeleteButton =
+ dialog.shadowRoot.querySelector(".confirm-button");
+ let dismissButton = dialog.shadowRoot.querySelector(".dismiss-button");
+ let message = dialog.shadowRoot.querySelector(".message");
+ let title = dialog.shadowRoot.querySelector(".title");
+
+ await content.document.l10n.translateElements([
+ title,
+ message,
+ cancelButton,
+ confirmDeleteButton,
+ ]);
+
+ Assert.equal(
+ title.textContent,
+ "Remove this login?",
+ "Title contents should match l10n attribute set on outer element"
+ );
+ Assert.equal(
+ message.textContent,
+ "This action cannot be undone.",
+ "Message contents should match l10n attribute set on outer element"
+ );
+ Assert.equal(
+ cancelButton.textContent,
+ "Cancel",
+ "Cancel button contents should match l10n attribute set on outer element"
+ );
+ Assert.equal(
+ confirmDeleteButton.textContent,
+ "Remove",
+ "Remove button contents should match l10n attribute set on outer element"
+ );
+
+ cancelButton.click();
+ try {
+ await showPromise;
+ Assert.ok(
+ false,
+ "Promise returned by show() should not resolve after clicking cancel button"
+ );
+ } catch (ex) {
+ Assert.ok(
+ true,
+ "Promise returned by show() should reject after clicking cancel button"
+ );
+ }
+ await ContentTaskUtils.waitForCondition(
+ () => dialog.hidden,
+ "Waiting for the dialog to be hidden"
+ );
+ Assert.ok(
+ dialog.hidden,
+ "Dialog should be hidden after clicking cancel button"
+ );
+
+ showPromise = loginItem.showConfirmationDialog("delete");
+ dismissButton.click();
+ try {
+ await showPromise;
+ Assert.ok(
+ false,
+ "Promise returned by show() should not resolve after clicking dismiss button"
+ );
+ } catch (ex) {
+ Assert.ok(
+ true,
+ "Promise returned by show() should reject after clicking dismiss button"
+ );
+ }
+ await ContentTaskUtils.waitForCondition(
+ () => dialog.hidden,
+ "Waiting for the dialog to be hidden"
+ );
+ Assert.ok(
+ dialog.hidden,
+ "Dialog should be hidden after clicking dismiss button"
+ );
+
+ showPromise = loginItem.showConfirmationDialog("delete");
+ confirmDeleteButton.click();
+ try {
+ await showPromise;
+ Assert.ok(
+ true,
+ "Promise returned by show() should resolve after clicking confirm button"
+ );
+ } catch (ex) {
+ Assert.ok(
+ false,
+ "Promise returned by show() should not reject after clicking confirm button"
+ );
+ }
+ await ContentTaskUtils.waitForCondition(
+ () => dialog.hidden,
+ "Waiting for the dialog to be hidden"
+ );
+ Assert.ok(
+ dialog.hidden,
+ "Dialog should be hidden after clicking confirm button"
+ );
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js b/browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js
new file mode 100644
index 0000000000..a10d92baac
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+const gTests = [
+ {
+ name: "test contextmenu on password field in create login view",
+ async setup(browser) {
+ // load up the create login view
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let createButton = loginList._createLoginButton;
+ createButton.click();
+ });
+ },
+ },
+];
+
+if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ gTests[gTests.length] = {
+ name: "test contextmenu on password field in edit login view",
+ async setup(browser) {
+ let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+
+ // load up the edit login view
+ await SpecialPowers.spawn(
+ browser,
+ [LoginHelper.loginToVanillaObject(TEST_LOGIN1)],
+ async login => {
+ let loginList = content.document.querySelector("login-list");
+ let loginListItem = loginList.shadowRoot.querySelector(
+ ".login-list-item[data-guid]:not([hidden])"
+ );
+ info("Clicking on the first login");
+
+ loginListItem.click();
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginItem._login.guid == loginListItem.dataset.guid &&
+ loginItem._login.guid == login.guid
+ );
+ }, "Waiting for login item to get populated");
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+ }
+ );
+ await osAuthDialogShown;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "Waiting for login-item to be in editing state"
+ );
+ });
+ },
+ };
+}
+
+/**
+ * Synthesize mouse clicks to open the password manager context menu popup
+ * for a target input element.
+ *
+ */
+async function openContextMenuForPasswordInput(browser) {
+ const doc = browser.ownerDocument;
+ const CONTEXT_MENU = doc.getElementById("contentAreaContextMenu");
+
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(
+ CONTEXT_MENU,
+ "popupshown"
+ );
+
+ let passwordInputCoords = await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+
+ // The password display field is in the DOM when password input is unfocused.
+ // To get the password input field, ensure it receives focus.
+ let passwordInput = loginItem.shadowRoot.querySelector(
+ "input[type='password']"
+ );
+ passwordInput.focus();
+ passwordInput = loginItem.shadowRoot.querySelector(
+ "input[name='password']"
+ );
+
+ passwordInput.focus();
+ let passwordRect = passwordInput.getBoundingClientRect();
+
+ // listen for the contextmenu event so we can assert on receiving it
+ // and examine the target
+ content.contextmenuPromise = new Promise(resolve => {
+ content.document.body.addEventListener(
+ "contextmenu",
+ event => {
+ info(
+ `Received event on target: ${event.target.nodeName}, type: ${event.target.type}`
+ );
+ content.console.log("got contextmenu event: ", event);
+ resolve(event);
+ },
+ { once: true }
+ );
+ });
+
+ let coords = {
+ x: passwordRect.x + passwordRect.width / 2,
+ y: passwordRect.y + passwordRect.height / 2,
+ };
+ return coords;
+ });
+
+ // add the offsets of the <browser> in the chrome window
+ let browserOffsets = browser.getBoundingClientRect();
+ let offsetX = browserOffsets.x + passwordInputCoords.x;
+ let offsetY = browserOffsets.y + passwordInputCoords.y;
+
+ // Synthesize a right mouse click over the password input element, we have to trigger
+ // both events because formfill code relies on this event happening before the contextmenu
+ // (which it does for real user input) in order to not show the password autocomplete.
+ let eventDetails = { type: "mousedown", button: 2 };
+ await EventUtils.synthesizeMouseAtPoint(offsetX, offsetY, eventDetails);
+
+ // Synthesize a contextmenu event to actually open the context menu.
+ eventDetails = { type: "contextmenu", button: 2 };
+ await EventUtils.synthesizeMouseAtPoint(offsetX, offsetY, eventDetails);
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let event = await content.contextmenuPromise;
+ // XXX the event target here is the login-item element,
+ // not the input[type='password'] in its shadowRoot
+ info("contextmenu event target: " + event.target.nodeName);
+ });
+
+ info("waiting for contextMenuShownPromise");
+ await contextMenuShownPromise;
+ return CONTEXT_MENU;
+}
+
+async function testContextMenuOnInputField(testData) {
+ let browser = gBrowser.selectedBrowser;
+
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await testData.setup(browser);
+
+ info("test setup completed");
+ let contextMenu = await openContextMenuForPasswordInput(browser);
+ let fillItem = contextMenu.querySelector("#fill-login");
+ Assert.ok(fillItem, "fill menu item exists");
+ Assert.ok(
+ fillItem && EventUtils.isHidden(fillItem),
+ "fill menu item is hidden"
+ );
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ info("Calling hidePopup on contextMenu");
+ contextMenu.hidePopup();
+ info("waiting for promiseHidden");
+ await promiseHidden;
+}
+
+for (let testData of gTests) {
+ let tmp = {
+ async [testData.name]() {
+ await testContextMenuOnInputField(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
diff --git a/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js b/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js
new file mode 100644
index 0000000000..f8ca37cc47
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.events.testing.asyncClipboard", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:logins" },
+ async function (browser) {
+ let TEST_LOGIN = {
+ guid: "70a",
+ username: "jared",
+ password: "deraj",
+ origin: "https://www.example.com",
+ };
+
+ await SpecialPowers.spawn(browser, [TEST_LOGIN], async function (login) {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+
+ // The login object needs to be cloned into the content global.
+ loginItem.setLogin(Cu.cloneInto(login, content));
+
+ // Lower the timeout for the test.
+ Object.defineProperty(
+ loginItem.constructor,
+ "COPY_BUTTON_RESET_TIMEOUT",
+ {
+ configurable: true,
+ writable: true,
+ value: 1000,
+ }
+ );
+ });
+
+ let testCases = [[TEST_LOGIN.username, ".copy-username-button"]];
+ if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ testCases[1] = [TEST_LOGIN.password, ".copy-password-button"];
+ }
+
+ for (let testCase of testCases) {
+ let testObj = {
+ expectedValue: testCase[0],
+ copyButtonSelector: testCase[1],
+ };
+ info(
+ "waiting for " + testObj.expectedValue + " to be placed on clipboard"
+ );
+ let reauthObserved = true;
+ if (testObj.copyButtonSelector.includes("password")) {
+ reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
+
+ await SimpleTest.promiseClipboardChange(
+ testObj.expectedValue,
+ async () => {
+ await SpecialPowers.spawn(
+ browser,
+ [testObj],
+ async function (aTestObj) {
+ let loginItem = content.document.querySelector("login-item");
+ let copyButton = loginItem.shadowRoot.querySelector(
+ aTestObj.copyButtonSelector
+ );
+ info("Clicking 'copy' button");
+ copyButton.click();
+ }
+ );
+ }
+ );
+ await reauthObserved;
+ Assert.ok(true, testObj.expectedValue + " is on clipboard now");
+
+ await SpecialPowers.spawn(
+ browser,
+ [testObj],
+ async function (aTestObj) {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let copyButton = loginItem.shadowRoot.querySelector(
+ aTestObj.copyButtonSelector
+ );
+ let otherCopyButton =
+ copyButton == loginItem._copyUsernameButton
+ ? loginItem._copyPasswordButton
+ : loginItem._copyUsernameButton;
+ Assert.ok(
+ !otherCopyButton.dataset.copied,
+ "The other copy button should have the 'copied' state removed"
+ );
+ Assert.ok(
+ copyButton.dataset.copied,
+ "Success message should be shown"
+ );
+ }
+ );
+ }
+
+ // Wait for the 'copied' attribute to get removed from the copyPassword
+ // button, which is the last button that is clicked in the above testcase.
+ // Since another Copy button isn't clicked, the state won't get cleared
+ // instantly. This test covers the built-in timeout of the visual display.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let copyButton = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ )._copyPasswordButton;
+ await ContentTaskUtils.waitForCondition(
+ () => !copyButton.dataset.copied,
+ "'copied' attribute should be removed after a timeout"
+ );
+ });
+ }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_createLogin.js b/browser/components/aboutlogins/tests/browser/browser_createLogin.js
new file mode 100644
index 0000000000..a876320ecf
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_createLogin.js
@@ -0,0 +1,535 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ let aboutLoginsTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(aboutLoginsTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_create_login() {
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ Assert.ok(
+ !loginList._selectedGuid,
+ "should not be a selected guid by default"
+ );
+ Assert.ok(
+ content.document.documentElement.classList.contains("no-logins"),
+ "Should initially be in no logins view"
+ );
+ Assert.ok(
+ loginList.classList.contains("no-logins"),
+ "login-list should initially be in no logins view"
+ );
+ Assert.equal(
+ loginList._loginGuidsSortedOrder.length,
+ 0,
+ "login list should be empty"
+ );
+ });
+
+ let testCases = [
+ ["ftp://ftp.example.com/", "ftp://ftp.example.com"],
+ ["https://example.com/foo", "https://example.com"],
+ ["http://example.com/", "http://example.com"],
+ [
+ "https://testuser1:testpass1@bugzilla.mozilla.org/show_bug.cgi?id=1556934",
+ "https://bugzilla.mozilla.org",
+ ],
+ ["https://www.example.com/bar", "https://www.example.com"],
+ ];
+
+ for (let i = 0; i < testCases.length; i++) {
+ let originTuple = testCases[i];
+ info("Testcase " + i);
+ let storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [[originTuple, i]],
+ async ([aOriginTuple, index]) => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let createButton = loginList._createLoginButton;
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginList._blankLoginListItem),
+ "the blank login list item should be hidden initially"
+ );
+ Assert.ok(
+ !createButton.disabled,
+ "Create button should not be disabled initially"
+ );
+
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let usernameInput = loginItem.shadowRoot.querySelector(
+ "input[name='username']"
+ );
+ usernameInput.placeholder = "dummy placeholder";
+
+ createButton.click();
+
+ Assert.ok(
+ ContentTaskUtils.is_visible(loginList._blankLoginListItem),
+ "the blank login list item should be visible after clicking on the create button"
+ );
+ Assert.ok(
+ createButton.disabled,
+ "Create button should be disabled after being clicked"
+ );
+
+ let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
+ Assert.ok(
+ ContentTaskUtils.is_visible(cancelButton),
+ "cancel button should be visible in create mode with no logins saved"
+ );
+
+ let originInput = loginItem.shadowRoot.querySelector(
+ "input[name='origin']"
+ );
+ let passwordInput = loginItem.shadowRoot.querySelector(
+ "input[name='password']"
+ );
+
+ Assert.equal(
+ content.document.l10n.getAttributes(usernameInput).id,
+ null,
+ "there should be no placeholder id on the username input in edit mode"
+ );
+ Assert.equal(
+ usernameInput.placeholder,
+ "",
+ "there should be no placeholder on the username input in edit mode"
+ );
+ originInput.value = aOriginTuple[0];
+ usernameInput.value = "testuser1";
+ passwordInput.value = "testpass1";
+
+ let saveChangesButton = loginItem.shadowRoot.querySelector(
+ ".save-changes-button"
+ );
+ saveChangesButton.click();
+ }
+ );
+
+ info("waiting for login to get added to storage");
+ await storageChangedPromised;
+ info("login added to storage");
+
+ let canTestOSKeyStoreLogin = OSKeyStoreTestUtils.canTestOSKeyStoreLogin();
+ if (canTestOSKeyStoreLogin) {
+ storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ }
+ await SpecialPowers.spawn(browser, [originTuple], async aOriginTuple => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.documentElement.classList.contains(
+ "no-logins"
+ );
+ }, "waiting for no-logins view to exit");
+ Assert.ok(
+ !content.document.documentElement.classList.contains("no-logins"),
+ "Should no longer be in no logins view"
+ );
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ Assert.ok(
+ !loginList.classList.contains("no-logins"),
+ "login-list should no longer be in no logins view"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginList._blankLoginListItem),
+ "the blank login list item should be hidden after adding new login"
+ );
+ Assert.ok(
+ !loginList._createLoginButton.disabled,
+ "Create button shouldn't be disabled after exiting create login view"
+ );
+
+ let loginGuid = await ContentTaskUtils.waitForCondition(() => {
+ return loginList._loginGuidsSortedOrder.find(
+ guid => loginList._logins[guid].login.origin == aOriginTuple[1]
+ );
+ }, "Waiting for login to be displayed");
+ Assert.ok(loginGuid, "Expected login found in login-list");
+
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ Assert.equal(loginItem._login.guid, loginGuid, "login-item should match");
+
+ let { login, listItem } = loginList._logins[loginGuid];
+ Assert.ok(
+ listItem.classList.contains("selected"),
+ "list item should be selected"
+ );
+ Assert.equal(
+ login.origin,
+ aOriginTuple[1],
+ "Stored login should only include the origin of the URL provided during creation"
+ );
+ Assert.equal(
+ login.username,
+ "testuser1",
+ "Stored login should have username provided during creation"
+ );
+ Assert.equal(
+ login.password,
+ "testpass1",
+ "Stored login should have password provided during creation"
+ );
+
+ let usernameInput = loginItem.shadowRoot.querySelector(
+ "input[name='username']"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => usernameInput.placeholder,
+ "waiting for placeholder to get set"
+ );
+ Assert.ok(
+ usernameInput.placeholder,
+ "there should be a placeholder on the username input when not in edit mode"
+ );
+ });
+
+ if (!canTestOSKeyStoreLogin) {
+ continue;
+ }
+
+ let reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ await SpecialPowers.spawn(browser, [originTuple], async aOriginTuple => {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ info("clicking on edit button");
+ editButton.click();
+ });
+ info("waiting for oskeystore auth");
+ await reauthObserved;
+
+ await SpecialPowers.spawn(browser, [originTuple], async aOriginTuple => {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "waiting for 'edit' mode"
+ );
+ info("in edit mode");
+
+ let usernameInput = loginItem.shadowRoot.querySelector(
+ "input[name='username']"
+ );
+ let passwordInput = loginItem.shadowRoot.querySelector(
+ "input[type='password']"
+ );
+ passwordInput.focus();
+ passwordInput = loginItem.shadowRoot.querySelector(
+ "input[name='password']"
+ );
+ usernameInput.value = "testuser2";
+ passwordInput.value = "testpass2";
+
+ let saveChangesButton = loginItem.shadowRoot.querySelector(
+ ".save-changes-button"
+ );
+ info("clicking save changes button");
+ saveChangesButton.click();
+ });
+
+ info("waiting for login to get modified in storage");
+ await storageChangedPromised;
+ info("login modified in storage");
+
+ await SpecialPowers.spawn(browser, [originTuple], async aOriginTuple => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let login;
+ await ContentTaskUtils.waitForCondition(() => {
+ login = Object.values(loginList._logins).find(
+ obj => obj.login.origin == aOriginTuple[1]
+ ).login;
+ info(`${login.origin} / ${login.username} / ${login.password}`);
+ return (
+ login.origin == aOriginTuple[1] &&
+ login.username == "testuser2" &&
+ login.password == "testpass2"
+ );
+ }, "waiting for the login to get updated");
+ Assert.equal(
+ login.origin,
+ aOriginTuple[1],
+ "Stored login should only include the origin of the URL provided during creation"
+ );
+ Assert.equal(
+ login.username,
+ "testuser2",
+ "Stored login should have modified username"
+ );
+ Assert.equal(
+ login.password,
+ "testpass2",
+ "Stored login should have modified password"
+ );
+ });
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [testCases.length],
+ async testCasesLength => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ Assert.equal(
+ loginList._loginGuidsSortedOrder.length,
+ 5,
+ "login list should have a login per testcase"
+ );
+ }
+ );
+});
+
+add_task(async function test_cancel_create_login() {
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ Assert.ok(
+ loginList._selectedGuid,
+ "there should be a selected guid before create mode"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginList._blankLoginListItem),
+ "the blank login list item should be hidden before create mode"
+ );
+
+ let createButton = content.document
+ .querySelector("login-list")
+ .shadowRoot.querySelector(".create-login-button");
+ createButton.click();
+
+ Assert.ok(
+ !loginList._selectedGuid,
+ "there should be no selected guid when in create mode"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_visible(loginList._blankLoginListItem),
+ "the blank login list item should be visible in create mode"
+ );
+
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
+ cancelButton.click();
+
+ Assert.ok(
+ loginList._selectedGuid,
+ "there should be a selected guid after canceling create mode"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginList._blankLoginListItem),
+ "the blank login list item should be hidden after canceling create mode"
+ );
+ });
+});
+
+add_task(
+ async function test_cancel_create_login_with_filter_showing_one_login() {
+ const browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ const loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+
+ const loginFilter = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector("login-filter")
+ );
+ loginFilter.value = "bugzilla.mozilla.org";
+ Assert.equal(
+ loginList._list.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).length,
+ 1,
+ "filter should have one login showing"
+ );
+ let visibleLoginGuid = loginList.shadowRoot.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ )[0].dataset.guid;
+
+ let createButton = loginList._createLoginButton;
+ createButton.click();
+
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
+ Assert.ok(
+ ContentTaskUtils.is_visible(cancelButton),
+ "cancel button should be visible in create mode with one login showing"
+ );
+ cancelButton.click();
+
+ Assert.equal(
+ loginFilter.value,
+ "bugzilla.mozilla.org",
+ "login-filter should not be cleared if there was a login in the list"
+ );
+ Assert.equal(
+ loginList.shadowRoot.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ )[0].dataset.guid,
+ visibleLoginGuid,
+ "the same login should still be visible"
+ );
+ });
+ }
+);
+
+add_task(async function test_cancel_create_login_with_logins_filtered_out() {
+ const browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ const loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ const loginFilter = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector("login-filter")
+ );
+ loginFilter.value = "XXX-no-logins-should-match-this-XXX";
+ await Promise.resolve();
+ Assert.equal(
+ loginList._list.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).length,
+ 0,
+ "filter should have no logins showing"
+ );
+
+ let createButton = loginList._createLoginButton;
+ createButton.click();
+
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
+ Assert.ok(
+ ContentTaskUtils.is_visible(cancelButton),
+ "cancel button should be visible in create mode with no logins showing"
+ );
+ cancelButton.click();
+ await Promise.resolve();
+
+ Assert.equal(
+ loginFilter.value,
+ "",
+ "login-filter should be cleared if there were no logins in the list"
+ );
+ let visibleLoginItems = loginList.shadowRoot.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ );
+ Assert.equal(
+ visibleLoginItems.length,
+ 5,
+ "all logins should be visible with blank filter"
+ );
+ Assert.equal(
+ loginList._selectedGuid,
+ visibleLoginItems[0].dataset.guid,
+ "the first item in the list should be selected"
+ );
+ });
+});
+
+add_task(async function test_create_duplicate_login() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ return;
+ }
+
+ let browser = gBrowser.selectedBrowser;
+ EXPECTED_ERROR_MESSAGE = "This login already exists.";
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ let createButton = loginList._createLoginButton;
+ createButton.click();
+
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let originInput = loginItem.shadowRoot.querySelector(
+ "input[name='origin']"
+ );
+ let usernameInput = loginItem.shadowRoot.querySelector(
+ "input[name='username']"
+ );
+ let passwordInput = loginItem.shadowRoot.querySelector(
+ "input[name='password']"
+ );
+ const EXISTING_USERNAME = "testuser2";
+ const EXISTING_ORIGIN = "https://example.com";
+ originInput.value = EXISTING_ORIGIN;
+ usernameInput.value = EXISTING_USERNAME;
+ passwordInput.value = "different password value";
+
+ let saveChangesButton = loginItem.shadowRoot.querySelector(
+ ".save-changes-button"
+ );
+ saveChangesButton.click();
+
+ await ContentTaskUtils.waitForCondition(
+ () => !loginItem._errorMessage.hidden,
+ "waiting until the error message is visible"
+ );
+ let duplicatedGuid = Object.values(loginList._logins).find(
+ v =>
+ v.login.origin == EXISTING_ORIGIN &&
+ v.login.username == EXISTING_USERNAME
+ ).login.guid;
+ Assert.equal(
+ loginItem._errorMessageLink.dataset.errorGuid,
+ duplicatedGuid,
+ "Error message has GUID of existing duplicated login set on it"
+ );
+
+ let confirmationDialog = Cu.waiveXrays(
+ content.document.querySelector("confirmation-dialog")
+ );
+ Assert.ok(
+ confirmationDialog.hidden,
+ "the discard-changes dialog should be hidden before clicking the error-message-text"
+ );
+ loginItem._errorMessageLink.querySelector("a").click();
+ Assert.ok(
+ !confirmationDialog.hidden,
+ "the discard-changes dialog should be visible"
+ );
+ let discardChangesButton =
+ confirmationDialog.shadowRoot.querySelector(".confirm-button");
+ discardChangesButton.click();
+
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ Object.keys(loginItem._login).length > 1 &&
+ loginItem._login.guid == duplicatedGuid,
+ "waiting until the existing duplicated login is selected"
+ );
+ Assert.equal(
+ loginList._selectedGuid,
+ duplicatedGuid,
+ "the duplicated login should be selected in the list"
+ );
+ });
+ EXPECTED_ERROR_MESSAGE = null;
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js b/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
new file mode 100644
index 0000000000..8aa4201378
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function test_show_logins() {
+ let browser = gBrowser.selectedBrowser;
+
+ await SpecialPowers.spawn(
+ browser,
+ [[TEST_LOGIN1.guid, TEST_LOGIN2.guid]],
+ async loginGuids => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let loginFound = await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList._loginGuidsSortedOrder.length == 2 &&
+ loginList._loginGuidsSortedOrder.includes(loginGuids[0]) &&
+ loginList._loginGuidsSortedOrder.includes(loginGuids[1])
+ );
+ }, "Waiting for logins to be displayed");
+ Assert.ok(
+ !content.document.documentElement.classList.contains("no-logins"),
+ "Should no longer be in no logins view"
+ );
+ Assert.ok(
+ !loginList.classList.contains("no-logins"),
+ "login-list should no longer be in no logins view"
+ );
+ Assert.ok(loginFound, "Newly added logins should be added to the page");
+ }
+ );
+});
+
+add_task(async function test_login_item() {
+ let browser = gBrowser.selectedBrowser;
+
+ function waitForDelete() {
+ let numLogins = Services.logins.countLogins("", "", "");
+ return TestUtils.waitForCondition(
+ () => Services.logins.countLogins("", "", "") < numLogins,
+ "Error waiting for login deletion"
+ );
+ }
+
+ async function deleteFirstLoginAfterEdit() {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = content.document.querySelector("login-list");
+ let loginListItem = loginList.shadowRoot.querySelector(
+ ".login-list-item[data-guid]:not([hidden])"
+ );
+ info("Clicking on the first login");
+ loginListItem.click();
+
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let loginItemPopulated = await ContentTaskUtils.waitForCondition(() => {
+ return loginItem._login.guid == loginListItem.dataset.guid;
+ }, "Waiting for login item to get populated");
+ Assert.ok(loginItemPopulated, "The login item should get populated");
+ });
+ let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+ });
+ await reauthObserved;
+ return SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let usernameInput = loginItem.shadowRoot.querySelector(
+ "input[name='username']"
+ );
+ let passwordInput = loginItem._passwordInput;
+ usernameInput.value += "-undone";
+ passwordInput.value += "-undone";
+
+ let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
+ deleteButton.click();
+
+ let confirmDeleteDialog = Cu.waiveXrays(
+ content.document.querySelector("confirmation-dialog")
+ );
+ let confirmButton =
+ confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+ confirmButton.click();
+ });
+ }
+
+ function deleteFirstLogin() {
+ return SpecialPowers.spawn(browser, [], async () => {
+ let loginList = content.document.querySelector("login-list");
+ let loginListItem = loginList.shadowRoot.querySelector(
+ ".login-list-item[data-guid]:not([hidden])"
+ );
+ info("Clicking on the first login");
+ loginListItem.click();
+
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let loginItemPopulated = await ContentTaskUtils.waitForCondition(() => {
+ return loginItem._login.guid == loginListItem.dataset.guid;
+ }, "Waiting for login item to get populated");
+ Assert.ok(loginItemPopulated, "The login item should get populated");
+
+ let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
+ deleteButton.click();
+
+ let confirmDeleteDialog = Cu.waiveXrays(
+ content.document.querySelector("confirmation-dialog")
+ );
+ let confirmButton =
+ confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+ confirmButton.click();
+ });
+ }
+
+ let onDeletePromise;
+ if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ // Can only test Edit mode in official builds
+ onDeletePromise = waitForDelete();
+ await deleteFirstLoginAfterEdit();
+ await onDeletePromise;
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = content.document.querySelector("login-list");
+ Assert.ok(
+ !content.document.documentElement.classList.contains("no-logins"),
+ "Should not be in no logins view as there is still one login"
+ );
+ Assert.ok(
+ !loginList.classList.contains("no-logins"),
+ "Should not be in no logins view as there is still one login"
+ );
+
+ let confirmDiscardDialog = Cu.waiveXrays(
+ content.document.querySelector("confirmation-dialog")
+ );
+ Assert.ok(
+ confirmDiscardDialog.hidden,
+ "Discard confirm dialog should not show up after delete an edited login"
+ );
+ });
+ } else {
+ onDeletePromise = waitForDelete();
+ await deleteFirstLogin();
+ await onDeletePromise;
+ }
+
+ onDeletePromise = waitForDelete();
+ await deleteFirstLogin();
+ await onDeletePromise;
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = content.document.querySelector("login-list");
+ Assert.ok(
+ content.document.documentElement.classList.contains("no-logins"),
+ "Should be in no logins view as all logins got deleted"
+ );
+ Assert.ok(
+ loginList.classList.contains("no-logins"),
+ "login-list should be in no logins view as all logins got deleted"
+ );
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_fxAccounts.js b/browser/components/aboutlogins/tests/browser/browser_fxAccounts.js
new file mode 100644
index 0000000000..b66f204c92
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_fxAccounts.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+
+function mockState(state) {
+ UIState.get = () => ({
+ status: state.status,
+ lastSync: new Date(),
+ email: state.email,
+ avatarURL: state.avatarURL,
+ });
+}
+
+add_setup(async function () {
+ let aboutLoginsTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ let getState = UIState.get;
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(aboutLoginsTab);
+ UIState.get = getState;
+ });
+});
+
+add_task(async function test_logged_out() {
+ mockState({ status: UIState.STATUS_NOT_CONFIGURED });
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let fxAccountsButton = content.document.querySelector("fxaccounts-button");
+ Assert.ok(fxAccountsButton, "fxAccountsButton should exist");
+ fxAccountsButton = Cu.waiveXrays(fxAccountsButton);
+ await ContentTaskUtils.waitForCondition(
+ () => fxAccountsButton._loggedIn === false,
+ "waiting for _loggedIn to strictly equal false"
+ );
+ Assert.equal(
+ fxAccountsButton._loggedIn,
+ false,
+ "state should reflect not logged in"
+ );
+ });
+});
+
+add_task(async function test_login_syncing_enabled() {
+ const TEST_EMAIL = "test@example.com";
+ const TEST_AVATAR_URL =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+ mockState({
+ status: UIState.STATUS_SIGNED_IN,
+ email: TEST_EMAIL,
+ avatarURL: TEST_AVATAR_URL,
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["services.sync.engine.passwords", true]],
+ });
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(
+ browser,
+ [[TEST_EMAIL, TEST_AVATAR_URL]],
+ async ([expectedEmail, expectedAvatarURL]) => {
+ let fxAccountsButton =
+ content.document.querySelector("fxaccounts-button");
+ Assert.ok(fxAccountsButton, "fxAccountsButton should exist");
+ fxAccountsButton = Cu.waiveXrays(fxAccountsButton);
+ await ContentTaskUtils.waitForCondition(
+ () => fxAccountsButton._email === expectedEmail,
+ "waiting for _email to strictly equal expectedEmail"
+ );
+ Assert.equal(
+ fxAccountsButton._loggedIn,
+ true,
+ "state should reflect logged in"
+ );
+ Assert.equal(
+ fxAccountsButton._email,
+ expectedEmail,
+ "state should have email set"
+ );
+ Assert.equal(
+ fxAccountsButton._avatarURL,
+ expectedAvatarURL,
+ "state should have avatarURL set"
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_loginFilter.js b/browser/components/aboutlogins/tests/browser/browser_loginFilter.js
new file mode 100644
index 0000000000..765e68713f
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_loginFilter.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ const aboutLoginsTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(aboutLoginsTab);
+ });
+});
+
+add_task(async function focus_filter_by_ctrl_f() {
+ const browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ function getActiveElement() {
+ let element = content.document.activeElement;
+
+ while (element?.shadowRoot) {
+ element = element.shadowRoot.activeElement;
+ }
+
+ return element;
+ }
+
+ //// State after load
+
+ const loginFilter = content.document
+ .querySelector("login-list")
+ .shadowRoot.querySelector("login-filter")
+ .shadowRoot.querySelector("input");
+ Assert.equal(
+ getActiveElement(),
+ loginFilter,
+ "login filter must be focused after opening about:logins"
+ );
+
+ //// Focus something else (Create Login button)
+
+ content.document
+ .querySelector("login-list")
+ .shadowRoot.querySelector(".create-login-button")
+ .focus();
+ Assert.notEqual(
+ getActiveElement(),
+ loginFilter,
+ "login filter is not focused"
+ );
+
+ //// Ctrl+F key
+
+ EventUtils.synthesizeKey("f", { accelKey: true }, content);
+ Assert.equal(
+ getActiveElement(),
+ loginFilter,
+ "Ctrl+F/Cmd+F focused login filter"
+ );
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js b/browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js
new file mode 100644
index 0000000000..952149f0db
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_showLoginItemErrors() {
+ const browser = gBrowser.selectedBrowser;
+ let LOGIN_TO_UPDATE = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "user2",
+ "pass2"
+ );
+ LOGIN_TO_UPDATE = await Services.logins.addLoginAsync(LOGIN_TO_UPDATE);
+ EXPECTED_ERROR_MESSAGE = "This login already exists.";
+ const LOGIN_UPDATES = {
+ origin: "https://example.com",
+ password: "my1GoodPassword",
+ username: "user1",
+ };
+
+ await SpecialPowers.spawn(
+ browser,
+ [[LoginHelper.loginToVanillaObject(LOGIN_TO_UPDATE), LOGIN_UPDATES]],
+ async ([loginToUpdate, loginUpdates]) => {
+ const loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ const loginItemErrorMessage = Cu.waiveXrays(
+ loginItem.shadowRoot.querySelector(".error-message")
+ );
+ const loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+
+ const createButton = loginList._createLoginButton;
+ createButton.click();
+
+ const event = Cu.cloneInto(
+ {
+ bubbles: true,
+ detail: loginUpdates,
+ },
+ content
+ );
+
+ content.dispatchEvent(
+ // adds first login
+ new content.CustomEvent("AboutLoginsCreateLogin", event)
+ );
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList.shadowRoot.querySelectorAll(".login-list-item").length === 3
+ );
+ }, "Waiting for login item to be created.");
+
+ Assert.ok(
+ loginItemErrorMessage.hidden,
+ "An error message should not be displayed after adding a new login."
+ );
+
+ content.dispatchEvent(
+ // adds a duplicate of the first login
+ new content.CustomEvent("AboutLoginsCreateLogin", event)
+ );
+
+ const loginItemErrorMessageVisible =
+ await ContentTaskUtils.waitForCondition(() => {
+ return !loginItemErrorMessage.hidden;
+ }, "Waiting for error message to be shown after attempting to create a duplicate login.");
+ Assert.ok(
+ loginItemErrorMessageVisible,
+ "An error message should be shown after user attempts to add a login that already exists."
+ );
+
+ const loginItemErrorMessageText =
+ loginItemErrorMessage.querySelector("span:not([hidden])");
+ Assert.equal(
+ loginItemErrorMessageText.dataset.l10nId,
+ "about-logins-error-message-duplicate-login-with-link",
+ "The correct error message is displayed."
+ );
+
+ let loginListItem = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector(
+ `.login-list-item[data-guid='${loginToUpdate.guid}']`
+ )
+ );
+ loginListItem.click();
+
+ Assert.ok(
+ loginItemErrorMessage.hidden,
+ "The error message should no longer be visible."
+ );
+ }
+ );
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ // The rest of the test uses Edit mode which causes an OS prompt in official builds.
+ return;
+ }
+ let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(
+ browser,
+ [[LoginHelper.loginToVanillaObject(LOGIN_TO_UPDATE), LOGIN_UPDATES]],
+ async ([loginToUpdate, loginUpdates]) => {
+ const loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ const editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+
+ const updateEvent = Cu.cloneInto(
+ {
+ bubbles: true,
+ detail: Object.assign({ guid: loginToUpdate.guid }, loginUpdates),
+ },
+ content
+ );
+
+ content.dispatchEvent(
+ // attempt to update LOGIN_TO_UPDATE to a username/origin combination that already exists.
+ new content.CustomEvent("AboutLoginsUpdateLogin", updateEvent)
+ );
+
+ const loginItemErrorMessage = Cu.waiveXrays(
+ loginItem.shadowRoot.querySelector(".error-message")
+ );
+ const loginAlreadyExistsErrorShownAfterUpdate =
+ await ContentTaskUtils.waitForCondition(() => {
+ return !loginItemErrorMessage.hidden;
+ }, "Waiting for error message to show after updating login to existing login.");
+ Assert.ok(
+ loginAlreadyExistsErrorShownAfterUpdate,
+ "An error message should be shown after updating a login to a username/origin combination that already exists."
+ );
+ }
+ );
+ info("making sure os auth dialog is shown");
+ await reauthObserved;
+ info("saw os auth dialog");
+ EXPECTED_ERROR_MESSAGE = null;
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_loginListChanges.js b/browser/components/aboutlogins/tests/browser/browser_loginListChanges.js
new file mode 100644
index 0000000000..13df6c1ef6
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_loginListChanges.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function test_login_added() {
+ let login = {
+ guid: "70",
+ username: "jared",
+ password: "deraj",
+ origin: "https://www.example.com",
+ };
+ let browser = gBrowser.selectedBrowser;
+ browser.browsingContext.currentWindowGlobal
+ .getActor("AboutLogins")
+ .sendAsyncMessage("AboutLogins:LoginAdded", login);
+
+ await SpecialPowers.spawn(browser, [login], async addedLogin => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ let loginFound = await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList._loginGuidsSortedOrder.length == 1 &&
+ loginList._loginGuidsSortedOrder[0] == addedLogin.guid
+ );
+ }, "Waiting for login to be added");
+ Assert.ok(loginFound, "Newly added logins should be added to the page");
+ });
+});
+
+add_task(async function test_login_modified() {
+ let login = {
+ guid: "70",
+ username: "jared@example.com",
+ password: "deraj",
+ origin: "https://www.example.com",
+ };
+ let browser = gBrowser.selectedBrowser;
+ browser.browsingContext.currentWindowGlobal
+ .getActor("AboutLogins")
+ .sendAsyncMessage("AboutLogins:LoginModified", login);
+
+ await SpecialPowers.spawn(browser, [login], async modifiedLogin => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ let loginFound = await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList._loginGuidsSortedOrder.length == 1 &&
+ loginList._loginGuidsSortedOrder[0] == modifiedLogin.guid &&
+ loginList._logins[loginList._loginGuidsSortedOrder[0]].login.username ==
+ modifiedLogin.username
+ );
+ }, "Waiting for username to get updated");
+ Assert.ok(loginFound, "The login should get updated on the page");
+ });
+});
+
+add_task(async function test_login_removed() {
+ let login = {
+ guid: "70",
+ username: "jared@example.com",
+ password: "deraj",
+ origin: "https://www.example.com",
+ };
+ let browser = gBrowser.selectedBrowser;
+ browser.browsingContext.currentWindowGlobal
+ .getActor("AboutLogins")
+ .sendAsyncMessage("AboutLogins:LoginRemoved", login);
+
+ await SpecialPowers.spawn(browser, [login], async removedLogin => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ let loginRemoved = await ContentTaskUtils.waitForCondition(() => {
+ return !loginList._loginGuidsSortedOrder.length;
+ }, "Waiting for login to get removed");
+ Assert.ok(loginRemoved, "The login should be removed from the page");
+ });
+});
+
+add_task(async function test_all_logins_removed() {
+ // Setup the test with 2 logins.
+ let logins = [
+ {
+ guid: "70",
+ username: "jared",
+ password: "deraj",
+ origin: "https://www.example.com",
+ },
+ {
+ guid: "71",
+ username: "ntim",
+ password: "verysecurepassword",
+ origin: "https://www.example.com",
+ },
+ ];
+
+ let browser = gBrowser.selectedBrowser;
+ browser.browsingContext.currentWindowGlobal
+ .getActor("AboutLogins")
+ .sendAsyncMessage("AboutLogins:AllLogins", logins);
+
+ await SpecialPowers.spawn(browser, [logins], async addedLogins => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ let loginFound = await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList._loginGuidsSortedOrder.length == 2 &&
+ loginList._loginGuidsSortedOrder[0] == addedLogins[0].guid &&
+ loginList._loginGuidsSortedOrder[1] == addedLogins[1].guid
+ );
+ }, "Waiting for login to be added");
+ Assert.ok(loginFound, "Newly added logins should be added to the page");
+ Assert.ok(
+ !content.document.documentElement.classList.contains("no-logins"),
+ "Should not be in no logins view after adding logins"
+ );
+ Assert.ok(
+ !loginList.classList.contains("no-logins"),
+ "login-list should not be in no logins view after adding logins"
+ );
+ });
+
+ Services.logins.removeAllUserFacingLogins();
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ let loginFound = await ContentTaskUtils.waitForCondition(() => {
+ return !loginList._loginGuidsSortedOrder.length;
+ }, "Waiting for logins to be cleared");
+ Assert.ok(loginFound, "Logins should be cleared");
+ Assert.ok(
+ content.document.documentElement.classList.contains("no-logins"),
+ "Should be in no logins view after clearing"
+ );
+ Assert.ok(
+ loginList.classList.contains("no-logins"),
+ "login-list should be in no logins view after clearing"
+ );
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_loginSortOrderRestored.js b/browser/components/aboutlogins/tests/browser/browser_loginSortOrderRestored.js
new file mode 100644
index 0000000000..fb39dda30c
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_loginSortOrderRestored.js
@@ -0,0 +1,172 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+EXPECTED_BREACH = {
+ AddedDate: "2018-12-20T23:56:26Z",
+ BreachDate: "2018-12-16",
+ Domain: "breached.example.com",
+ Name: "Breached",
+ PwnCount: 1643100,
+ DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
+ _status: "synced",
+ id: "047940fe-d2fd-4314-b636-b4a952ee0043",
+ last_modified: "1541615610052",
+ schema: "1541615609018",
+};
+
+const SORT_PREF_NAME = "signon.management.page.sort";
+
+add_setup(async function () {
+ TEST_LOGIN3.QueryInterface(Ci.nsILoginMetaInfo).timePasswordChanged = 1;
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ info(`TEST_LOGIN1 added with guid=${TEST_LOGIN1.guid}`);
+ TEST_LOGIN3 = await addLogin(TEST_LOGIN3);
+ info(`TEST_LOGIN3 added with guid=${TEST_LOGIN3.guid}`);
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ Services.prefs.clearUserPref(SORT_PREF_NAME);
+ });
+});
+
+add_task(async function test_sort_order_persisted() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logins",
+ },
+ async function (browser) {
+ await ContentTask.spawn(
+ browser,
+ [TEST_LOGIN1.guid, TEST_LOGIN3.guid],
+ async function ([testLogin1Guid, testLogin3Guid]) {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginList._sortSelect.value == "alerts",
+ "Waiting for login-list sort to get changed to 'alerts'. Current value is: " +
+ loginList._sortSelect.value
+ );
+ Assert.equal(
+ loginList._sortSelect.value,
+ "alerts",
+ "selected sort should be 'alerts' since there is a breached login"
+ );
+ Assert.equal(
+ loginList._list.querySelector(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).dataset.guid,
+ testLogin3Guid,
+ "the first login should be TEST_LOGIN3 since they are sorted by alerts"
+ );
+
+ loginList._sortSelect.value = "last-changed";
+ loginList._sortSelect.dispatchEvent(
+ new content.Event("change", { bubbles: true })
+ );
+ Assert.equal(
+ loginList._list.querySelector(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).dataset.guid,
+ testLogin1Guid,
+ "the first login should be TEST_LOGIN1 since it has the most recent timePasswordChanged value"
+ );
+ }
+ );
+ }
+ );
+
+ Assert.equal(
+ Services.prefs.getCharPref(SORT_PREF_NAME),
+ "last-changed",
+ "'last-changed' should be stored in the pref"
+ );
+
+ // Set the pref to the value used in Fx70-76 to confirm our
+ // backwards-compat support that "breached" is changed to "alerts"
+ Services.prefs.setCharPref(SORT_PREF_NAME, "breached");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logins",
+ },
+ async function (browser) {
+ await ContentTask.spawn(
+ browser,
+ TEST_LOGIN3.guid,
+ async function (testLogin3Guid) {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginList._sortSelect.value == "alerts",
+ "Waiting for login-list sort to get changed to 'alerts'. Current value is: " +
+ loginList._sortSelect.value
+ );
+ Assert.equal(
+ loginList._sortSelect.value,
+ "alerts",
+ "selected sort should be restored to 'alerts' since 'breached' was in prefs"
+ );
+ Assert.equal(
+ loginList._list.querySelector(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).dataset.guid,
+ testLogin3Guid,
+ "the first login should be TEST_LOGIN3 since they are sorted by alerts"
+ );
+ }
+ );
+ }
+ );
+
+ let storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "removeLogin"
+ );
+ Services.logins.removeLogin(TEST_LOGIN3);
+ await storageChangedPromised;
+ TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
+
+ Assert.equal(
+ Services.prefs.getCharPref(SORT_PREF_NAME),
+ "breached",
+ "confirm that the stored sort is still 'breached' and as such shouldn't apply when the page loads"
+ );
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logins",
+ },
+ async function (browser) {
+ await ContentTask.spawn(
+ browser,
+ TEST_LOGIN2.guid,
+ async function (testLogin2Guid) {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ loginList._list.querySelector(
+ ".login-list-item[data-guid]:not([hidden])"
+ ),
+ "wait for a visible loging to get populated"
+ );
+ Assert.equal(
+ loginList._sortSelect.value,
+ "name",
+ "selected sort should be name since 'alerts' no longer applies with no breached or vulnerable logins"
+ );
+ Assert.equal(
+ loginList._list.querySelector(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).dataset.guid,
+ testLogin2Guid,
+ "the first login should be TEST_LOGIN2 since it is sorted first by 'name'"
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_noLoginsView.js b/browser/components/aboutlogins/tests/browser/browser_noLoginsView.js
new file mode 100644
index 0000000000..b86304aac1
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_noLoginsView.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function test_no_logins_class() {
+ let { platform } = AppConstants;
+ let wizardPromise;
+
+ // The import link is hidden on Linux, so we don't wait for the migration
+ // wizard to open on that platform.
+ if (AppConstants.platform != "linux") {
+ wizardPromise = BrowserTestUtils.waitForMigrationWizard(window);
+ }
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [platform],
+ async aPlatform => {
+ let loginList = content.document.querySelector("login-list");
+
+ Assert.ok(
+ content.document.documentElement.classList.contains("no-logins"),
+ "root should be in no logins view"
+ );
+ Assert.ok(
+ loginList.classList.contains("no-logins"),
+ "login-list should be in no logins view"
+ );
+
+ let loginIntro = Cu.waiveXrays(
+ content.document.querySelector("login-intro")
+ );
+ let loginItem = content.document.querySelector("login-item");
+ let loginListIntro = loginList.shadowRoot.querySelector(".intro");
+ let loginListList = loginList.shadowRoot.querySelector("ol");
+
+ Assert.ok(
+ !ContentTaskUtils.is_hidden(loginIntro),
+ "login-intro should be shown in no logins view"
+ );
+ Assert.ok(
+ !ContentTaskUtils.is_hidden(loginListIntro),
+ "login-list intro should be shown in no logins view"
+ );
+
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginItem),
+ "login-item should be hidden in no logins view"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginListList),
+ "login-list logins list should be hidden in no logins view"
+ );
+ Assert.equal(
+ content.document.l10n.getAttributes(
+ loginIntro.shadowRoot.querySelector(".heading")
+ ).id,
+ "about-logins-login-intro-heading-logged-out2",
+ "The default message should be the non-logged-in message"
+ );
+ Assert.ok(
+ loginIntro.shadowRoot
+ .querySelector("a.intro-help-link")
+ .href.includes("password-manager-remember-delete-edit-logins"),
+ "Check support href populated"
+ );
+
+ loginIntro.updateState(Cu.cloneInto({ loggedIn: true }, content));
+
+ Assert.equal(
+ content.document.l10n.getAttributes(
+ loginIntro.shadowRoot.querySelector(".heading")
+ ).id,
+ "about-logins-login-intro-heading-logged-in",
+ "When logged in the message should update"
+ );
+
+ let importClass = Services.prefs.getBoolPref(
+ "signon.management.page.fileImport.enabled"
+ )
+ ? ".intro-import-text.file-import"
+ : ".intro-import-text.no-file-import";
+ Assert.equal(
+ ContentTaskUtils.is_hidden(
+ loginIntro.shadowRoot.querySelector(importClass)
+ ),
+ aPlatform == "linux",
+ "the import link should be hidden on Linux builds"
+ );
+ if (aPlatform == "linux") {
+ // End the test now for Linux since the link is hidden.
+ return;
+ }
+ loginIntro.shadowRoot.querySelector(importClass + " > a").click();
+ info("waiting for MigrationWizard to open");
+ }
+ );
+ if (AppConstants.platform == "linux") {
+ // End the test now for Linux since the link is hidden.
+ return;
+ }
+ let wizard = await wizardPromise;
+ Assert.ok(wizard, "Migrator window opened");
+ await BrowserTestUtils.closeMigrationWizard(wizard);
+});
+
+add_task(
+ async function login_selected_when_login_added_and_in_no_logins_view() {
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ let loginList = content.document.querySelector("login-list");
+ let loginItem = content.document.querySelector("login-item");
+ let loginIntro = content.document.querySelector("login-intro");
+ Assert.ok(
+ loginList.classList.contains("empty-search"),
+ "login-list should be showing no logins view from a search with no results"
+ );
+ Assert.ok(
+ loginList.classList.contains("no-logins"),
+ "login-list should be showing no logins view since there are no saved logins"
+ );
+ Assert.ok(
+ !loginList.classList.contains("create-login-selected"),
+ "login-list should not be in create-login-selected mode"
+ );
+ Assert.ok(
+ loginItem.classList.contains("no-logins"),
+ "login-item should be marked as having no-logins"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginItem),
+ "login-item should be hidden"
+ );
+ Assert.ok(
+ !ContentTaskUtils.is_hidden(loginIntro),
+ "login-intro should be visible"
+ );
+ });
+
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [TEST_LOGIN1.guid],
+ async testLogin1Guid => {
+ let loginList = content.document.querySelector("login-list");
+ let loginItem = content.document.querySelector("login-item");
+ let loginIntro = content.document.querySelector("login-intro");
+ await ContentTaskUtils.waitForCondition(() => {
+ return !loginList.classList.contains("no-logins");
+ }, "waiting for login-list to leave the no-logins view");
+ Assert.ok(
+ !loginList.classList.contains("empty-search"),
+ "login-list should not be showing no logins view since one login exists"
+ );
+ Assert.ok(
+ !loginList.classList.contains("no-logins"),
+ "login-list should not be showing no logins view since one login exists"
+ );
+ Assert.ok(
+ !loginList.classList.contains("create-login-selected"),
+ "login-list should not be in create-login-selected mode"
+ );
+ Assert.equal(
+ loginList.shadowRoot.querySelector(
+ ".login-list-item.selected[data-guid]"
+ ).dataset.guid,
+ testLogin1Guid,
+ "the login that was just added should be selected"
+ );
+ Assert.ok(
+ !loginItem.classList.contains("no-logins"),
+ "login-item should not be marked as having no-logins"
+ );
+ Assert.equal(
+ Cu.waiveXrays(loginItem)._login.guid,
+ testLogin1Guid,
+ "the login-item should have the newly added login selected"
+ );
+ Assert.ok(
+ !ContentTaskUtils.is_hidden(loginItem),
+ "login-item should be visible"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginIntro),
+ "login-intro should be hidden"
+ );
+ }
+ );
+ }
+);
diff --git a/browser/components/aboutlogins/tests/browser/browser_openExport.js b/browser/components/aboutlogins/tests/browser/browser_openExport.js
new file mode 100644
index 0000000000..c5df84c447
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openExport.js
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test the export logins file picker appears.
+ */
+
+let { OSKeyStore } = ChromeUtils.importESModule(
+ "resource://gre/modules/OSKeyStore.sys.mjs"
+);
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+let { MockFilePicker } = SpecialPowers;
+
+add_setup(async function () {
+ await TestUtils.waitForCondition(() => {
+ Services.telemetry.clearEvents();
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ }, "Waiting for content telemetry events to get cleared");
+
+ MockFilePicker.init(window);
+ MockFilePicker.useAnyFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ registerCleanupFunction(() => {
+ MockFilePicker.cleanup();
+ });
+});
+
+function waitForFilePicker() {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ MockFilePicker.showCallback = null;
+ Assert.ok(true, "Saw the file picker");
+ resolve();
+ };
+ });
+}
+
+add_task(async function test_open_export() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:logins" },
+ async function (browser) {
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "menu-button",
+ {},
+ browser
+ );
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let menuButton = content.document.querySelector("menu-button");
+ return ContentTaskUtils.waitForCondition(function waitForMenu() {
+ return !menuButton.shadowRoot.querySelector(".menu").hidden;
+ }, "waiting for menu to open");
+ });
+
+ function getExportMenuItem() {
+ let menuButton = window.document.querySelector("menu-button");
+ let exportButton =
+ menuButton.shadowRoot.querySelector(".menuitem-export");
+ return exportButton;
+ }
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getExportMenuItem,
+ {},
+ browser
+ );
+
+ // First event is for opening about:logins
+ await LoginTestUtils.telemetry.waitForEventCount(2);
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "mgmt_menu_item_used", "export"]],
+ { category: "pwmgr", method: "mgmt_menu_item_used" },
+ { process: "content" }
+ );
+
+ info("Clicking confirm button");
+ let osReAuthPromise = null;
+
+ if (
+ OSKeyStore.canReauth() &&
+ !OSKeyStoreTestUtils.canTestOSKeyStoreLogin()
+ ) {
+ todo(
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin(),
+ "Cannot test OS key store login in this build."
+ );
+ return;
+ }
+
+ if (OSKeyStore.canReauth()) {
+ osReAuthPromise = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
+ let filePicker = waitForFilePicker();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ () => {
+ let confirmExportDialog = window.document.querySelector(
+ "confirmation-dialog"
+ );
+ return confirmExportDialog.shadowRoot.querySelector(
+ ".confirm-button"
+ );
+ },
+ {},
+ browser
+ );
+
+ if (osReAuthPromise) {
+ Assert.ok(osReAuthPromise, "Waiting for OS re-auth promise");
+ await osReAuthPromise;
+ }
+
+ info("waiting for Export file picker to get opened");
+ await filePicker;
+ Assert.ok(true, "Export file picker opened");
+
+ info("Waiting for the export to complete");
+ let expectedEvents = [
+ [
+ "pwmgr",
+ "reauthenticate",
+ "os_auth",
+ osReAuthPromise ? "success" : "success_unsupported_platform",
+ ],
+ ["pwmgr", "mgmt_menu_item_used", "export_complete"],
+ ];
+ await LoginTestUtils.telemetry.waitForEventCount(
+ expectedEvents.length,
+ "parent"
+ );
+
+ TelemetryTestUtils.assertEvents(
+ expectedEvents,
+ { category: "pwmgr", method: /(reauthenticate|mgmt_menu_item_used)/ },
+ { process: "parent" }
+ );
+ }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_openFiltered.js b/browser/components/aboutlogins/tests/browser/browser_openFiltered.js
new file mode 100644
index 0000000000..fcd9692065
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openFiltered.js
@@ -0,0 +1,295 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ let storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ TEST_LOGIN1 = await Services.logins.addLoginAsync(TEST_LOGIN1);
+ await storageChangedPromised;
+ storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ TEST_LOGIN2 = await Services.logins.addLoginAsync(TEST_LOGIN2);
+ await storageChangedPromised;
+ let tabOpenedPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url =>
+ url.includes(
+ `about:logins?filter=${encodeURIComponent(TEST_LOGIN1.origin)}`
+ ),
+ true
+ );
+ LoginHelper.openPasswordManager(window, {
+ filterString: TEST_LOGIN1.origin,
+ entryPoint: "preferences",
+ });
+ await tabOpenedPromise;
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_query_parameter_filter() {
+ let browser = gBrowser.selectedBrowser;
+ let vanillaLogins = [
+ LoginHelper.loginToVanillaObject(TEST_LOGIN1),
+ LoginHelper.loginToVanillaObject(TEST_LOGIN2),
+ ];
+ await SpecialPowers.spawn(browser, [vanillaLogins], async logins => {
+ const loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return loginList._loginGuidsSortedOrder.length == 2;
+ }, "Waiting for logins to be cached");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ const selectedLoginItem = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector("li[aria-selected='true']")
+ );
+ return selectedLoginItem.dataset.guid === logins[0].guid;
+ }, "Waiting for TEST_LOGIN1 to be selected for the login-item view");
+
+ const loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+
+ Assert.ok(
+ ContentTaskUtils.is_visible(loginItem),
+ "login-item should be visible when a login is selected"
+ );
+ const loginIntro = content.document.querySelector("login-intro");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginIntro),
+ "login-intro should be hidden when a login is selected"
+ );
+
+ const loginFilter = loginList.shadowRoot.querySelector("login-filter");
+
+ const xRayLoginFilter = Cu.waiveXrays(loginFilter);
+ Assert.equal(
+ xRayLoginFilter.value,
+ logins[0].origin,
+ "The filter should be prepopulated"
+ );
+ Assert.equal(
+ loginList.shadowRoot.activeElement,
+ loginFilter,
+ "login-filter should be focused"
+ );
+ Assert.equal(
+ loginFilter.shadowRoot.activeElement,
+ loginFilter.shadowRoot.querySelector(".filter"),
+ "the actual input inside of login-filter should be focused"
+ );
+
+ let hiddenLoginListItems = loginList.shadowRoot.querySelectorAll(
+ ".login-list-item[hidden]"
+ );
+ let visibleLoginListItems = loginList.shadowRoot.querySelectorAll(
+ ".login-list-item:not([hidden])"
+ );
+ Assert.equal(
+ visibleLoginListItems.length,
+ 1,
+ "The one login should be visible"
+ );
+ Assert.equal(
+ visibleLoginListItems[0].dataset.guid,
+ logins[0].guid,
+ "TEST_LOGIN1 should be visible"
+ );
+ Assert.equal(
+ hiddenLoginListItems.length,
+ 2,
+ "One saved login and one blank login should be hidden"
+ );
+ Assert.equal(
+ hiddenLoginListItems[0].id,
+ "new-login-list-item",
+ "#new-login-list-item should be hidden"
+ );
+ Assert.equal(
+ hiddenLoginListItems[1].dataset.guid,
+ logins[1].guid,
+ "TEST_LOGIN2 should be hidden"
+ );
+ });
+});
+
+add_task(async function test_query_parameter_filter_no_logins_for_site() {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ const HOSTNAME_WITH_NO_LOGINS = "xxx-no-logins-for-site-xxx";
+ let tabOpenedPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url =>
+ url.includes(
+ `about:logins?filter=${encodeURIComponent(HOSTNAME_WITH_NO_LOGINS)}`
+ ),
+ true
+ );
+ LoginHelper.openPasswordManager(window, {
+ filterString: HOSTNAME_WITH_NO_LOGINS,
+ entryPoint: "preferences",
+ });
+ await tabOpenedPromise;
+
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ await ContentTaskUtils.waitForCondition(() => {
+ return loginList._loginGuidsSortedOrder.length == 2;
+ }, "Waiting for logins to be cached");
+ Assert.equal(
+ loginList._loginGuidsSortedOrder.length,
+ 2,
+ "login list should have two logins stored"
+ );
+
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginList._list),
+ "the login list should be hidden when there is a search with no results"
+ );
+ let intro = loginList.shadowRoot.querySelector(".intro");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(intro),
+ "the intro should be hidden when there is a search with no results"
+ );
+ let emptySearchMessage = loginList.shadowRoot.querySelector(
+ ".empty-search-message"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_visible(emptySearchMessage),
+ "the empty search message should be visible when there is a search with no results"
+ );
+
+ let visibleLoginListItems = loginList.shadowRoot.querySelectorAll(
+ ".login-list-item:not([hidden])"
+ );
+ Assert.equal(visibleLoginListItems.length, 0, "No login should be visible");
+
+ Assert.ok(
+ !loginList._createLoginButton.disabled,
+ "create button should be enabled"
+ );
+
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(!loginItem.dataset.isNewLogin, "should not be in create mode");
+ Assert.ok(!loginItem.dataset.editing, "should not be in edit mode");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginItem),
+ "login-item should be hidden when a login is not selected and we're not in create mode"
+ );
+ let loginIntro = content.document.querySelector("login-intro");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginIntro),
+ "login-intro should be hidden when a login is not selected and we're not in create mode"
+ );
+
+ loginList._createLoginButton.click();
+
+ Assert.ok(loginItem.dataset.isNewLogin, "should be in create mode");
+ Assert.ok(loginItem.dataset.editing, "should be in edit mode");
+ Assert.ok(
+ ContentTaskUtils.is_visible(loginItem),
+ "login-item should be visible in create mode"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginIntro),
+ "login-intro should be hidden in create mode"
+ );
+ });
+});
+
+add_task(async function test_query_parameter_filter_no_login_until_backspace() {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ let tabOpenedPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:logins?filter=" + encodeURIComponent(TEST_LOGIN1.origin) + "x",
+ true
+ );
+ LoginHelper.openPasswordManager(window, {
+ filterString: TEST_LOGIN1.origin + "x",
+ entryPoint: "preferences",
+ });
+ await tabOpenedPromise;
+
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ await ContentTaskUtils.waitForCondition(() => {
+ return loginList._loginGuidsSortedOrder.length == 2;
+ }, "Waiting for logins to be cached");
+ Assert.equal(
+ loginList._loginGuidsSortedOrder.length,
+ 2,
+ "login list should have two logins stored"
+ );
+
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginList._list),
+ "the login list should be hidden when there is a search with no results"
+ );
+
+ // Backspace the trailing 'x' to get matching logins
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.sendChar("KEY_Backspace", content);
+
+ let intro = loginList.shadowRoot.querySelector(".intro");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(intro),
+ "the intro should be hidden when there is no selection"
+ );
+ let emptySearchMessage = loginList.shadowRoot.querySelector(
+ ".empty-search-message"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(emptySearchMessage),
+ "the empty search message should be hidden when there is matching logins"
+ );
+
+ let visibleLoginListItems = loginList.shadowRoot.querySelectorAll(
+ ".login-list-item:not([hidden])"
+ );
+ Assert.equal(
+ visibleLoginListItems.length,
+ 1,
+ "One login should be visible after backspacing"
+ );
+
+ Assert.ok(
+ !loginList._createLoginButton.disabled,
+ "create button should be enabled"
+ );
+
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(!loginItem.dataset.isNewLogin, "should not be in create mode");
+ Assert.ok(!loginItem.dataset.editing, "should not be in edit mode");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginItem),
+ "login-item should be hidden when a login is not selected and we're not in create mode"
+ );
+ let loginIntro = content.document.querySelector("login-intro");
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginIntro),
+ "login-intro should be hidden when a login is not selected and we're not in create mode"
+ );
+
+ loginList._createLoginButton.click();
+
+ Assert.ok(loginItem.dataset.isNewLogin, "should be in create mode");
+ Assert.ok(loginItem.dataset.editing, "should be in edit mode");
+ Assert.ok(
+ ContentTaskUtils.is_visible(loginItem),
+ "login-item should be visible in create mode"
+ );
+ Assert.ok(
+ ContentTaskUtils.is_hidden(loginIntro),
+ "login-intro should be hidden in create mode"
+ );
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_openImport.js b/browser/components/aboutlogins/tests/browser/browser_openImport.js
new file mode 100644
index 0000000000..627e0d6e3b
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openImport.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ await TestUtils.waitForCondition(() => {
+ Services.telemetry.clearEvents();
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ }, "Waiting for content telemetry events to get cleared");
+
+ let aboutLoginsTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(aboutLoginsTab);
+ });
+});
+
+add_task(async function test_open_import() {
+ let promiseImportWindow = BrowserTestUtils.waitForMigrationWizard(window);
+
+ let browser = gBrowser.selectedBrowser;
+ await BrowserTestUtils.synthesizeMouseAtCenter("menu-button", {}, browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ return ContentTaskUtils.waitForCondition(() => {
+ let menuButton = Cu.waiveXrays(
+ content.document.querySelector("menu-button")
+ );
+ return !menuButton.shadowRoot.querySelector(".menu").hidden;
+ }, "waiting for menu to open");
+ });
+
+ function getImportItem() {
+ let menuButton = window.document.querySelector("menu-button");
+ return menuButton.shadowRoot.querySelector(".menuitem-import-browser");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(getImportItem, {}, browser);
+
+ info("waiting for Import to get opened");
+ let importWindow = await promiseImportWindow;
+ Assert.ok(true, "Import opened");
+
+ // First event is for opening about:logins
+ await LoginTestUtils.telemetry.waitForEventCount(2);
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "mgmt_menu_item_used", "import_from_browser"]],
+ { category: "pwmgr", method: "mgmt_menu_item_used" },
+ { process: "content" }
+ );
+
+ await BrowserTestUtils.closeMigrationWizard(importWindow);
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_openImportCSV.js b/browser/components/aboutlogins/tests/browser/browser_openImportCSV.js
new file mode 100644
index 0000000000..c4994215d8
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openImportCSV.js
@@ -0,0 +1,411 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { FileTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+let { MockFilePicker } = SpecialPowers;
+
+/**
+ * A helper class to deal with Login CSV import UI.
+ */
+class CsvImportHelper {
+ /**
+ * Waits until the mock file picker is opened and sets the destFilePath as it's selected file.
+ *
+ * @param {nsIFile} destFile
+ * The file being passed to the picker.
+ * @returns {string} A promise that is resolved when the picker selects the file.
+ */
+ static waitForOpenFilePicker(destFile) {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = fp => {
+ info("showCallback");
+ info("fileName: " + destFile.path);
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 1;
+ info("done showCallback");
+ resolve();
+ };
+ });
+ }
+
+ /**
+ * Clicks the 3 dot menu and then "Import from a file..." and then it serves a CSV file.
+ * It also does the needed assertions and telemetry validations.
+ * If you await for it to return, it will have processed the CSV file already.
+ *
+ * @param {browser} browser
+ * The browser object.
+ * @param {string[]} linesInFile
+ * An array of strings to be used to generate the CSV file. Each string is a line.
+ * @returns {Promise} A promise that is resolved when the picker selects the file.
+ */
+ static async clickImportFromCsvMenu(browser, linesInFile) {
+ MockFilePicker.init(window);
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ let csvFile = await LoginTestUtils.file.setupCsvFileWithLines(linesInFile);
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("menu-button", {}, browser);
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let menuButton = content.document.querySelector("menu-button");
+ return ContentTaskUtils.waitForCondition(function waitForMenu() {
+ return !menuButton.shadowRoot.querySelector(".menu").hidden;
+ }, "waiting for menu to open");
+ });
+
+ Services.telemetry.clearEvents();
+
+ function getImportMenuItem() {
+ let menuButton = window.document.querySelector("menu-button");
+ let importButton = menuButton.shadowRoot.querySelector(
+ ".menuitem-import-file"
+ );
+ // Force the menu item to be visible for the test.
+ importButton.hidden = false;
+ return importButton;
+ }
+
+ BrowserTestUtils.synthesizeMouseAtCenter(getImportMenuItem, {}, browser);
+
+ async function waitForFilePicker() {
+ let filePickerPromise = CsvImportHelper.waitForOpenFilePicker(csvFile);
+ // First event is for opening about:logins
+ await LoginTestUtils.telemetry.waitForEventCount(
+ 1,
+ "content",
+ "pwmgr",
+ "mgmt_menu_item_used"
+ );
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "mgmt_menu_item_used", "import_from_csv"]],
+ { category: "pwmgr", method: "mgmt_menu_item_used" },
+ { process: "content", clear: false }
+ );
+
+ info("waiting for Import file picker to get opened");
+ await filePickerPromise;
+ Assert.ok(true, "Import file picker opened");
+ }
+
+ await waitForFilePicker();
+ }
+
+ /**
+ * An utility method to fetch the data from the CSV import success dialog.
+ *
+ * @param {browser} browser
+ * The browser object.
+ * @returns {Promise<Object>} A promise that contains added, modified, noChange and errors count.
+ */
+ static async getCsvImportSuccessDialogData(browser) {
+ return SpecialPowers.spawn(browser, [], async () => {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("import-summary-dialog")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => !dialog.hidden,
+ "Waiting for the dialog to be visible"
+ );
+
+ let added = dialog.shadowRoot.querySelector(
+ ".import-items-added .result-count"
+ ).textContent;
+ let modified = dialog.shadowRoot.querySelector(
+ ".import-items-modified .result-count"
+ ).textContent;
+ let noChange = dialog.shadowRoot.querySelector(
+ ".import-items-no-change .result-count"
+ ).textContent;
+ let errors = dialog.shadowRoot.querySelector(
+ ".import-items-errors .result-count"
+ ).textContent;
+ const dialogData = {
+ added,
+ modified,
+ noChange,
+ errors,
+ };
+ if (dialog.shadowRoot.activeElement) {
+ dialogData.l10nFocused =
+ dialog.shadowRoot.activeElement.getAttribute("data-l10n-id");
+ }
+ return dialogData;
+ });
+ }
+
+ /**
+ * An utility method to fetch the data from the CSV import error dialog.
+ *
+ * @param {browser} browser
+ * The browser object.
+ * @returns {Promise<Object>} A promise that contains the hidden state and l10n id for title, description and focused element.
+ */
+ static async getCsvImportErrorDialogData(browser) {
+ return SpecialPowers.spawn(browser, [], async () => {
+ const dialog = Cu.waiveXrays(
+ content.document.querySelector("import-error-dialog")
+ );
+ const l10nTitle = dialog._genericDialog
+ .querySelector(".error-title")
+ .getAttribute("data-l10n-id");
+ const l10nDescription = dialog._genericDialog
+ .querySelector(".error-description")
+ .getAttribute("data-l10n-id");
+ return {
+ hidden: dialog.hidden,
+ l10nFocused:
+ dialog.shadowRoot.activeElement.getAttribute("data-l10n-id"),
+ l10nTitle,
+ l10nDescription,
+ };
+ });
+ }
+
+ /**
+ * An utility method to wait until CSV import is complete.
+ *
+ * @returns {Promise} A promise that gets resolved when the import is complete.
+ */
+ static async waitForImportToComplete() {
+ info("Waiting for the import to complete");
+ await LoginTestUtils.telemetry.waitForEventCount(1, "parent");
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "mgmt_menu_item_used", "import_csv_complete"]],
+ { category: "pwmgr", method: "mgmt_menu_item_used" },
+ { process: "parent" }
+ );
+ }
+
+ /**
+ * An utility method open the about:loginsimportreport page.
+ *
+ * @param {browser} browser
+ * The browser object.
+ * @returns {Promise<Object>} A promise that contains the about:loginsimportreport tab.
+ */
+ static async clickDetailedReport(browser) {
+ let loadedReportTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:loginsimportreport",
+ true
+ );
+ await SpecialPowers.spawn(browser, [], async () => {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("import-summary-dialog")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => !dialog.hidden,
+ "Waiting for the dialog to be visible"
+ );
+ let detailedReportLink = dialog.shadowRoot.querySelector(
+ ".open-detailed-report"
+ );
+
+ detailedReportLink.click();
+ });
+ return loadedReportTab;
+ }
+
+ /**
+ * An utility method to fetch data from the about:loginsimportreport page.
+ *
+ * @param {browser} browser
+ * The browser object.
+ * @returns {Promise<Object>} A promise that contains the detailed report data like added, modified, noChange, errors and rows.
+ */
+ static async getDetailedReportData(browser) {
+ const data = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ function getCount(selector) {
+ const attribute = content.document
+ .querySelector(selector)
+ .getAttribute("data-l10n-args");
+ return JSON.parse(attribute).count;
+ }
+ const rows = [];
+ for (let element of content.document.querySelectorAll(".row-details")) {
+ rows.push(element.getAttribute("data-l10n-id"));
+ }
+ const added = getCount(".new-logins");
+ const modified = getCount(".exiting-logins");
+ const noChange = getCount(".duplicate-logins");
+ const errors = getCount(".errors-logins");
+ return {
+ rows,
+ added,
+ modified,
+ noChange,
+ errors,
+ };
+ }
+ );
+ return data;
+ }
+}
+
+const random = Math.round(Math.random() * 100000001);
+
+add_setup(async function () {
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_open_import_one_item_from_csv() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:logins" },
+ async browser => {
+ await CsvImportHelper.clickImportFromCsvMenu(browser, [
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ `https://example.com,joe${random}@example.com,qwerty,My realm,,{${random}-e194-4279-ae1b-d7d281bb46f0},1589617814635,1589710449871,1589617846802`,
+ ]);
+ await CsvImportHelper.waitForImportToComplete();
+
+ let summary = await CsvImportHelper.getCsvImportSuccessDialogData(
+ browser
+ );
+ Assert.equal(summary.added, "1", "It should have one item as added");
+ Assert.equal(
+ summary.l10nFocused,
+ "about-logins-import-dialog-done",
+ "dismiss button should be focused"
+ );
+ }
+ );
+});
+
+add_task(async function test_open_import_all_four_categories() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:logins" },
+ async browser => {
+ const initialCsvData = [
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ `https://example1.com,existing${random},existing,,,{${random}-07a1-4bcf-86f0-7d56b9c1f48f},1582229924361,1582495972623,1582229924000`,
+ `https://example1.com,duplicate,duplicate,,,{dddd0080-07a1-4bcf-86f0-7d56b9c1f48f},1582229924361,1582495972623,1582229924363`,
+ ];
+ const updatedCsvData = [
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ `https://example1.com,added${random},added,,,,,,`,
+ `https://example1.com,existing${random},modified,,,{${random}-07a1-4bcf-86f0-7d56b9c1f48f},1582229924361,1582495972623,1582229924363`,
+ `https://example1.com,duplicate,duplicate,,,{dddd0080-07a1-4bcf-86f0-7d56b9c1f48f},1582229924361,1582495972623,1582229924363`,
+ `https://example1.com,error,,,,,,,`,
+ ];
+
+ await CsvImportHelper.clickImportFromCsvMenu(browser, initialCsvData);
+ await CsvImportHelper.waitForImportToComplete();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "dismiss-button",
+ {},
+ browser
+ );
+ await CsvImportHelper.clickImportFromCsvMenu(browser, updatedCsvData);
+ await CsvImportHelper.waitForImportToComplete();
+
+ let summary = await CsvImportHelper.getCsvImportSuccessDialogData(
+ browser
+ );
+ Assert.equal(summary.added, "1", "It should have one item as added");
+ Assert.equal(
+ summary.modified,
+ "1",
+ "It should have one item as modified"
+ );
+ Assert.equal(
+ summary.noChange,
+ "1",
+ "It should have one item as unchanged"
+ );
+ Assert.equal(summary.errors, "1", "It should have one item as error");
+ }
+ );
+});
+
+add_task(async function test_open_import_all_four_detailed_report() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:logins" },
+ async browser => {
+ const initialCsvData = [
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ `https://example2.com,existing${random},existing,,,{${random}-07a1-4bcf-86f0-7d56b9c1f48f},1582229924361,1582495972623,1582229924000`,
+ "https://example2.com,duplicate,duplicate,,,{dddd0080-07a1-4bcf-86f0-7d56b9c1f48f},1582229924361,1582495972623,1582229924363",
+ ];
+ const updatedCsvData = [
+ "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
+ `https://example2.com,added${random},added,,,,,,`,
+ `https://example2.com,existing${random},modified,,,{${random}-07a1-4bcf-86f0-7d56b9c1f48f},1582229924361,1582495972623,1582229924363`,
+ "https://example2.com,duplicate,duplicate,,,{dddd0080-07a1-4bcf-86f0-7d56b9c1f48f},1582229924361,1582495972623,1582229924363",
+ "https://example2.com,error,,,,,,,",
+ ];
+
+ await CsvImportHelper.clickImportFromCsvMenu(browser, initialCsvData);
+ await CsvImportHelper.waitForImportToComplete();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "dismiss-button",
+ {},
+ browser
+ );
+ await CsvImportHelper.clickImportFromCsvMenu(browser, updatedCsvData);
+ await CsvImportHelper.waitForImportToComplete();
+ const reportTab = await CsvImportHelper.clickDetailedReport(browser);
+ const report = await CsvImportHelper.getDetailedReportData(browser);
+ BrowserTestUtils.removeTab(reportTab);
+ const { added, modified, noChange, errors, rows } = report;
+ Assert.equal(added, 1, "It should have one item as added");
+ Assert.equal(modified, 1, "It should have one item as modified");
+ Assert.equal(noChange, 1, "It should have one item as unchanged");
+ Assert.equal(errors, 1, "It should have one item as error");
+ Assert.deepEqual(
+ [
+ "about-logins-import-report-row-description-added",
+ "about-logins-import-report-row-description-modified",
+ "about-logins-import-report-row-description-no-change",
+ "about-logins-import-report-row-description-error-missing-field",
+ ],
+ rows,
+ "It should have expected rows in order"
+ );
+ }
+ );
+});
+
+add_task(async function test_open_import_from_csv_with_invalid_file() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:logins" },
+ async browser => {
+ await CsvImportHelper.clickImportFromCsvMenu(browser, [
+ "invalid csv file",
+ ]);
+
+ info("Waiting for the import error dialog");
+ const errorDialog = await CsvImportHelper.getCsvImportErrorDialogData(
+ browser
+ );
+ Assert.equal(errorDialog.hidden, false, "Dialog should not be hidden");
+ Assert.equal(
+ errorDialog.l10nTitle,
+ "about-logins-import-dialog-error-file-format-title",
+ "Dialog error title should be correct"
+ );
+ Assert.equal(
+ errorDialog.l10nDescription,
+ "about-logins-import-dialog-error-file-format-description",
+ "Dialog error description should be correct"
+ );
+ Assert.equal(
+ errorDialog.l10nFocused,
+ "about-logins-import-dialog-error-learn-more",
+ "Learn more link should be focused."
+ );
+ }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_openPreferences.js b/browser/components/aboutlogins/tests/browser/browser_openPreferences.js
new file mode 100644
index 0000000000..57ca74ba87
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openPreferences.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ await TestUtils.waitForCondition(() => {
+ Services.telemetry.clearEvents();
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ }, "Waiting for content telemetry events to get cleared");
+
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function test_open_preferences() {
+ // We want to make sure we visit about:preferences#privacy-logins , as that is
+ // what causes us to scroll to and highlight the "logins" section. However,
+ // about:preferences will redirect the URL, so the eventual load event will happen
+ // on about:preferences#privacy . The `wantLoad` parameter we pass to
+ // `waitForNewTab` needs to take this into account:
+ let seenFirstURL = false;
+ let promiseNewTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => {
+ if (url == "about:preferences#privacy-logins") {
+ seenFirstURL = true;
+ return true;
+ } else if (url == "about:preferences#privacy") {
+ Assert.ok(
+ seenFirstURL,
+ "Must have seen an onLocationChange notification for the privacy-logins hash"
+ );
+ return true;
+ }
+ return false;
+ },
+ true
+ );
+
+ let browser = gBrowser.selectedBrowser;
+ await BrowserTestUtils.synthesizeMouseAtCenter("menu-button", {}, browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ return ContentTaskUtils.waitForCondition(() => {
+ let menuButton = Cu.waiveXrays(
+ content.document.querySelector("menu-button")
+ );
+ return !menuButton.shadowRoot.querySelector(".menu").hidden;
+ }, "waiting for menu to open");
+ });
+
+ function getPrefsItem() {
+ let menuButton = window.document.querySelector("menu-button");
+ return menuButton.shadowRoot.querySelector(".menuitem-preferences");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(getPrefsItem, {}, browser);
+
+ info("waiting for new tab to get opened");
+ let newTab = await promiseNewTab;
+ Assert.ok(true, "New tab opened to about:preferences");
+
+ BrowserTestUtils.removeTab(newTab);
+
+ // First event is for opening about:logins
+ await LoginTestUtils.telemetry.waitForEventCount(2);
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "mgmt_menu_item_used", "preferences"]],
+ { category: "pwmgr", method: "mgmt_menu_item_used" },
+ { process: "content" }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_openPreferencesExternal.js b/browser/components/aboutlogins/tests/browser/browser_openPreferencesExternal.js
new file mode 100644
index 0000000000..e4290371fb
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openPreferencesExternal.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function test_open_feedback() {
+ const menuArray = [
+ {
+ urlFinal:
+ "https://example.com/password-manager-remember-delete-edit-logins",
+ urlBase: "https://example.com/",
+ pref: "app.support.baseURL",
+ selector: ".menuitem-help",
+ },
+ ];
+
+ for (const { urlFinal, urlBase, pref, selector } of menuArray) {
+ info("Test on " + urlFinal);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [[pref, urlBase]],
+ });
+
+ let promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser, urlFinal);
+
+ let browser = gBrowser.selectedBrowser;
+ await BrowserTestUtils.synthesizeMouseAtCenter("menu-button", {}, browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ return ContentTaskUtils.waitForCondition(() => {
+ let menuButton = content.document.querySelector("menu-button");
+ return !menuButton.shadowRoot.querySelector(".menu").hidden;
+ }, "waiting for menu to open");
+ });
+
+ // Not using synthesizeMouseAtCenter here because the element we want clicked on
+ // is in the shadow DOM and therefore requires using a function 1st argument
+ // to BrowserTestUtils.synthesizeMouseAtCenter but we need to pass an
+ // arbitrary selector. See bug 1557489 for more info. As a workaround, this
+ // manually calculates the position to click.
+ let { x, y } = await SpecialPowers.spawn(
+ browser,
+ [selector],
+ async menuItemSelector => {
+ let menuButton = content.document.querySelector("menu-button");
+ let prefsItem = menuButton.shadowRoot.querySelector(menuItemSelector);
+ return prefsItem.getBoundingClientRect();
+ }
+ );
+ await BrowserTestUtils.synthesizeMouseAtPoint(x + 5, y + 5, {}, browser);
+
+ info("waiting for new tab to get opened");
+ let newTab = await promiseNewTab;
+ Assert.ok(true, "New tab opened to" + urlFinal);
+
+ BrowserTestUtils.removeTab(newTab);
+ }
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_openSite.js b/browser/components/aboutlogins/tests/browser/browser_openSite.js
new file mode 100644
index 0000000000..f33d57a8e4
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_openSite.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_launch_login_item() {
+ let promiseNewTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_LOGIN1.origin + "/"
+ );
+
+ let browser = gBrowser.selectedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let originInput = loginItem.shadowRoot.querySelector("a[name='origin']");
+ let EventUtils = ContentTaskUtils.getEventUtils(content);
+ // Use synthesizeMouseAtCenter to generate an event that more closely resembles the
+ // properties of the event object that will be seen when the user clicks the element
+ // (.click() sets originalTarget while synthesizeMouse has originalTarget as a Restricted object).
+ await EventUtils.synthesizeMouseAtCenter(originInput, {}, content);
+ });
+
+ info("waiting for new tab to get opened");
+ let newTab = await promiseNewTab;
+ Assert.ok(true, "New tab opened to " + TEST_LOGIN1.origin);
+ BrowserTestUtils.removeTab(newTab);
+
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ return;
+ }
+
+ promiseNewTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_LOGIN1.origin + "/"
+ );
+ let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ loginItem._editButton.click();
+ });
+ await reauthObserved;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ loginItem._usernameInput.value += "-changed";
+
+ Assert.ok(
+ content.document.querySelector("confirmation-dialog").hidden,
+ "discard-changes confirmation-dialog should be hidden before opening the site"
+ );
+
+ let originInput = loginItem.shadowRoot.querySelector("a[name='origin']");
+ let EventUtils = ContentTaskUtils.getEventUtils(content);
+ // Use synthesizeMouseAtCenter to generate an event that more closely resembles the
+ // properties of the event object that will be seen when the user clicks the element
+ // (.click() sets originalTarget while synthesizeMouse has originalTarget as a Restricted object).
+ await EventUtils.synthesizeMouseAtCenter(originInput, {}, content);
+ });
+
+ info("waiting for new tab to get opened");
+ newTab = await promiseNewTab;
+ Assert.ok(true, "New tab opened to " + TEST_LOGIN1.origin);
+
+ let modifiedLogin = TEST_LOGIN1.clone();
+ modifiedLogin.timeLastUsed = 9000;
+ let storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ Services.logins.modifyLogin(TEST_LOGIN1, modifiedLogin);
+ await storageChangedPromised;
+
+ BrowserTestUtils.removeTab(newTab);
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return !content.document.querySelector("confirmation-dialog").hidden;
+ }, "waiting for confirmation-dialog to appear");
+ Assert.ok(
+ !content.document.querySelector("confirmation-dialog").hidden,
+ "discard-changes confirmation-dialog should be visible after logging in to a site with a modified login present in the form"
+ );
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js b/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
new file mode 100644
index 0000000000..ca054e449a
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test() {
+ info(
+ `updatechannel: ${UpdateUtils.getUpdateChannel(false)}; platform: ${
+ AppConstants.platform
+ }`
+ );
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ Assert.ok(
+ true,
+ `skipping test since oskeystore cannot be automated in this environment`
+ );
+ return;
+ }
+
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+
+ registerCleanupFunction(function () {
+ Services.logins.removeAllUserFacingLogins();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+
+ // Show OS auth dialog when Reveal Password checkbox is checked if not on a new login
+ let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ });
+ await osAuthDialogShown;
+ info("OS auth dialog shown and canceled");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ Assert.ok(
+ !revealCheckbox.checked,
+ "reveal checkbox should be unchecked if OS auth dialog canceled"
+ );
+ });
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ });
+ await osAuthDialogShown;
+ info("OS auth dialog shown and authenticated");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ Assert.ok(
+ revealCheckbox.checked,
+ "reveal checkbox should be checked if OS auth dialog authenticated"
+ );
+ });
+
+ info("'Edit' shouldn't show the prompt since the user has authenticated now");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ !loginItem.dataset.editing,
+ "Not in edit mode before clicking 'Edit'"
+ );
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "waiting for 'edit' mode"
+ );
+ Assert.ok(loginItem.dataset.editing, "In edit mode");
+ });
+
+ info("Test that the OS auth prompt is shown after about:logins is reopened");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+
+ // Show OS auth dialog since the page has been reloaded.
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ });
+ await osAuthDialogShown;
+ info("OS auth dialog shown and canceled");
+
+ // Show OS auth dialog since the previous attempt was canceled
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ info("clicking on reveal checkbox to hide the password");
+ revealCheckbox.click();
+ });
+ await osAuthDialogShown;
+ info("OS auth dialog shown and passed");
+
+ // Show OS auth dialog since the timeout will have expired
+ osAuthDialogShown = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ info("clicking on reveal checkbox to reveal password");
+ revealCheckbox.click();
+ });
+ info("waiting for os auth dialog");
+ await osAuthDialogShown;
+ info("OS auth dialog shown and passed after timeout expiration");
+
+ // Disable the OS auth feature and confirm the prompt doesn't appear
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.management.page.os-auth.enabled", false]],
+ });
+ info("Reload about:logins to reset the timeout");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+
+ info("'Edit' shouldn't show the prompt since the feature has been disabled");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ !loginItem.dataset.editing,
+ "Not in edit mode before clicking 'Edit'"
+ );
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "waiting for 'edit' mode"
+ );
+ Assert.ok(loginItem.dataset.editing, "In edit mode");
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_primaryPassword.js b/browser/components/aboutlogins/tests/browser/browser_primaryPassword.js
new file mode 100644
index 0000000000..79a1e9a1da
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_primaryPassword.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function waitForLoginCountToReach(browser, loginCount) {
+ return SpecialPowers.spawn(
+ browser,
+ [loginCount],
+ async expectedLoginCount => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return loginList._loginGuidsSortedOrder.length == expectedLoginCount;
+ });
+ return loginList._loginGuidsSortedOrder.length;
+ }
+ );
+}
+
+add_setup(async function () {
+ await addLogin(TEST_LOGIN1);
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ LoginTestUtils.primaryPassword.disable();
+ });
+});
+
+add_task(async function test() {
+ // Confirm that the mocking of the OS auth dialog isn't enabled so the
+ // test will timeout if a real OS auth dialog is shown. We don't show
+ // the OS auth dialog when Primary Password is enabled.
+ Assert.equal(
+ Services.prefs.getStringPref(
+ "toolkit.osKeyStore.unofficialBuildOnlyLogin",
+ ""
+ ),
+ "",
+ "Pref should be set to default value of empty string to start the test"
+ );
+ LoginTestUtils.primaryPassword.enable();
+
+ let mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("cancel");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ await mpDialogShown;
+
+ let browser = gBrowser.selectedBrowser;
+ let logins = await waitForLoginCountToReach(browser, 0);
+ Assert.equal(
+ logins,
+ 0,
+ "No logins should be displayed when MP is set and unauthenticated"
+ );
+
+ let notification;
+ await TestUtils.waitForCondition(
+ () =>
+ (notification = gBrowser
+ .getNotificationBox()
+ .getNotificationWithValue("primary-password-login-required")),
+ "waiting for primary-password-login-required notification"
+ );
+
+ Assert.ok(
+ notification,
+ "primary-password-login-required notification should be visible"
+ );
+
+ let buttons = notification.buttonContainer.querySelectorAll(
+ ".notification-button"
+ );
+ Assert.equal(buttons.length, 1, "Should have one button.");
+
+ let refreshPromise = BrowserTestUtils.browserLoaded(browser);
+ // Sign in with the Primary Password this time the dialog is shown
+ mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("authenticate");
+ // Click the button to reload the page.
+ buttons[0].click();
+ await refreshPromise;
+ info("Page reloaded");
+
+ await mpDialogShown;
+ info("Primary Password dialog shown and authenticated");
+
+ logins = await waitForLoginCountToReach(browser, 1);
+ Assert.equal(
+ logins,
+ 1,
+ "Logins should be displayed when MP is set and authenticated"
+ );
+
+ // Show MP dialog when Copy Password button clicked
+ mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("cancel");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let copyButton = loginItem.shadowRoot.querySelector(
+ ".copy-password-button"
+ );
+ copyButton.click();
+ });
+ await mpDialogShown;
+ info("Primary Password dialog shown and canceled");
+ mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("authenticate");
+ info("Clicking copy password button again");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let copyButton = loginItem.shadowRoot.querySelector(
+ ".copy-password-button"
+ );
+ copyButton.click();
+ });
+ await mpDialogShown;
+ info("Primary Password dialog shown and authenticated");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let copyButton = loginItem.shadowRoot.querySelector(
+ ".copy-password-button"
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ return copyButton.disabled;
+ }, "Waiting for copy button to be disabled");
+ info("Password was copied to clipboard");
+ });
+
+ // Show MP dialog when Reveal Password checkbox is checked if not on a new login
+ mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("cancel");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ });
+ await mpDialogShown;
+ info("Primary Password dialog shown and canceled");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ Assert.ok(
+ !revealCheckbox.checked,
+ "reveal checkbox should be unchecked if MP dialog canceled"
+ );
+ });
+ mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("authenticate");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ });
+ await mpDialogShown;
+ info("Primary Password dialog shown and authenticated");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ Assert.ok(
+ revealCheckbox.checked,
+ "reveal checkbox should be checked if MP dialog authenticated"
+ );
+ });
+
+ info("Test toggling the password visibility on a new login");
+ await SpecialPowers.spawn(browser, [], async function createNewToggle() {
+ let createButton = content.document
+ .querySelector("login-list")
+ .shadowRoot.querySelector(".create-login-button");
+ createButton.click();
+
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let passwordField = loginItem.shadowRoot.querySelector(
+ "input[name='password']"
+ );
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ Assert.ok(ContentTaskUtils.is_visible(revealCheckbox), "Toggle visible");
+ Assert.ok(!revealCheckbox.checked, "Not revealed initially");
+ Assert.equal(passwordField.type, "password", "type is password");
+ revealCheckbox.click();
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return passwordField.type == "text";
+ }, "Waiting for type='text'");
+ Assert.ok(revealCheckbox.checked, "Not revealed after click");
+
+ let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
+ cancelButton.click();
+ });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ const loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+
+ const loginFilter = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector("login-filter")
+ );
+ loginFilter.value = "pass1";
+ Assert.equal(
+ loginList._list.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).length,
+ 1,
+ "login-list should show corresponding result when primary password is enabled"
+ );
+ loginFilter.value = "";
+ Assert.equal(
+ loginList._list.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).length,
+ 1,
+ "login-list should show all results since the filter is empty"
+ );
+ });
+ LoginTestUtils.primaryPassword.disable();
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ Cu.waiveXrays(content).AboutLoginsUtils.primaryPasswordEnabled = false;
+ const loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ const loginFilter = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector("login-filter")
+ );
+ loginFilter.value = "pass1";
+ Assert.equal(
+ loginList._list.querySelectorAll(
+ ".login-list-item[data-guid]:not([hidden])"
+ ).length,
+ 1,
+ "login-list should show login with matching password since MP is disabled"
+ );
+ });
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_login_item_after_successful_auth() {
+ // Confirm that the mocking of the OS auth dialog isn't enabled so the
+ // test will timeout if a real OS auth dialog is shown. We don't show
+ // the OS auth dialog when Primary Password is enabled.
+ Assert.equal(
+ Services.prefs.getStringPref(
+ "toolkit.osKeyStore.unofficialBuildOnlyLogin",
+ ""
+ ),
+ "",
+ "Pref should be set to default value of empty string to start the test"
+ );
+ LoginTestUtils.primaryPassword.enable();
+
+ let mpDialogShown = forceAuthTimeoutAndWaitForMPDialog("authenticate");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ await mpDialogShown;
+
+ let browser = gBrowser.selectedBrowser;
+ let logins = await waitForLoginCountToReach(browser, 1);
+ Assert.equal(
+ logins,
+ 1,
+ "Logins should be displayed when MP is set and authenticated"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ !loginItem.classList.contains("no-logins"),
+ "Login item should have content after MP is authenticated"
+ );
+ });
+
+ LoginTestUtils.primaryPassword.disable();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js b/browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js
new file mode 100644
index 0000000000..41503e2b4d
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js
@@ -0,0 +1,555 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const OS_REAUTH_PREF = "signon.management.page.os-auth.enabled";
+
+async function openRemoveAllDialog(browser) {
+ await SimpleTest.promiseFocus(browser);
+ await BrowserTestUtils.synthesizeMouseAtCenter("menu-button", {}, browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let menuButton = content.document.querySelector("menu-button");
+ let menu = menuButton.shadowRoot.querySelector("ul.menu");
+ await ContentTaskUtils.waitForCondition(() => !menu.hidden);
+ });
+ function getRemoveAllMenuButton() {
+ let menuButton = window.document.querySelector("menu-button");
+ return menuButton.shadowRoot.querySelector(".menuitem-remove-all-logins");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getRemoveAllMenuButton,
+ {},
+ browser
+ );
+ info("remove all dialog should be opened");
+}
+
+async function activateLoginItemEdit(browser) {
+ await SimpleTest.promiseFocus(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(loginItem, "Login item should exist");
+ });
+ function getLoginItemEditButton() {
+ let loginItem = window.document.querySelector("login-item");
+ return loginItem.shadowRoot.querySelector(".edit-button");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getLoginItemEditButton,
+ {},
+ browser
+ );
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = content.document.querySelector("login-item");
+ loginItem.shadowRoot.querySelector(".edit-button").click();
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "Waiting for login-item to enter edit mode"
+ );
+ });
+ info("login-item should be in edit mode");
+}
+
+async function activateCreateNewLogin(browser) {
+ await SimpleTest.promiseFocus(browser);
+ function getCreateNewLoginButton() {
+ let loginList = window.document.querySelector("login-list");
+ return loginList.shadowRoot.querySelector(".create-login-button");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getCreateNewLoginButton,
+ {},
+ browser
+ );
+}
+
+async function waitForRemoveAllLogins() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject, topic, changeType) {
+ if (changeType != "removeAllLogins") {
+ return;
+ }
+
+ Services.obs.removeObserver(observer, "passwordmgr-storage-changed");
+ resolve();
+ }, "passwordmgr-storage-changed");
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [[OS_REAUTH_PREF, false]],
+ });
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ await SpecialPowers.popPrefEnv();
+ });
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+});
+
+add_task(async function test_remove_all_dialog_l10n() {
+ Assert.ok(TEST_LOGIN1, "test_login1");
+ let browser = gBrowser.selectedBrowser;
+ await openRemoveAllDialog(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("remove-logins-dialog")
+ );
+ Assert.ok(!dialog.hidden);
+ let title = dialog.shadowRoot.querySelector(".title");
+ let message = dialog.shadowRoot.querySelector(".message");
+ let label = dialog.shadowRoot.querySelector(".checkbox-text");
+ let cancelButton = dialog.shadowRoot.querySelector(".cancel-button");
+ let removeAllButton = dialog.shadowRoot.querySelector(".confirm-button");
+ await content.document.l10n.translateElements([
+ title,
+ message,
+ label,
+ cancelButton,
+ removeAllButton,
+ ]);
+ Assert.equal(
+ title.dataset.l10nId,
+ "about-logins-confirm-remove-all-dialog-title",
+ "Title contents should match l10n-id attribute set on element"
+ );
+ Assert.equal(
+ message.dataset.l10nId,
+ "about-logins-confirm-remove-all-dialog-message",
+ "Message contents should match l10n-id attribute set on element"
+ );
+ Assert.equal(
+ label.dataset.l10nId,
+ "about-logins-confirm-remove-all-dialog-checkbox-label",
+ "Label contents should match l10n-id attribute set on outer element"
+ );
+ Assert.equal(
+ cancelButton.dataset.l10nId,
+ "confirmation-dialog-cancel-button",
+ "Cancel button contents should match l10n-id attribute set on outer element"
+ );
+ Assert.equal(
+ removeAllButton.dataset.l10nId,
+ "about-logins-confirm-remove-all-dialog-confirm-button-label",
+ "Remove all button contents should match l10n-id attribute set on outer element"
+ );
+ Assert.equal(
+ JSON.parse(title.dataset.l10nArgs).count,
+ 1,
+ "Title contents should match l10n-args attribute set on element"
+ );
+ Assert.equal(
+ JSON.parse(message.dataset.l10nArgs).count,
+ 1,
+ "Message contents should match l10n-args attribute set on element"
+ );
+ Assert.equal(
+ JSON.parse(label.dataset.l10nArgs).count,
+ 1,
+ "Label contents should match l10n-id attribute set on outer element"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ dialog.shadowRoot.querySelector(".cancel-button"),
+ {},
+ content
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => dialog.hidden,
+ "Waiting for the dialog to be hidden after clicking cancel button"
+ );
+ });
+});
+
+add_task(async function test_remove_all_dialog_keyboard_navigation() {
+ let browser = gBrowser.selectedBrowser;
+ await openRemoveAllDialog(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("remove-logins-dialog")
+ );
+ let cancelButton = dialog.shadowRoot.querySelector(".cancel-button");
+ let removeAllButton = dialog.shadowRoot.querySelector(".confirm-button");
+ Assert.equal(
+ removeAllButton.disabled,
+ true,
+ "Remove all should be disabled on dialog open"
+ );
+ await EventUtils.synthesizeKey(" ", {}, content);
+ Assert.equal(
+ removeAllButton.disabled,
+ false,
+ "Remove all should be enabled when activating the checkbox"
+ );
+ await EventUtils.synthesizeKey(" ", {}, content);
+ Assert.equal(
+ removeAllButton.disabled,
+ true,
+ "Remove all should be disabled after deactivating the checkbox"
+ );
+ await EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ Assert.equal(
+ dialog.shadowRoot.activeElement,
+ cancelButton,
+ "Cancel button should be the next element in tab order"
+ );
+ await EventUtils.synthesizeKey(" ", {}, content);
+ await ContentTaskUtils.waitForCondition(
+ () => dialog.hidden,
+ "Waiting for the dialog to be hidden after activating cancel button via Space key"
+ );
+ });
+ await openRemoveAllDialog(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("remove-logins-dialog")
+ );
+ await EventUtils.synthesizeKey("KEY_Escape", {}, content);
+ await ContentTaskUtils.waitForCondition(
+ () => dialog.hidden,
+ "Waiting for the dialog to be hidden after activating Escape key"
+ );
+ });
+ await openRemoveAllDialog(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("remove-logins-dialog")
+ );
+ let dismissButton = dialog.shadowRoot.querySelector(".dismiss-button");
+ await EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, content);
+ Assert.equal(
+ dialog.shadowRoot.activeElement,
+ dismissButton,
+ "dismiss button should be focused"
+ );
+ await EventUtils.synthesizeKey(" ", {}, content);
+ await ContentTaskUtils.waitForCondition(
+ () => dialog.hidden,
+ "Waiting for the dialog to be hidden after activating X button"
+ );
+ });
+});
+
+add_task(async function test_remove_all_dialog_remove_logins() {
+ TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
+ let browser = gBrowser.selectedBrowser;
+ let removeAllPromise = waitForRemoveAllLogins();
+
+ await openRemoveAllDialog(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("remove-logins-dialog")
+ );
+ let title = dialog.shadowRoot.querySelector(".title");
+ let message = dialog.shadowRoot.querySelector(".message");
+ let label = dialog.shadowRoot.querySelector(".checkbox-text");
+ let cancelButton = dialog.shadowRoot.querySelector(".cancel-button");
+ let removeAllButton = dialog.shadowRoot.querySelector(".confirm-button");
+
+ let checkbox = dialog.shadowRoot.querySelector(".checkbox");
+
+ await content.document.l10n.translateElements([
+ title,
+ message,
+ cancelButton,
+ removeAllButton,
+ label,
+ checkbox,
+ ]);
+ Assert.equal(
+ dialog.shadowRoot.activeElement,
+ checkbox,
+ "Checkbox should be the focused element on dialog open"
+ );
+ Assert.equal(
+ title.dataset.l10nId,
+ "about-logins-confirm-remove-all-dialog-title",
+ "Title contents should match l10n-id attribute set on element"
+ );
+ Assert.equal(
+ JSON.parse(title.dataset.l10nArgs).count,
+ 2,
+ "Title contents should match l10n-args attribute set on element"
+ );
+ Assert.equal(
+ message.dataset.l10nId,
+ "about-logins-confirm-remove-all-dialog-message",
+ "Message contents should match l10n-id attribute set on element"
+ );
+ Assert.equal(
+ JSON.parse(message.dataset.l10nArgs).count,
+ 2,
+ "Message contents should match l10n-args attribute set on element"
+ );
+ Assert.equal(
+ label.dataset.l10nId,
+ "about-logins-confirm-remove-all-dialog-checkbox-label",
+ "Label contents should match l10n-id attribute set on outer element"
+ );
+ Assert.equal(
+ JSON.parse(label.dataset.l10nArgs).count,
+ 2,
+ "Label contents should match l10n-id attribute set on outer element"
+ );
+ Assert.equal(
+ cancelButton.dataset.l10nId,
+ "confirmation-dialog-cancel-button",
+ "Cancel button contents should match l10n-id attribute set on outer element"
+ );
+ Assert.equal(
+ removeAllButton.dataset.l10nId,
+ "about-logins-confirm-remove-all-dialog-confirm-button-label",
+ "Remove all button contents should match l10n-id attribute set on outer element"
+ );
+ Assert.equal(
+ removeAllButton.disabled,
+ true,
+ "Remove all button should be disabled on dialog open"
+ );
+ });
+ function activateConfirmCheckbox() {
+ let dialog = window.document.querySelector("remove-logins-dialog");
+ return dialog.shadowRoot.querySelector(".checkbox");
+ }
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ activateConfirmCheckbox,
+ {},
+ browser
+ );
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("remove-logins-dialog")
+ );
+ let removeAllButton = dialog.shadowRoot.querySelector(".confirm-button");
+ Assert.equal(
+ removeAllButton.disabled,
+ false,
+ "Remove all should be enabled after clicking the checkbox"
+ );
+ });
+ function getDialogRemoveAllButton() {
+ let dialog = window.document.querySelector("remove-logins-dialog");
+ return dialog.shadowRoot.querySelector(".confirm-button");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getDialogRemoveAllButton,
+ {},
+ browser
+ );
+ await removeAllPromise;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.documentElement.classList.contains("no-logins"),
+ "Waiting for no logins view since all logins should be deleted"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ !content.document.documentElement.classList.contains("login-selected"),
+ "Waiting for the FxA Sync illustration to reappear"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginList.classList.contains("no-logins"),
+ "Waiting for login-list to be in no logins view as all logins should be deleted"
+ );
+ });
+ await BrowserTestUtils.synthesizeMouseAtCenter("menu-button", {}, browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let menuButton = content.document.querySelector("menu-button");
+ let removeAllMenuButton = menuButton.shadowRoot.querySelector(
+ ".menuitem-remove-all-logins"
+ );
+ Assert.ok(
+ removeAllMenuButton.disabled,
+ "Remove all logins menu button is disabled if there are no logins"
+ );
+ });
+ await SpecialPowers.spawn(browser, [], async () => {
+ let menuButton = Cu.waiveXrays(
+ content.document.querySelector("menu-button")
+ );
+ let menu = menuButton.shadowRoot.querySelector("ul.menu");
+ await EventUtils.synthesizeKey("KEY_Escape", {}, content);
+ await ContentTaskUtils.waitForCondition(
+ () => menu.hidden,
+ "Waiting for menu to close"
+ );
+ });
+});
+
+add_task(async function test_edit_mode_resets_on_remove_all_with_login() {
+ TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
+ let removeAllPromise = waitForRemoveAllLogins();
+ let browser = gBrowser.selectedBrowser;
+ await activateLoginItemEdit(browser);
+ await openRemoveAllDialog(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ loginItem.dataset.editing,
+ "Login item is still in edit mode when the remove all dialog opens"
+ );
+ });
+ function getDialogCancelButton() {
+ let dialog = window.document.querySelector("remove-logins-dialog");
+ return dialog.shadowRoot.querySelector(".cancel-button");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getDialogCancelButton,
+ {},
+ browser
+ );
+ await TestUtils.waitForTick();
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ loginItem.dataset.editing,
+ "Login item should be in editing mode after activating the cancel button in the remove all dialog"
+ );
+ });
+
+ await openRemoveAllDialog(browser);
+ function activateConfirmCheckbox() {
+ let dialog = window.document.querySelector("remove-logins-dialog");
+ return dialog.shadowRoot.querySelector(".checkbox");
+ }
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ activateConfirmCheckbox,
+ {},
+ browser
+ );
+ await SpecialPowers.spawn(browser, [], async () => {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("remove-logins-dialog")
+ );
+ let removeAllButton = dialog.shadowRoot.querySelector(".confirm-button");
+ Assert.equal(
+ removeAllButton.disabled,
+ false,
+ "Remove all should be enabled after clicking the checkbox"
+ );
+ });
+ function getDialogRemoveAllButton() {
+ let dialog = window.document.querySelector("remove-logins-dialog");
+ return dialog.shadowRoot.querySelector(".confirm-button");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getDialogRemoveAllButton,
+ {},
+ browser
+ );
+ await TestUtils.waitForTick();
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ !loginItem.dataset.editing,
+ "Login item should not be in editing mode after activating the confirm button in the remove all dialog"
+ );
+ });
+ await removeAllPromise;
+});
+
+add_task(async function test_remove_all_when_creating_new_login() {
+ TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
+ let removeAllPromise = waitForRemoveAllLogins();
+ let browser = gBrowser.selectedBrowser;
+ await activateCreateNewLogin(browser);
+ await openRemoveAllDialog(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ loginItem.dataset.editing,
+ "Login item should be in edit mode when the remove all dialog opens"
+ );
+ Assert.ok(
+ loginItem.dataset.isNewLogin,
+ "Login item should be in the 'new login' state when the remove all dialog opens"
+ );
+ });
+ function getDialogCancelButton() {
+ let dialog = window.document.querySelector("remove-logins-dialog");
+ return dialog.shadowRoot.querySelector(".cancel-button");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getDialogCancelButton,
+ {},
+ browser
+ );
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ loginItem.dataset.editing,
+ "Login item is still in edit mode after cancelling out of the remove all dialog"
+ );
+ Assert.ok(
+ loginItem.dataset.isNewLogin,
+ "Login item should be in the 'newLogin' state after cancelling out of the remove all dialog"
+ );
+ });
+
+ await openRemoveAllDialog(browser);
+ function activateConfirmCheckbox() {
+ let dialog = window.document.querySelector("remove-logins-dialog");
+ return dialog.shadowRoot.querySelector(".checkbox");
+ }
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ activateConfirmCheckbox,
+ {},
+ browser
+ );
+ await SpecialPowers.spawn(browser, [], async () => {
+ let dialog = Cu.waiveXrays(
+ content.document.querySelector("remove-logins-dialog")
+ );
+ let removeAllButton = dialog.shadowRoot.querySelector(".confirm-button");
+ Assert.equal(
+ removeAllButton.disabled,
+ false,
+ "Remove all should be enabled after clicking the checkbox"
+ );
+ });
+ function getDialogRemoveAllButton() {
+ let dialog = window.document.querySelector("remove-logins-dialog");
+ return dialog.shadowRoot.querySelector(".confirm-button");
+ }
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ getDialogRemoveAllButton,
+ {},
+ browser
+ );
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = content.document.querySelector("login-item");
+ Assert.ok(
+ !loginItem.dataset.editing,
+ "Login item should not be in editing mode after activating the confirm button in the remove all dialog"
+ );
+ Assert.ok(
+ !loginItem.dataset.isNewLogin,
+ "Login item should not be in 'new login' mode after activating the confirm button in the remove all dialog"
+ );
+ });
+ await removeAllPromise;
+});
+
+add_task(async function test_ensure_icons_are_not_draggable() {
+ TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
+ let browser = gBrowser.selectedBrowser;
+ await openRemoveAllDialog(browser);
+ await SpecialPowers.spawn(browser, [], async () => {
+ let dialog = content.document.querySelector("remove-logins-dialog");
+ let warningIcon = dialog.shadowRoot.querySelector(".warning-icon");
+ Assert.ok(!warningIcon.draggable, "Warning icon should not be draggable");
+ let dismissIcon = dialog.shadowRoot.querySelector(".dismiss-icon");
+ Assert.ok(!dismissIcon.draggable, "Dismiss icon should not be draggable");
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js b/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js
new file mode 100644
index 0000000000..5ab03f9867
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function checkLoginDisplayed(browser, testGuid) {
+ await SpecialPowers.spawn(browser, [testGuid], async function (guid) {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ let loginFound = await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList._loginGuidsSortedOrder.length == 1 &&
+ loginList._loginGuidsSortedOrder[0] == guid
+ );
+ }, "Waiting for login to be displayed in page");
+ Assert.ok(loginFound, "Confirming that login is displayed in page");
+ });
+}
+
+add_task(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ registerCleanupFunction(() => {
+ Services.logins.removeAllUserFacingLogins();
+ });
+
+ const testGuid = TEST_LOGIN1.guid;
+ const tab = BrowserTestUtils.addTab(gBrowser, "about:logins");
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await checkLoginDisplayed(browser, testGuid);
+
+ BrowserTestUtils.removeTab(tab);
+ info("Adding a lazy about:logins tab...");
+ let lazyTab = BrowserTestUtils.addTab(gBrowser, "about:logins", {
+ createLazyBrowser: true,
+ });
+
+ Assert.equal(lazyTab.linkedPanel, "", "Tab is lazy");
+ let tabLoaded = new Promise(resolve => {
+ gBrowser.addTabsProgressListener({
+ async onLocationChange(aBrowser) {
+ if (lazyTab.linkedBrowser == aBrowser) {
+ gBrowser.removeTabsProgressListener(this);
+ await Promise.resolve();
+ resolve();
+ }
+ },
+ });
+ });
+
+ info("Switching tab to cause it to get restored");
+ const browserLoaded = BrowserTestUtils.browserLoaded(lazyTab.linkedBrowser);
+ await BrowserTestUtils.switchTab(gBrowser, lazyTab);
+
+ await tabLoaded;
+ await browserLoaded;
+
+ let lazyBrowser = lazyTab.linkedBrowser;
+ await checkLoginDisplayed(lazyBrowser, testGuid);
+
+ BrowserTestUtils.removeTab(lazyTab);
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js b/browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js
new file mode 100644
index 0000000000..0305107d23
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js
@@ -0,0 +1,276 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+EXPECTED_BREACH = {
+ AddedDate: "2018-12-20T23:56:26Z",
+ BreachDate: "2018-12-16",
+ Domain: "breached.example.com",
+ Name: "Breached",
+ PwnCount: 1643100,
+ DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
+ _status: "synced",
+ id: "047940fe-d2fd-4314-b636-b4a952ee0043",
+ last_modified: "1541615610052",
+ schema: "1541615609018",
+};
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_tab_key_nav() {
+ const browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ // Helper function for getting the resulting DOM element given a list of selectors possibly inside shadow DOM
+ const selectWithShadowRootIfNeeded = (document, selectorsArray) =>
+ selectorsArray.reduce(
+ (selectionSoFar, currentSelector) =>
+ selectionSoFar.shadowRoot
+ ? selectionSoFar.shadowRoot.querySelector(currentSelector)
+ : selectionSoFar.querySelector(currentSelector),
+ document
+ );
+
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ // list [selector, shadow root selector] for each element
+ // in the order we expect them to be navigated.
+ const expectedElementsInOrder = [
+ ["login-list", "login-filter", "input"],
+ ["login-list", "button.create-login-button"],
+ ["login-list", "select#login-sort"],
+ ["login-list", "ol"],
+ ["login-item", "button.edit-button"],
+ ["login-item", "button.delete-button"],
+ ["login-item", "a.origin-input"],
+ ["login-item", "button.copy-username-button"],
+ ["login-item", "input.reveal-password-checkbox"],
+ ["login-item", "button.copy-password-button"],
+ ];
+
+ const firstElement = selectWithShadowRootIfNeeded(
+ content.document,
+ expectedElementsInOrder.at(0)
+ );
+
+ const lastElement = selectWithShadowRootIfNeeded(
+ content.document,
+ expectedElementsInOrder.at(-1)
+ );
+
+ async function tab() {
+ EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ await new Promise(resolve => content.requestAnimationFrame(resolve));
+ // The following line can help with focus trap debugging:
+ // await new Promise(resolve => content.window.setTimeout(resolve, 500));
+ }
+ async function shiftTab() {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, content);
+ await new Promise(resolve => content.requestAnimationFrame(resolve));
+ // await new Promise(resolve => content.window.setTimeout(resolve, 500));
+ }
+
+ // Getting focused shadow DOM element itself instead of shadowRoot,
+ // using recursion for any component-nesting level, as in:
+ // document.activeElement.shadowRoot.activeElement.shadowRoot.activeElement
+ function getFocusedElement() {
+ let element = content.document.activeElement;
+ const getShadowRootFocus = e => {
+ if (e.shadowRoot) {
+ return getShadowRootFocus(e.shadowRoot.activeElement);
+ }
+ return e;
+ };
+ return getShadowRootFocus(element);
+ }
+
+ // Ensure the test starts in a valid state
+ firstElement.focus();
+ // Assert that we tab navigate correctly
+ for (let expectedSelector of expectedElementsInOrder) {
+ const expectedElement = selectWithShadowRootIfNeeded(
+ content.document,
+ expectedSelector
+ );
+
+ // By default, MacOS will skip over certain text controls, such as links.
+ if (
+ content.window.navigator.platform.toLowerCase().includes("mac") &&
+ expectedElement.tagName === "A"
+ ) {
+ continue;
+ }
+
+ const actualElement = getFocusedElement();
+
+ Assert.equal(
+ actualElement,
+ expectedElement,
+ "Actual focused element should equal the expected focused element"
+ );
+ await tab();
+ }
+
+ lastElement.focus();
+
+ // Assert that we shift + tab navigate correctly starting from the last ordered element
+ for (let expectedSelector of expectedElementsInOrder.reverse()) {
+ const expectedElement = selectWithShadowRootIfNeeded(
+ content.document,
+ expectedSelector
+ );
+ // By default, MacOS will skip over certain text controls, such as links.
+ if (
+ content.window.navigator.platform.toLowerCase().includes("mac") &&
+ expectedElement.tagName === "A"
+ ) {
+ continue;
+ }
+
+ const actualElement = getFocusedElement();
+ Assert.equal(
+ actualElement,
+ expectedElement,
+ "Actual focused element should equal the expected focused element"
+ );
+ await shiftTab();
+ }
+ await tab(); // tab back to the first element
+ });
+});
+
+add_task(async function test_tab_to_create_button() {
+ const browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async () => {
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+
+ function waitForAnimationFrame() {
+ return new Promise(resolve => content.requestAnimationFrame(resolve));
+ }
+
+ async function tab() {
+ EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ await waitForAnimationFrame();
+ }
+
+ const loginList = content.document.querySelector("login-list");
+ const loginFilter = loginList.shadowRoot.querySelector("login-filter");
+ const loginSort = loginList.shadowRoot.getElementById("login-sort");
+ const loginListbox = loginList.shadowRoot.querySelector("ol");
+ const createButton = loginList.shadowRoot.querySelector(
+ ".create-login-button"
+ );
+
+ const getFocusedElement = () => loginList.shadowRoot.activeElement;
+ Assert.equal(getFocusedElement(), loginFilter, "login-filter is focused");
+
+ await tab();
+ Assert.equal(getFocusedElement(), createButton, "create button is focused");
+
+ await tab();
+ Assert.equal(getFocusedElement(), loginSort, "login sort is focused");
+
+ await tab();
+ Assert.equal(getFocusedElement(), loginListbox, "listbox is focused next");
+
+ await tab();
+ Assert.equal(getFocusedElement(), null, "login-list isn't focused again");
+ });
+});
+
+add_task(async function test_tab_to_edit_button() {
+ TEST_LOGIN3 = await addLogin(TEST_LOGIN3);
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(
+ browser,
+ [[TEST_LOGIN1.guid, TEST_LOGIN3.guid]],
+ async ([testLoginNormalGuid, testLoginBreachedGuid]) => {
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+
+ function waitForAnimationFrame() {
+ return new Promise(resolve => content.requestAnimationFrame(resolve));
+ }
+
+ async function tab() {
+ EventUtils.synthesizeKey("KEY_Tab", {}, content);
+ await waitForAnimationFrame();
+ }
+
+ const loginList = content.document.querySelector("login-list");
+ const loginItem = content.document.querySelector("login-item");
+ const loginFilter = loginList.shadowRoot.querySelector("login-filter");
+ const createButton = loginList.shadowRoot.querySelector(
+ ".create-login-button"
+ );
+ const loginSort = loginList.shadowRoot.getElementById("login-sort");
+ const loginListbox = loginList.shadowRoot.querySelector("ol");
+ const editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ const breachAlert = loginItem.shadowRoot.querySelector(".breach-alert");
+ const getFocusedElement = () => {
+ if (content.document.activeElement == loginList) {
+ return loginList.shadowRoot.activeElement;
+ }
+ if (content.document.activeElement == loginItem) {
+ return loginItem.shadowRoot.activeElement;
+ }
+ if (content.document.activeElement == loginFilter) {
+ return loginFilter.shadowRoot.activeElement;
+ }
+ Assert.ok(
+ false,
+ "not expecting a different element to get focused in this test: " +
+ content.document.activeElement.outerHTML
+ );
+ return undefined;
+ };
+
+ for (let guidToSelect of [testLoginNormalGuid, testLoginBreachedGuid]) {
+ let loginListItem = loginList.shadowRoot.querySelector(
+ `.login-list-item[data-guid="${guidToSelect}"]`
+ );
+ loginListItem.click();
+ await ContentTaskUtils.waitForCondition(() => {
+ let waivedLoginItem = Cu.waiveXrays(loginItem);
+ return (
+ waivedLoginItem._login &&
+ waivedLoginItem._login.guid == guidToSelect
+ );
+ }, "waiting for login-item to show the selected login");
+
+ Assert.equal(
+ breachAlert.hidden,
+ guidToSelect == testLoginNormalGuid,
+ ".breach-alert should be hidden if the login is not breached. current login breached? " +
+ (guidToSelect == testLoginBreachedGuid)
+ );
+
+ createButton.focus();
+ Assert.equal(
+ getFocusedElement(),
+ createButton,
+ "create button is focused"
+ );
+
+ await tab();
+ Assert.equal(getFocusedElement(), loginSort, "login sort is focused");
+
+ await tab();
+ Assert.equal(
+ getFocusedElement(),
+ loginListbox,
+ "listbox is focused next"
+ );
+
+ await tab();
+ Assert.equal(getFocusedElement(), editButton, "edit button is focused");
+ }
+ }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_updateLogin.js b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
new file mode 100644
index 0000000000..efa8bbdd7b
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
@@ -0,0 +1,421 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CONCEALED_PASSWORD_TEXT } = ChromeUtils.importESModule(
+ "chrome://browser/content/aboutlogins/aboutLoginsUtils.mjs"
+);
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ });
+});
+
+add_task(async function test_show_logins() {
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_LOGIN1.guid], async loginGuid => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ let loginFound = await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginList._loginGuidsSortedOrder.length == 1 &&
+ loginList._loginGuidsSortedOrder[0] == loginGuid
+ );
+ }, "Waiting for login to be displayed");
+ Assert.ok(
+ loginFound,
+ "Stored logins should be displayed upon loading the page"
+ );
+ });
+});
+
+add_task(async function test_login_item() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ return;
+ }
+
+ async function test_discard_dialog(
+ login,
+ exitPointSelector,
+ concealedPasswordText
+ ) {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "Entering edit mode"
+ );
+ await Promise.resolve();
+
+ let usernameInput = loginItem.shadowRoot.querySelector(
+ "input[name='username']"
+ );
+ let passwordInput = loginItem._passwordInput;
+ usernameInput.value += "-undome";
+ passwordInput.value += "-undome";
+
+ let dialog = content.document.querySelector("confirmation-dialog");
+ Assert.ok(dialog.hidden, "Confirm dialog should initially be hidden");
+
+ let exitPoint =
+ loginItem.shadowRoot.querySelector(exitPointSelector) ||
+ loginList.shadowRoot.querySelector(exitPointSelector);
+ exitPoint.click();
+
+ Assert.ok(!dialog.hidden, "Confirm dialog should be visible");
+
+ let confirmDiscardButton =
+ dialog.shadowRoot.querySelector(".confirm-button");
+ await content.document.l10n.translateElements([
+ dialog.shadowRoot.querySelector(".title"),
+ dialog.shadowRoot.querySelector(".message"),
+ confirmDiscardButton,
+ ]);
+
+ confirmDiscardButton.click();
+
+ Assert.ok(
+ dialog.hidden,
+ "Confirm dialog should be hidden after confirming"
+ );
+
+ await Promise.resolve();
+
+ let loginListItem = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector(".login-list-item[data-guid]")
+ );
+
+ loginListItem.click();
+
+ await ContentTaskUtils.waitForCondition(
+ () => usernameInput.value == login.username
+ );
+
+ Assert.equal(
+ usernameInput.value,
+ login.username,
+ "Username change should be reverted"
+ );
+ Assert.equal(
+ passwordInput.value,
+ login.password,
+ "Password change should be reverted"
+ );
+ let passwordDisplayInput = loginItem._passwordDisplayInput;
+ Assert.equal(
+ passwordDisplayInput.value,
+ concealedPasswordText,
+ "Password change should be reverted for display"
+ );
+ Assert.ok(
+ !passwordInput.hasAttribute("value"),
+ "Password shouldn't be exposed in @value"
+ );
+ Assert.equal(
+ passwordInput.style.width,
+ login.password.length + "ch",
+ "Password field width shouldn't have changed"
+ );
+ }
+
+ let browser = gBrowser.selectedBrowser;
+ let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(
+ browser,
+ [LoginHelper.loginToVanillaObject(TEST_LOGIN1)],
+ async login => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let loginListItem = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector(".login-list-item[data-guid]")
+ );
+ loginListItem.click();
+
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ let loginItemPopulated = await ContentTaskUtils.waitForCondition(() => {
+ return (
+ loginItem._login.guid == loginListItem.dataset.guid &&
+ loginItem._login.guid == login.guid
+ );
+ }, "Waiting for login item to get populated");
+ Assert.ok(loginItemPopulated, "The login item should get populated");
+
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+ }
+ );
+ info("waiting for oskeystore auth #1");
+ await reauthObserved;
+ await SpecialPowers.spawn(
+ browser,
+ [
+ LoginHelper.loginToVanillaObject(TEST_LOGIN1),
+ ".create-login-button",
+ CONCEALED_PASSWORD_TEXT,
+ ],
+ test_discard_dialog
+ );
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+ });
+ info("waiting for oskeystore auth #2");
+ await reauthObserved;
+ await SpecialPowers.spawn(
+ browser,
+ [
+ LoginHelper.loginToVanillaObject(TEST_LOGIN1),
+ ".cancel-button",
+ CONCEALED_PASSWORD_TEXT,
+ ],
+ test_discard_dialog
+ );
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+ });
+ info("waiting for oskeystore auth #3");
+ await reauthObserved;
+ await SpecialPowers.spawn(
+ browser,
+ [LoginHelper.loginToVanillaObject(TEST_LOGIN1), CONCEALED_PASSWORD_TEXT],
+ async (login, concealedPasswordText) => {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "Entering edit mode"
+ );
+ await Promise.resolve();
+ let usernameInput = loginItem.shadowRoot.querySelector(
+ "input[name='username']"
+ );
+ let passwordInput = loginItem._passwordInput;
+ let passwordDisplayInput = loginItem._passwordDisplayInput;
+
+ Assert.ok(
+ loginItem.dataset.editing,
+ "LoginItem should be in 'edit' mode"
+ );
+ Assert.equal(
+ passwordInput.type,
+ "password",
+ "Password should still be hidden before revealed in edit mode"
+ );
+
+ passwordDisplayInput.focus();
+
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ Assert.ok(
+ revealCheckbox.checked,
+ "reveal-checkbox should be checked when password input is focused"
+ );
+
+ Assert.equal(
+ passwordInput.type,
+ "text",
+ "Password should be shown as text when focused"
+ );
+ let saveChangesButton = loginItem.shadowRoot.querySelector(
+ ".save-changes-button"
+ );
+
+ saveChangesButton.click();
+
+ await ContentTaskUtils.waitForCondition(() => {
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ return !editButton.disabled;
+ }, "Waiting to exit edit mode");
+
+ Assert.ok(
+ !revealCheckbox.checked,
+ "reveal-checkbox should be unchecked after saving changes"
+ );
+ Assert.ok(
+ !loginItem.dataset.editing,
+ "LoginItem should not be in 'edit' mode after saving"
+ );
+ Assert.equal(
+ passwordInput.type,
+ "password",
+ "Password should be hidden after exiting edit mode"
+ );
+ Assert.equal(
+ usernameInput.value,
+ login.username,
+ "Username change should be reverted"
+ );
+ Assert.equal(
+ passwordInput.value,
+ login.password,
+ "Password change should be reverted"
+ );
+ Assert.equal(
+ passwordDisplayInput.value,
+ concealedPasswordText,
+ "Password change should be reverted for display"
+ );
+ Assert.ok(
+ !passwordInput.hasAttribute("value"),
+ "Password shouldn't be exposed in @value"
+ );
+ Assert.equal(
+ passwordInput.style.width,
+ login.password.length + "ch",
+ "Password field width shouldn't have changed"
+ );
+ }
+ );
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+ });
+ info("waiting for oskeystore auth #4");
+ await reauthObserved;
+ await SpecialPowers.spawn(
+ browser,
+ [LoginHelper.loginToVanillaObject(TEST_LOGIN1)],
+ async login => {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "Entering edit mode"
+ );
+ await Promise.resolve();
+
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ revealCheckbox.click();
+ Assert.ok(
+ revealCheckbox.checked,
+ "reveal-checkbox should be checked after clicking"
+ );
+
+ let usernameInput = loginItem.shadowRoot.querySelector(
+ "input[name='username']"
+ );
+ let passwordInput = loginItem._passwordInput;
+
+ usernameInput.value += "-saveme";
+ passwordInput.value += "-saveme";
+
+ Assert.ok(
+ loginItem.dataset.editing,
+ "LoginItem should be in 'edit' mode"
+ );
+
+ let saveChangesButton = loginItem.shadowRoot.querySelector(
+ ".save-changes-button"
+ );
+ saveChangesButton.click();
+
+ await ContentTaskUtils.waitForCondition(() => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let guid = loginList._loginGuidsSortedOrder[0];
+ let updatedLogin = loginList._logins[guid].login;
+ return (
+ updatedLogin &&
+ updatedLogin.username == usernameInput.value &&
+ updatedLogin.password == passwordInput.value
+ );
+ }, "Waiting for corresponding login in login list to update");
+
+ Assert.ok(
+ !revealCheckbox.checked,
+ "reveal-checkbox should be unchecked after saving changes"
+ );
+ Assert.ok(
+ !loginItem.dataset.editing,
+ "LoginItem should not be in 'edit' mode after saving"
+ );
+ Assert.equal(
+ passwordInput.style.width,
+ passwordInput.value.length + "ch",
+ "Password field width should be correctly updated"
+ );
+ }
+ );
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ await SpecialPowers.spawn(browser, [], async () => {
+ let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
+ let editButton = loginItem.shadowRoot.querySelector(".edit-button");
+ editButton.click();
+ });
+ info("waiting for oskeystore auth #5");
+ await reauthObserved;
+ await SpecialPowers.spawn(
+ browser,
+ [LoginHelper.loginToVanillaObject(TEST_LOGIN1)],
+ async login => {
+ let loginItem = Cu.waiveXrays(
+ content.document.querySelector("login-item")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.editing,
+ "Entering edit mode"
+ );
+ await Promise.resolve();
+
+ Assert.ok(
+ loginItem.dataset.editing,
+ "LoginItem should be in 'edit' mode"
+ );
+ let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
+ deleteButton.click();
+ let confirmDeleteDialog = Cu.waiveXrays(
+ content.document.querySelector("confirmation-dialog")
+ );
+ let confirmDeleteButton =
+ confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+ confirmDeleteButton.click();
+
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let loginListItem = Cu.waiveXrays(
+ loginList.shadowRoot.querySelector(".login-list-item[data-guid]")
+ );
+ await ContentTaskUtils.waitForCondition(() => {
+ loginListItem = loginList.shadowRoot.querySelector(
+ ".login-list-item[data-guid]"
+ );
+ return !loginListItem;
+ }, "Waiting for login to be removed from list");
+
+ Assert.ok(
+ !loginItem.dataset.editing,
+ "LoginItem should not be in 'edit' mode after deleting"
+ );
+ }
+ );
+});
diff --git a/browser/components/aboutlogins/tests/browser/browser_vulnerableLoginAddedInSecondaryWindow.js b/browser/components/aboutlogins/tests/browser/browser_vulnerableLoginAddedInSecondaryWindow.js
new file mode 100644
index 0000000000..fac3e91af4
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_vulnerableLoginAddedInSecondaryWindow.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+EXPECTED_BREACH = {
+ AddedDate: "2018-12-20T23:56:26Z",
+ BreachDate: "2018-12-16",
+ Domain: "breached.example.com",
+ Name: "Breached",
+ PwnCount: 1643100,
+ DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"],
+ _status: "synced",
+ id: "047940fe-d2fd-4314-b636-b4a952ee0043",
+ last_modified: "1541615610052",
+ schema: "1541615609018",
+};
+
+let tabInSecondWindow;
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
+ TEST_LOGIN3 = await addLogin(TEST_LOGIN3);
+
+ let breaches = await LoginBreaches.getPotentialBreachesByLoginGUID([
+ TEST_LOGIN3,
+ ]);
+ Assert.ok(breaches.size, "TEST_LOGIN3 should be marked as breached");
+
+ // Remove the breached login so the 'alerts' option
+ // is hidden when opening about:logins.
+ Services.logins.removeLogin(TEST_LOGIN3);
+
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ tabInSecondWindow = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: newWin.gBrowser,
+ url: "about:logins",
+ });
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ Services.logins.removeAllUserFacingLogins();
+ await BrowserTestUtils.closeWindow(newWin);
+ });
+});
+
+add_task(async function test_new_login_marked_vulnerable_in_both_windows() {
+ const ORIGIN_FOR_NEW_VULNERABLE_LOGIN = "https://vulnerable";
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ Assert.ok(
+ loginList.shadowRoot.querySelector("#login-sort").namedItem("alerts")
+ .hidden,
+ "The 'alerts' option should be hidden before adding a vulnerable login to the list"
+ );
+ });
+
+ await SpecialPowers.spawn(
+ tabInSecondWindow.linkedBrowser,
+ [[TEST_LOGIN3.password, ORIGIN_FOR_NEW_VULNERABLE_LOGIN]],
+ async ([passwordOfBreachedAccount, originForNewVulnerableLogin]) => {
+ let loginList = content.document.querySelector("login-list");
+
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ loginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]")
+ .length == 2,
+ "waiting for all two initials logins to get added to login-list"
+ );
+
+ let loginSort = loginList.shadowRoot.querySelector("#login-sort");
+ Assert.ok(
+ loginSort.namedItem("alerts").hidden,
+ "The 'alerts' option should be hidden when there are no breached or vulnerable logins in the list"
+ );
+
+ let createButton = loginList.shadowRoot.querySelector(
+ ".create-login-button"
+ );
+ createButton.click();
+
+ let loginItem = content.document.querySelector("login-item");
+ await ContentTaskUtils.waitForCondition(
+ () => loginItem.dataset.isNewLogin,
+ "waiting for create login form to be visible"
+ );
+
+ let originInput = loginItem.shadowRoot.querySelector(
+ "input[name='origin']"
+ );
+ originInput.value = originForNewVulnerableLogin;
+ let passwordInput = loginItem.shadowRoot.querySelector(
+ "input[name='password']"
+ );
+ passwordInput.value = passwordOfBreachedAccount;
+
+ let saveButton = loginItem.shadowRoot.querySelector(
+ ".save-changes-button"
+ );
+ saveButton.click();
+
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ loginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]")
+ .length == 3,
+ "waiting for new login to get added to login-list"
+ );
+
+ let vulnerableLoginGuid = Cu.waiveXrays(loginItem)._login.guid;
+ let vulnerableListItem = loginList.shadowRoot.querySelector(
+ `.login-list-item[data-guid="${vulnerableLoginGuid}"]`
+ );
+
+ Assert.ok(
+ vulnerableListItem.classList.contains("vulnerable"),
+ "vulnerable login list item should be marked as such"
+ );
+ Assert.ok(
+ !loginItem.shadowRoot.querySelector(".vulnerable-alert").hidden,
+ "vulnerable alert on login-item should be visible"
+ );
+
+ Assert.ok(
+ !loginSort.namedItem("alerts").hidden,
+ "The 'alerts' option should be visible after adding a vulnerable login to the list"
+ );
+ }
+ );
+ console.log("xxxxxxx ---- 0");
+
+ tabInSecondWindow.linkedBrowser.reload();
+ await BrowserTestUtils.browserLoaded(
+ tabInSecondWindow.linkedBrowser,
+ false,
+ url => url.includes("about:logins")
+ );
+
+ console.log("xxxxxxx ---- 1");
+
+ await SpecialPowers.spawn(tabInSecondWindow.linkedBrowser, [], async () => {
+ let loginList = content.document.querySelector("login-list");
+ let loginSort = loginList.shadowRoot.querySelector("#login-sort");
+
+ await ContentTaskUtils.waitForCondition(
+ () => loginSort.value == "alerts",
+ "waiting for sort to get updated to 'alerts'"
+ );
+
+ Assert.equal(
+ loginSort.value,
+ "alerts",
+ "The login list should be sorted by Alerts"
+ );
+ let loginListItems = loginList.shadowRoot.querySelectorAll(
+ ".login-list-item[data-guid]"
+ );
+ for (let i = 1; i < loginListItems.length; i++) {
+ if (loginListItems[i].matches(".vulnerable, .breached")) {
+ Assert.ok(
+ loginListItems[i - 1].matches(".vulnerable, .breached"),
+ `The previous login-list-item must be vulnerable or breached if the current one is (second window, i=${i})`
+ );
+ }
+ }
+ });
+ console.log("xxxxxxx ---- 2");
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [ORIGIN_FOR_NEW_VULNERABLE_LOGIN],
+ async originForNewVulnerableLogin => {
+ let loginList = Cu.waiveXrays(
+ content.document.querySelector("login-list")
+ );
+ let vulnerableListItem;
+ await ContentTaskUtils.waitForCondition(() => {
+ let entry = Object.entries(loginList._logins).find(
+ ([guid, { login, listItem }]) =>
+ login.origin == originForNewVulnerableLogin
+ );
+ vulnerableListItem = entry[1].listItem;
+ return !!entry;
+ }, "waiting for vulnerable list item to get added to login-list");
+ Assert.ok(
+ vulnerableListItem.classList.contains("vulnerable"),
+ "vulnerable login list item should be marked as such"
+ );
+
+ Assert.ok(
+ !loginList.shadowRoot.querySelector("#login-sort").namedItem("alerts")
+ .hidden,
+ "The 'alerts' option should be visible after adding a vulnerable login to the list"
+ );
+ }
+ );
+ gBrowser.selectedBrowser.reload();
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, url =>
+ url.includes("about:logins")
+ );
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
+ await ContentTaskUtils.waitForCondition(
+ () => loginList.shadowRoot.querySelector("#login-sort").value == "alerts",
+ "waiting for sort to get updated to 'alerts'"
+ );
+ let loginListItems = loginList.shadowRoot.querySelectorAll(
+ ".login-list-item[data-guid]"
+ );
+ for (let i = 1; i < loginListItems.length; i++) {
+ if (loginListItems[i].matches(".vulnerable, .breached")) {
+ Assert.ok(
+ loginListItems[i - 1].matches(".vulnerable, .breached"),
+ `The previous login-list-item must be vulnerable or breached if the current one is (first window, i=${i})`
+ );
+ }
+ }
+ });
+});
diff --git a/browser/components/aboutlogins/tests/browser/head.js b/browser/components/aboutlogins/tests/browser/head.js
new file mode 100644
index 0000000000..2aec0e632a
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/head.js
@@ -0,0 +1,225 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let { LoginBreaches } = ChromeUtils.importESModule(
+ "resource:///modules/LoginBreaches.sys.mjs"
+);
+let { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+let { _AboutLogins } = ChromeUtils.importESModule(
+ "resource:///actors/AboutLoginsParent.sys.mjs"
+);
+let { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
+);
+var { LoginTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+);
+
+let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+
+let TEST_LOGIN1 = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "user1",
+ "pass1",
+ "username",
+ "password"
+);
+let TEST_LOGIN2 = new nsLoginInfo(
+ "https://2.example.com",
+ "https://2.example.com",
+ null,
+ "user2",
+ "pass2",
+ "username",
+ "password"
+);
+
+let TEST_LOGIN3 = new nsLoginInfo(
+ "https://breached.example.com",
+ "https://breached.example.com",
+ null,
+ "breachedLogin1",
+ "pass3",
+ "breachedLogin",
+ "password"
+);
+TEST_LOGIN3.QueryInterface(Ci.nsILoginMetaInfo).timePasswordChanged = 123456;
+
+async function addLogin(login) {
+ const result = await Services.logins.addLoginAsync(login);
+ registerCleanupFunction(() => {
+ let matchData = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag2
+ );
+ matchData.setPropertyAsAUTF8String("guid", result.guid);
+
+ let logins = Services.logins.searchLogins(matchData);
+ if (!logins.length) {
+ return;
+ }
+ // Use the login that was returned from searchLogins
+ // in case the initial login object was changed by the test code,
+ // since removeLogin makes sure that the login argument exactly
+ // matches the login that it will be removing.
+ Services.logins.removeLogin(logins[0]);
+ });
+ return result;
+}
+
+let EXPECTED_BREACH = null;
+let EXPECTED_ERROR_MESSAGE = null;
+add_setup(async function setup_head() {
+ const db = RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).db;
+ if (EXPECTED_BREACH) {
+ await db.create(EXPECTED_BREACH, {
+ useRecordId: true,
+ });
+ }
+ await db.importChanges({}, Date.now());
+ if (EXPECTED_BREACH) {
+ await RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).emit(
+ "sync",
+ { data: { current: [EXPECTED_BREACH] } }
+ );
+ }
+
+ SpecialPowers.registerConsoleListener(function onConsoleMessage(msg) {
+ if (msg.isWarning || !msg.errorMessage) {
+ // Ignore warnings and non-errors.
+ return;
+ }
+
+ if (msg.errorMessage.includes('Unknown event: ["jsonfile", "load"')) {
+ // Ignore telemetry errors from JSONFile.sys.mjs.
+ return;
+ }
+
+ if (
+ msg.errorMessage == "Refreshing device list failed." ||
+ msg.errorMessage == "Skipping device list refresh; not signed in"
+ ) {
+ // Ignore errors from browser-sync.js.
+ return;
+ }
+ if (
+ msg.errorMessage.includes(
+ "ReferenceError: MigrationWizard is not defined"
+ )
+ ) {
+ // todo(Bug 1587237): Ignore error when loading the Migration Wizard in automation.
+ return;
+ }
+ if (
+ msg.errorMessage.includes("Error detecting Chrome profiles") ||
+ msg.errorMessage.includes(
+ "Library/Application Support/Chromium/Local State (No such file or directory)"
+ ) ||
+ msg.errorMessage.includes(
+ "Library/Application Support/Google/Chrome/Local State (No such file or directory)"
+ )
+ ) {
+ // Ignore errors that can occur when the migrator is looking for a
+ // Chrome/Chromium profile
+ return;
+ }
+ if (msg.errorMessage.includes("Can't find profile directory.")) {
+ // Ignore error messages for no profile found in old XULStore.jsm
+ return;
+ }
+ if (msg.errorMessage.includes("Error reading typed URL history")) {
+ // The Migrator when opened can log this exception if there is no Edge
+ // history on the machine.
+ return;
+ }
+ if (msg.errorMessage.includes(EXPECTED_ERROR_MESSAGE)) {
+ return;
+ }
+ if (msg.errorMessage == "FILE_FORMAT_ERROR") {
+ // Ignore errors handled by the error message dialog.
+ return;
+ }
+ if (
+ msg.errorMessage ==
+ "NotFoundError: No such JSWindowActor 'MarionetteEvents'"
+ ) {
+ // Ignore MarionetteEvents error (Bug 1730837, Bug 1710079).
+ return;
+ }
+ Assert.ok(false, msg.message || msg.errorMessage);
+ });
+
+ registerCleanupFunction(async () => {
+ EXPECTED_ERROR_MESSAGE = null;
+ await db.clear();
+ Services.telemetry.clearEvents();
+ SpecialPowers.postConsoleSentinel();
+ });
+});
+
+/**
+ * Waits for the primary password prompt and performs an action.
+ * @param {string} action Set to "authenticate" to log in or "cancel" to
+ * close the dialog without logging in.
+ */
+function waitForMPDialog(action, aWindow = window) {
+ const BRAND_BUNDLE = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+ const BRAND_FULL_NAME = BRAND_BUNDLE.GetStringFromName("brandFullName");
+ let dialogShown = TestUtils.topicObserved("common-dialog-loaded");
+ return dialogShown.then(function ([subject]) {
+ let dialog = subject.Dialog;
+ let expected = "Password Required - " + BRAND_FULL_NAME;
+ Assert.equal(
+ dialog.args.title,
+ expected,
+ "Dialog is the Primary Password dialog"
+ );
+ if (action == "authenticate") {
+ SpecialPowers.wrap(dialog.ui.password1Textbox).setUserInput(
+ LoginTestUtils.primaryPassword.primaryPassword
+ );
+ dialog.ui.button0.click();
+ } else if (action == "cancel") {
+ dialog.ui.button1.click();
+ }
+ return BrowserTestUtils.waitForEvent(aWindow, "DOMModalDialogClosed");
+ });
+}
+
+/**
+ * Allows for tests to reset the MP auth expiration and
+ * return a promise that will resolve after the MP dialog has
+ * been presented.
+ *
+ * @param {string} action Set to "authenticate" to log in or "cancel" to
+ * close the dialog without logging in.
+ * @returns {Promise} Resolves after the MP dialog has been presented and actioned upon
+ */
+function forceAuthTimeoutAndWaitForMPDialog(action, aWindow = window) {
+ const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes (duplicated from AboutLoginsParent.sys.mjs)
+ _AboutLogins._authExpirationTime -= AUTH_TIMEOUT_MS + 1;
+ return waitForMPDialog(action, aWindow);
+}
+
+/**
+ * Allows for tests to reset the OS auth expiration and
+ * return a promise that will resolve after the OS auth dialog has
+ * been presented.
+ *
+ * @param {bool} loginResult True if the auth prompt should pass, otherwise false will fail
+ * @returns {Promise} Resolves after the OS auth dialog has been presented
+ */
+function forceAuthTimeoutAndWaitForOSKeyStoreLogin({ loginResult }) {
+ const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes (duplicated from AboutLoginsParent.sys.mjs)
+ _AboutLogins._authExpirationTime -= AUTH_TIMEOUT_MS + 1;
+ return OSKeyStoreTestUtils.waitForOSKeyStoreLogin(loginResult);
+}