From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../tests/browser/AboutLoginsTestUtils.sys.mjs | 107 ++++ .../aboutlogins/tests/browser/browser.ini | 58 ++ .../browser_aaa_eventTelemetry_run_first.js | 271 ++++++++ .../browser_alertDismissedAfterChangingPassword.js | 227 +++++++ .../browser_breachAlertShowingForAddedLogin.js | 123 ++++ .../tests/browser/browser_confirmDeleteDialog.js | 128 ++++ .../tests/browser/browser_contextmenuFillLogins.js | 185 ++++++ .../tests/browser/browser_copyToClipboardButton.js | 118 ++++ .../tests/browser/browser_createLogin.js | 535 ++++++++++++++++ .../tests/browser/browser_deleteLogin.js | 182 ++++++ .../tests/browser/browser_fxAccounts.js | 96 +++ .../tests/browser/browser_loginFilter.js | 60 ++ .../tests/browser/browser_loginItemErrors.js | 153 +++++ .../tests/browser/browser_loginListChanges.js | 144 +++++ .../browser/browser_loginSortOrderRestored.js | 172 +++++ .../tests/browser/browser_noLoginsView.js | 199 ++++++ .../tests/browser/browser_openExport.js | 149 +++++ .../tests/browser/browser_openFiltered.js | 295 +++++++++ .../tests/browser/browser_openImport.js | 60 ++ .../tests/browser/browser_openImportCSV.js | 411 ++++++++++++ .../tests/browser/browser_openPreferences.js | 82 +++ .../browser/browser_openPreferencesExternal.js | 65 ++ .../aboutlogins/tests/browser/browser_openSite.js | 94 +++ .../tests/browser/browser_osAuthDialog.js | 165 +++++ .../tests/browser/browser_primaryPassword.js | 282 +++++++++ .../tests/browser/browser_removeAllDialog.js | 555 ++++++++++++++++ .../tests/browser/browser_sessionRestore.js | 62 ++ .../aboutlogins/tests/browser/browser_tabKeyNav.js | 276 ++++++++ .../tests/browser/browser_updateLogin.js | 421 +++++++++++++ ...rowser_vulnerableLoginAddedInSecondaryWindow.js | 223 +++++++ .../components/aboutlogins/tests/browser/head.js | 225 +++++++ .../aboutlogins/tests/chrome/.eslintrc.js | 16 + .../aboutlogins/tests/chrome/aboutlogins_common.js | 97 +++ .../components/aboutlogins/tests/chrome/chrome.ini | 13 + .../tests/chrome/test_confirm_delete_dialog.html | 127 ++++ .../tests/chrome/test_fxaccounts_button.html | 96 +++ .../tests/chrome/test_login_filter.html | 178 ++++++ .../aboutlogins/tests/chrome/test_login_item.html | 481 ++++++++++++++ .../aboutlogins/tests/chrome/test_login_list.html | 697 +++++++++++++++++++++ .../aboutlogins/tests/chrome/test_menu_button.html | 260 ++++++++ browser/components/aboutlogins/tests/unit/head.js | 22 + .../unit/test_getPotentialBreachesByLoginGUID.js | 327 ++++++++++ .../components/aboutlogins/tests/unit/xpcshell.ini | 7 + 43 files changed, 8444 insertions(+) create mode 100644 browser/components/aboutlogins/tests/browser/AboutLoginsTestUtils.sys.mjs create mode 100644 browser/components/aboutlogins/tests/browser/browser.ini create mode 100644 browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_breachAlertShowingForAddedLogin.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_confirmDeleteDialog.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_createLogin.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_deleteLogin.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_fxAccounts.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_loginFilter.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_loginListChanges.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_loginSortOrderRestored.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_noLoginsView.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_openExport.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_openFiltered.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_openImport.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_openImportCSV.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_openPreferences.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_openPreferencesExternal.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_openSite.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_primaryPassword.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_sessionRestore.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_updateLogin.js create mode 100644 browser/components/aboutlogins/tests/browser/browser_vulnerableLoginAddedInSecondaryWindow.js create mode 100644 browser/components/aboutlogins/tests/browser/head.js create mode 100644 browser/components/aboutlogins/tests/chrome/.eslintrc.js create mode 100644 browser/components/aboutlogins/tests/chrome/aboutlogins_common.js create mode 100644 browser/components/aboutlogins/tests/chrome/chrome.ini create mode 100644 browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html create mode 100644 browser/components/aboutlogins/tests/chrome/test_fxaccounts_button.html create mode 100644 browser/components/aboutlogins/tests/chrome/test_login_filter.html create mode 100644 browser/components/aboutlogins/tests/chrome/test_login_item.html create mode 100644 browser/components/aboutlogins/tests/chrome/test_login_list.html create mode 100644 browser/components/aboutlogins/tests/chrome/test_menu_button.html create mode 100644 browser/components/aboutlogins/tests/unit/head.js create mode 100644 browser/components/aboutlogins/tests/unit/test_getPotentialBreachesByLoginGUID.js create mode 100644 browser/components/aboutlogins/tests/unit/xpcshell.ini (limited to 'browser/components/aboutlogins/tests') 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} 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} 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} 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 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} 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} 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} 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} 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); +} diff --git a/browser/components/aboutlogins/tests/chrome/.eslintrc.js b/browser/components/aboutlogins/tests/chrome/.eslintrc.js new file mode 100644 index 0000000000..9b6510bdd2 --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/.eslintrc.js @@ -0,0 +1,16 @@ +/* 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"; + +module.exports = { + overrides: [ + { + files: ["test_login_item.html"], + parserOptions: { + sourceType: "module", + }, + }, + ], +}; diff --git a/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js b/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js new file mode 100644 index 0000000000..d24c962da0 --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js @@ -0,0 +1,97 @@ +"use strict"; + +/* exported asyncElementRendered, importDependencies */ + +/** + * A helper to await on while waiting for an asynchronous rendering of a Custom + * Element. + * @returns {Promise} + */ +function asyncElementRendered() { + return Promise.resolve(); +} + +/** + * Import the templates from the real page to avoid duplication in the tests. + * @param {HTMLIFrameElement} templateFrame - Frame to copy the resources from + * @param {HTMLElement} destinationEl - Where to append the copied resources + */ +function importDependencies(templateFrame, destinationEl) { + let promises = []; + for (let template of templateFrame.contentDocument.querySelectorAll( + "template" + )) { + let imported = document.importNode(template, true); + destinationEl.appendChild(imported); + // Preload the styles in the actual page, to ensure they're loaded on time. + for (let element of imported.content.querySelectorAll( + "link[rel='stylesheet']" + )) { + let clone = element.cloneNode(true); + promises.push( + new Promise(resolve => { + clone.onload = function () { + resolve(); + clone.remove(); + }; + }) + ); + destinationEl.appendChild(clone); + } + } + return Promise.all(promises); +} + +Object.defineProperty(document, "l10n", { + configurable: true, + writable: true, + value: { + connectRoot() {}, + translateElements() { + return Promise.resolve(); + }, + getAttributes(element) { + return { + id: element.getAttribute("data-l10n-id"), + args: element.getAttribute("data-l10n-args") + ? JSON.parse(element.getAttribute("data-l10n-args")) + : {}, + }; + }, + setAttributes(element, id, args) { + element.setAttribute("data-l10n-id", id); + if (args) { + element.setAttribute("data-l10n-args", JSON.stringify(args)); + } else { + element.removeAttribute("data-l10n-args"); + } + }, + }, +}); + +Object.defineProperty(window, "AboutLoginsUtils", { + configurable: true, + writable: true, + value: { + getLoginOrigin(uriString) { + return uriString; + }, + setFocus(element) { + return element.focus(); + }, + async promptForPrimaryPassword(resolve, messageId) { + resolve(true); + }, + doLoginsMatch(login1, login2) { + return ( + login1.origin == login2.origin && + login1.username == login2.username && + login1.password == login2.password + ); + }, + fileImportEnabled: SpecialPowers.getBoolPref( + "signon.management.page.fileImport.enabled" + ), + primaryPasswordEnabled: false, + }, +}); diff --git a/browser/components/aboutlogins/tests/chrome/chrome.ini b/browser/components/aboutlogins/tests/chrome/chrome.ini new file mode 100644 index 0000000000..ac1ba7076c --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/chrome.ini @@ -0,0 +1,13 @@ +[DEFAULT] +scheme = https +prefs = + identity.fxaccounts.enabled=true +support-files = + aboutlogins_common.js + +[test_confirm_delete_dialog.html] +[test_fxaccounts_button.html] +[test_login_filter.html] +[test_login_item.html] +[test_login_list.html] +[test_menu_button.html] diff --git a/browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html b/browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html new file mode 100644 index 0000000000..68a58aee4f --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html @@ -0,0 +1,127 @@ + + + + + + Test the confirmation-dialog component + + + + + + + + +

+

+ +
+
+ + + diff --git a/browser/components/aboutlogins/tests/chrome/test_fxaccounts_button.html b/browser/components/aboutlogins/tests/chrome/test_fxaccounts_button.html new file mode 100644 index 0000000000..ce6046bf2a --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/test_fxaccounts_button.html @@ -0,0 +1,96 @@ + + + + + + Test the fxaccounts-button component + + + + + + + + +

+

+ +
+
+ + + + diff --git a/browser/components/aboutlogins/tests/chrome/test_login_filter.html b/browser/components/aboutlogins/tests/chrome/test_login_filter.html new file mode 100644 index 0000000000..00e0a96a51 --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/test_login_filter.html @@ -0,0 +1,178 @@ + + + + + + Test the login-filter component + + + + + + + + + +

+

+ +
+
+ + + + diff --git a/browser/components/aboutlogins/tests/chrome/test_login_item.html b/browser/components/aboutlogins/tests/chrome/test_login_item.html new file mode 100644 index 0000000000..a7946a0618 --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/test_login_item.html @@ -0,0 +1,481 @@ + + + + + + Test the login-item component + + + + + + + + + + +

+

+ +
+
+ + + + diff --git a/browser/components/aboutlogins/tests/chrome/test_login_list.html b/browser/components/aboutlogins/tests/chrome/test_login_list.html new file mode 100644 index 0000000000..98342978fb --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/test_login_list.html @@ -0,0 +1,697 @@ + + + + + + Test the login-list component + + + + + + + + +

+

+ +
+
+ + + + diff --git a/browser/components/aboutlogins/tests/chrome/test_menu_button.html b/browser/components/aboutlogins/tests/chrome/test_menu_button.html new file mode 100644 index 0000000000..2beede09f1 --- /dev/null +++ b/browser/components/aboutlogins/tests/chrome/test_menu_button.html @@ -0,0 +1,260 @@ + + + + + + Test the menu-button component + + + + + + + + +

+

+ +
+
+ + + + diff --git a/browser/components/aboutlogins/tests/unit/head.js b/browser/components/aboutlogins/tests/unit/head.js new file mode 100644 index 0000000000..938e06e3c0 --- /dev/null +++ b/browser/components/aboutlogins/tests/unit/head.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { LoginTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" +); +const { LoginHelper } = ChromeUtils.importESModule( + "resource://gre/modules/LoginHelper.sys.mjs" +); + +const TestData = LoginTestUtils.testData; +const newPropertyBag = LoginHelper.newPropertyBag; + +/** + * All the tests are implemented with add_task, this starts them automatically. + */ +function run_test() { + do_get_profile(); + run_next_test(); +} diff --git a/browser/components/aboutlogins/tests/unit/test_getPotentialBreachesByLoginGUID.js b/browser/components/aboutlogins/tests/unit/test_getPotentialBreachesByLoginGUID.js new file mode 100644 index 0000000000..a868572a6a --- /dev/null +++ b/browser/components/aboutlogins/tests/unit/test_getPotentialBreachesByLoginGUID.js @@ -0,0 +1,327 @@ +/** + * Test LoginBreaches.getPotentialBreachesByLoginGUID + */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +// Initializing BrowserGlue requires a profile on Windows. +do_get_profile(); + +const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService( + Ci.nsIObserver +); + +ChromeUtils.defineESModuleGetters(this, { + LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs", +}); + +const TEST_BREACHES = [ + { + AddedDate: "2018-12-20T23:56:26Z", + BreachDate: "2018-12-16", + Domain: "breached.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", + }, + { + AddedDate: "2018-12-20T23:56:26Z", + BreachDate: "2018-12-16", + Domain: "breached-subdomain.host.com", + Name: "Only a Sub-Domain was Breached", + PwnCount: 2754200, + DataClasses: ["Email addresses", "Usernames", "Passwords", "IP addresses"], + _status: "synced", + id: "047940fe-d2fd-4314-b636-b4a952ee0044", + last_modified: "1541615610052", + schema: "1541615609018", + }, + { + AddedDate: "2018-12-20T23:56:26Z", + BreachDate: "2018-12-16", + Domain: "breached-site-without-passwords.com", + Name: "Breached Site without passwords", + PwnCount: 987654, + DataClasses: ["Email addresses", "Usernames", "IP addresses"], + _status: "synced", + id: "047940fe-d2fd-4314-b636-b4a952ee0045", + last_modified: "1541615610052", + schema: "1541615609018", + }, +]; + +const CRASHING_URI_LOGIN = LoginTestUtils.testData.formLogin({ + origin: "chrome://grwatcher", + formActionOrigin: "https://www.example.com", + username: "username", + password: "password", + timePasswordChanged: new Date("2018-12-15").getTime(), +}); +const NOT_BREACHED_LOGIN = LoginTestUtils.testData.formLogin({ + origin: "https://www.example.com", + formActionOrigin: "https://www.example.com", + username: "username", + password: "password", + timePasswordChanged: new Date("2018-12-15").getTime(), +}); +const BREACHED_LOGIN = LoginTestUtils.testData.formLogin({ + origin: "https://www.breached.com", + formActionOrigin: "https://www.breached.com", + username: "username", + password: "password", + timePasswordChanged: new Date("2018-12-15").getTime(), +}); +const NOT_BREACHED_SUBDOMAIN_LOGIN = LoginTestUtils.testData.formLogin({ + origin: "https://not-breached-subdomain.host.com", + formActionOrigin: "https://not-breached-subdomain.host.com", + username: "username", + password: "password", +}); +const BREACHED_SUBDOMAIN_LOGIN = LoginTestUtils.testData.formLogin({ + origin: "https://breached-subdomain.host.com", + formActionOrigin: "https://breached-subdomain.host.com", + username: "username", + password: "password", + timePasswordChanged: new Date("2018-12-15").getTime(), +}); +const LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS = + LoginTestUtils.testData.formLogin({ + origin: "https://breached-site-without-passwords.com", + formActionOrigin: "https://breached-site-without-passwords.com", + username: "username", + password: "password", + timePasswordChanged: new Date("2018-12-15").getTime(), + }); +const LOGIN_WITH_NON_STANDARD_URI = LoginTestUtils.testData.formLogin({ + origin: "someApp://random/path/to/login", + formActionOrigin: "someApp://random/path/to/login", + username: "username", + password: "password", + timePasswordChanged: new Date("2018-12-15").getTime(), +}); + +add_task(async function test_notBreachedLogin() { + await Services.logins.addLoginAsync(NOT_BREACHED_LOGIN); + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [NOT_BREACHED_LOGIN], + TEST_BREACHES + ); + Assert.strictEqual( + breachesByLoginGUID.size, + 0, + "Should be 0 breached logins." + ); +}); + +add_task(async function test_breachedLogin() { + await Services.logins.addLoginAsync(BREACHED_LOGIN); + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [NOT_BREACHED_LOGIN, BREACHED_LOGIN], + TEST_BREACHES + ); + Assert.strictEqual( + breachesByLoginGUID.size, + 1, + "Should be 1 breached login: " + BREACHED_LOGIN.origin + ); + Assert.strictEqual( + breachesByLoginGUID.get(BREACHED_LOGIN.guid).breachAlertURL, + "https://monitor.firefox.com/breach-details/Breached?utm_source=firefox-desktop&utm_medium=referral&utm_campaign=about-logins&utm_content=about-logins", + "Breach alert link should be equal to the breachAlertURL" + ); +}); + +add_task(async function test_breachedLoginAfterCrashingUriLogin() { + await Services.logins.addLoginAsync(CRASHING_URI_LOGIN); + + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [CRASHING_URI_LOGIN, BREACHED_LOGIN], + TEST_BREACHES + ); + Assert.strictEqual( + breachesByLoginGUID.size, + 1, + "Should be 1 breached login: " + BREACHED_LOGIN.origin + ); + Assert.strictEqual( + breachesByLoginGUID.get(BREACHED_LOGIN.guid).breachAlertURL, + "https://monitor.firefox.com/breach-details/Breached?utm_source=firefox-desktop&utm_medium=referral&utm_campaign=about-logins&utm_content=about-logins", + "Breach alert link should be equal to the breachAlertURL" + ); +}); + +add_task(async function test_notBreachedSubdomain() { + await Services.logins.addLoginAsync(NOT_BREACHED_SUBDOMAIN_LOGIN); + + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [NOT_BREACHED_LOGIN, NOT_BREACHED_SUBDOMAIN_LOGIN], + TEST_BREACHES + ); + Assert.strictEqual( + breachesByLoginGUID.size, + 0, + "Should be 0 breached logins." + ); +}); + +add_task(async function test_breachedSubdomain() { + await Services.logins.addLoginAsync(BREACHED_SUBDOMAIN_LOGIN); + + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [NOT_BREACHED_SUBDOMAIN_LOGIN, BREACHED_SUBDOMAIN_LOGIN], + TEST_BREACHES + ); + Assert.strictEqual( + breachesByLoginGUID.size, + 1, + "Should be 1 breached login: " + BREACHED_SUBDOMAIN_LOGIN.origin + ); +}); + +add_task(async function test_breachedSiteWithoutPasswords() { + await Services.logins.addLoginAsync( + LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS + ); + + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS], + TEST_BREACHES + ); + Assert.strictEqual( + breachesByLoginGUID.size, + 0, + "Should be 0 breached login: " + + LOGIN_FOR_BREACHED_SITE_WITHOUT_PASSWORDS.origin + ); +}); + +add_task(async function test_breachAlertHiddenAfterDismissal() { + BREACHED_LOGIN.guid = "{d2de5ac1-4de6-e544-a7af-1f75abcba92b}"; + + await Services.logins.initializationPromise; + const storageJSON = Services.logins.wrappedJSObject._storage.wrappedJSObject; + + storageJSON.recordBreachAlertDismissal(BREACHED_LOGIN.guid); + + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [BREACHED_LOGIN, NOT_BREACHED_LOGIN], + TEST_BREACHES + ); + Assert.strictEqual( + breachesByLoginGUID.size, + 0, + "Should be 0 breached logins after dismissal: " + BREACHED_LOGIN.origin + ); + + info("Clear login storage"); + Services.logins.removeAllUserFacingLogins(); + + const breachesByLoginGUID2 = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [BREACHED_LOGIN, NOT_BREACHED_LOGIN], + TEST_BREACHES + ); + Assert.strictEqual( + breachesByLoginGUID2.size, + 1, + "Breached login should re-appear after clearing storage: " + + BREACHED_LOGIN.origin + ); +}); + +add_task(async function test_newBreachAfterDismissal() { + TEST_BREACHES[0].AddedDate = new Date().toISOString(); + + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [BREACHED_LOGIN, NOT_BREACHED_LOGIN], + TEST_BREACHES + ); + + Assert.strictEqual( + breachesByLoginGUID.size, + 1, + "Should be 1 breached login after new breach following the dismissal of a previous breach: " + + BREACHED_LOGIN.origin + ); +}); + +add_task(async function test_ExceptionsThrownByNonStandardURIsAreCaught() { + await Services.logins.addLoginAsync(LOGIN_WITH_NON_STANDARD_URI); + + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID( + [LOGIN_WITH_NON_STANDARD_URI, BREACHED_LOGIN], + TEST_BREACHES + ); + + Assert.strictEqual( + breachesByLoginGUID.size, + 1, + "Exceptions thrown by logins with non-standard URIs should be caught." + ); +}); + +add_task(async function test_setBreachesFromRemoteSettingsSync() { + const login = NOT_BREACHED_SUBDOMAIN_LOGIN; + const nowExampleIsInBreachedRecords = [ + { + AddedDate: "2018-12-20T23:56:26Z", + BreachDate: "2018-12-16", + Domain: "not-breached-subdomain.host.com", + Name: "not-breached-subdomain.host.com is now breached!", + PwnCount: 1643100, + DataClasses: [ + "Email addresses", + "Usernames", + "Passwords", + "IP addresses", + ], + _status: "synced", + id: "047940fe-d2fd-4314-b636-b4a952ee0044", + last_modified: "1541615610052", + schema: "1541615609018", + }, + ]; + async function emitSync() { + await RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).emit( + "sync", + { data: { current: nowExampleIsInBreachedRecords } } + ); + } + + const beforeSyncBreachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID([login]); + Assert.strictEqual( + beforeSyncBreachesByLoginGUID.size, + 0, + "Should be 0 breached login before not-breached-subdomain.host.com is added to fxmonitor-breaches collection and synced: " + ); + gBrowserGlue.observe(null, "browser-glue-test", "add-breaches-sync-handler"); + const db = RemoteSettings(LoginBreaches.REMOTE_SETTINGS_COLLECTION).db; + await db.importChanges({}, Date.now(), [nowExampleIsInBreachedRecords[0]]); + await emitSync(); + + const breachesByLoginGUID = + await LoginBreaches.getPotentialBreachesByLoginGUID([login]); + Assert.strictEqual( + breachesByLoginGUID.size, + 1, + "Should be 1 breached login after not-breached-subdomain.host.com is added to fxmonitor-breaches collection and synced: " + ); +}); diff --git a/browser/components/aboutlogins/tests/unit/xpcshell.ini b/browser/components/aboutlogins/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..e827d6d688 --- /dev/null +++ b/browser/components/aboutlogins/tests/unit/xpcshell.ini @@ -0,0 +1,7 @@ +[DEFAULT] +skip-if = toolkit == 'android' # bug 1730213 +head = head.js +firefox-appdir = browser + +[test_getPotentialBreachesByLoginGUID.js] +tags = remote-settings -- cgit v1.2.3