summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/test/browser/browser_context_menu.js')
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu.js678
1 files changed, 678 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
new file mode 100644
index 0000000000..63e161c003
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
@@ -0,0 +1,678 @@
+/**
+ * Test the password manager context menu.
+ */
+
+/* eslint no-shadow:"off" */
+
+"use strict";
+
+// The origin for the test URIs.
+const TEST_ORIGIN = "https://example.com";
+const MULTIPLE_FORMS_PAGE_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/multiple_forms.html";
+
+const CONTEXT_MENU = document.getElementById("contentAreaContextMenu");
+const POPUP_HEADER = document.getElementById("fill-login");
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(async function test_initialize() {
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ Services.prefs.setBoolPref("signon.usernameOnlyForm.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ Services.prefs.clearUserPref("signon.schemeUpgrades");
+ Services.prefs.clearUserPref("signon.usernameOnlyForm.enabled");
+ });
+ await Services.logins.addLogins(loginList());
+});
+
+/**
+ * Check if the context menu is populated with the right
+ * menuitems for the target password input field.
+ */
+add_task(async function test_context_menu_populate_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-password-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+});
+
+/**
+ * Check if the context menu is populated with the right
+ * menuitems for the target password input field.
+ */
+add_task(async function test_context_menu_populate_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-password-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+});
+
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field with a password field present.
+ */
+add_task(
+ async function test_context_menu_populate_username_with_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ TEST_ORIGIN +
+ "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-username-3");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+ }
+);
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field with a password field present.
+ */
+add_task(
+ async function test_context_menu_populate_username_with_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ TEST_ORIGIN +
+ "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-username-3");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+ }
+);
+
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field without a password field present.
+ */
+add_task(
+ async function test_context_menu_populate_username_with_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ TEST_ORIGIN +
+ "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-username-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+ }
+);
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field without a password field present.
+ */
+add_task(
+ async function test_context_menu_populate_username_with_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ TEST_ORIGIN +
+ "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-username-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+ }
+);
+
+/**
+ * Check if the password field is correctly filled when one
+ * login menuitem is clicked.
+ */
+add_task(async function test_context_menu_password_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ let formDescriptions = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let forms = Array.from(
+ content.document.getElementsByClassName("test-form")
+ );
+ return forms.map(f => f.getAttribute("description"));
+ }
+ );
+
+ for (let description of formDescriptions) {
+ info("Testing form: " + description);
+
+ let passwordInputIds = await SpecialPowers.spawn(
+ browser,
+ [{ description }],
+ async function ({ description }) {
+ let formElement = content.document.querySelector(
+ `[description="${description}"]`
+ );
+ let passwords = Array.from(
+ formElement.querySelectorAll(
+ "input[type='password'], input[data-type='password']"
+ )
+ );
+ return passwords.map(p => p.id);
+ }
+ );
+
+ for (let inputId of passwordInputIds) {
+ info("Testing password field: " + inputId);
+
+ // Synthesize a right mouse click over the password input element.
+ await openPasswordContextMenu(
+ browser,
+ "#" + inputId,
+ async function () {
+ let inputDisabled = await SpecialPowers.spawn(
+ browser,
+ [{ inputId }],
+ async function ({ inputId }) {
+ let input = content.document.getElementById(inputId);
+ return input.disabled || input.readOnly;
+ }
+ );
+
+ // If the password field is disabled or read-only, we want to see
+ // the disabled Fill Password popup header.
+ if (inputDisabled) {
+ Assert.ok(!POPUP_HEADER.hidden, "Popup menu is not hidden.");
+ Assert.ok(POPUP_HEADER.disabled, "Popup menu is disabled.");
+ await closePopup(CONTEXT_MENU);
+ }
+ Assert.equal(
+ POPUP_HEADER.getAttribute("data-l10n-id"),
+ "main-context-menu-use-saved-password",
+ "top-level label is correct"
+ );
+
+ return !inputDisabled;
+ }
+ );
+
+ if (CONTEXT_MENU.state != "open") {
+ continue;
+ }
+
+ // The only field affected by the password fill
+ // should be the target password field itself.
+ await assertContextMenuFill(browser, description, null, inputId, 1);
+ await SpecialPowers.spawn(
+ browser,
+ [{ inputId }],
+ async function ({ inputId }) {
+ let passwordField = content.document.getElementById(inputId);
+ Assert.equal(
+ passwordField.value,
+ "password1",
+ "Check upgraded login was actually used"
+ );
+ }
+ );
+
+ await closePopup(CONTEXT_MENU);
+ }
+ }
+ }
+ );
+});
+
+/**
+ * Check if the form is correctly filled when one
+ * username context menu login menuitem is clicked.
+ */
+add_task(async function test_context_menu_username_login_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ let formDescriptions = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let forms = Array.from(
+ content.document.getElementsByClassName("test-form")
+ );
+ return forms.map(f => f.getAttribute("description"));
+ }
+ );
+
+ for (let description of formDescriptions) {
+ info("Testing form: " + description);
+ let usernameInputIds = await SpecialPowers.spawn(
+ browser,
+ [{ description }],
+ async function ({ description }) {
+ let formElement = content.document.querySelector(
+ `[description="${description}"]`
+ );
+ let inputs = Array.from(
+ formElement.querySelectorAll(
+ "input[type='text']:not([data-type='password'])"
+ )
+ );
+ return inputs.map(p => p.id);
+ }
+ );
+
+ for (let inputId of usernameInputIds) {
+ info("Testing username field: " + inputId);
+
+ // Synthesize a right mouse click over the username input element.
+ await openPasswordContextMenu(
+ browser,
+ "#" + inputId,
+ async function () {
+ let headerHidden = POPUP_HEADER.hidden;
+ let headerDisabled = POPUP_HEADER.disabled;
+ let headerLabelID = POPUP_HEADER.getAttribute("data-l10n-id");
+
+ let data = {
+ description,
+ inputId,
+ headerHidden,
+ headerDisabled,
+ headerLabelID,
+ };
+ let shouldContinue = await SpecialPowers.spawn(
+ browser,
+ [data],
+ async function (data) {
+ let {
+ description,
+ inputId,
+ headerHidden,
+ headerDisabled,
+ headerLabelID,
+ } = data;
+ let formElement = content.document.querySelector(
+ `[description="${description}"]`
+ );
+ let usernameField = content.document.getElementById(inputId);
+ // We always want to check if the first password field is filled,
+ // since this is the current behavior from the _fillForm function.
+ let passwordField = formElement.querySelector(
+ "input[type='password'], input[data-type='password']"
+ );
+
+ // If we don't want to see the actual popup menu,
+ // check if the popup is hidden or disabled.
+ if (
+ !passwordField ||
+ usernameField.disabled ||
+ usernameField.readOnly ||
+ passwordField.disabled ||
+ passwordField.readOnly
+ ) {
+ if (!passwordField) {
+ // Should show popup for a username-only form.
+ if (usernameField.autocomplete == "username") {
+ Assert.ok(!headerHidden, "Popup menu is not hidden.");
+ } else {
+ Assert.ok(headerHidden, "Popup menu is hidden.");
+ }
+ } else {
+ Assert.ok(!headerHidden, "Popup menu is not hidden.");
+ Assert.ok(headerDisabled, "Popup menu is disabled.");
+ }
+ return false;
+ }
+ Assert.equal(
+ headerLabelID,
+ "main-context-menu-use-saved-login",
+ "top-level label is correct"
+ );
+ return true;
+ }
+ );
+
+ if (!shouldContinue) {
+ await closePopup(CONTEXT_MENU);
+ }
+
+ return shouldContinue;
+ }
+ );
+
+ if (CONTEXT_MENU.state != "open") {
+ continue;
+ }
+
+ let passwordFieldId = await SpecialPowers.spawn(
+ browser,
+ [{ description }],
+ async function ({ description }) {
+ let formElement = content.document.querySelector(
+ `[description="${description}"]`
+ );
+ return formElement.querySelector(
+ "input[type='password'], input[data-type='password']"
+ ).id;
+ }
+ );
+
+ // We shouldn't change any field that's not the target username field or the first password field
+ await assertContextMenuFill(
+ browser,
+ description,
+ inputId,
+ passwordFieldId,
+ 1
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ passwordFieldId }],
+ async function ({ passwordFieldId }) {
+ let passwordField =
+ content.document.getElementById(passwordFieldId);
+ if (!passwordField.hasAttribute("expectedFail")) {
+ Assert.equal(
+ passwordField.value,
+ "password1",
+ "Check upgraded login was actually used"
+ );
+ }
+ }
+ );
+
+ await closePopup(CONTEXT_MENU);
+ }
+ }
+ }
+ );
+});
+
+/**
+ * Check event telemetry is correctly recorded when opening the saved logins / management UI
+ * from the context menu
+ */
+add_task(async function test_context_menu_open_management() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-password-1");
+
+ let openingFunc = () => gContextMenu.openPasswordManager();
+ // wait until the management UI opens
+ let passwordManager = await openPasswordManager(openingFunc);
+ info("Management UI dialog was opened");
+
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "open_management", "contextmenu"]],
+ { category: "pwmgr", method: "open_management" },
+ { clear: true, process: "content" }
+ );
+
+ await passwordManager.close();
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+});
+
+/**
+ * Verify that only the expected form fields are filled.
+ */
+async function assertContextMenuFill(
+ browser,
+ formId,
+ usernameFieldId,
+ passwordFieldId,
+ loginIndex
+) {
+ let popupMenu = document.getElementById("fill-login-popup");
+ let unchangedSelector = `[description="${formId}"] input:not(#${passwordFieldId})`;
+
+ if (usernameFieldId) {
+ unchangedSelector += `:not(#${usernameFieldId})`;
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ unchangedSelector }],
+ async function ({ unchangedSelector }) {
+ let unchangedFields =
+ content.document.querySelectorAll(unchangedSelector);
+
+ // Store the value of fields that should remain unchanged.
+ if (unchangedFields.length) {
+ for (let field of unchangedFields) {
+ field.setAttribute("original-value", field.value);
+ }
+ }
+ }
+ );
+
+ // Execute the default command of the specified login menuitem found in the context menu.
+ let loginItem =
+ popupMenu.getElementsByClassName("context-login-item")[loginIndex];
+
+ // Find the used login by it's username (Use only unique usernames in this test).
+ let { username, password } = getLoginFromUsername(loginItem.label);
+
+ let data = {
+ username,
+ password,
+ usernameFieldId,
+ passwordFieldId,
+ formId,
+ unchangedSelector,
+ };
+ let continuePromise = ContentTask.spawn(browser, data, async function (data) {
+ let {
+ username,
+ password,
+ usernameFieldId,
+ passwordFieldId,
+ formId,
+ unchangedSelector,
+ } = data;
+ let form = content.document.querySelector(`[description="${formId}"]`);
+ await ContentTaskUtils.waitForEvent(
+ form,
+ "input",
+ "Username input value changed"
+ );
+
+ if (usernameFieldId) {
+ let usernameField = content.document.getElementById(usernameFieldId);
+
+ // If we have an username field, check if it's correctly filled
+ if (usernameField.getAttribute("expectedFail") == null) {
+ Assert.equal(
+ username,
+ usernameField.value,
+ "Username filled and correct."
+ );
+ }
+ }
+
+ if (passwordFieldId) {
+ let passwordField = content.document.getElementById(passwordFieldId);
+
+ // If we have a password field, check if it's correctly filled
+ if (passwordField && passwordField.getAttribute("expectedFail") == null) {
+ Assert.equal(
+ password,
+ passwordField.value,
+ "Password filled and correct."
+ );
+ }
+ }
+
+ let unchangedFields = content.document.querySelectorAll(unchangedSelector);
+
+ // Check that all fields that should not change have the same value as before.
+ if (unchangedFields.length) {
+ Assert.ok(() => {
+ for (let field of unchangedFields) {
+ if (field.value != field.getAttribute("original-value")) {
+ return false;
+ }
+ }
+ return true;
+ }, "Other fields were not changed.");
+ }
+ });
+
+ loginItem.doCommand();
+
+ return continuePromise;
+}
+
+/**
+ * Check if every login that matches the page origin are available at the context menu.
+ * @param {Element} contextMenu
+ * @param {Number} expectedCount - Number of logins expected in the context menu. Used to ensure
+ * we continue testing something useful.
+ */
+function checkMenu(contextMenu, expectedCount) {
+ let logins = loginList().filter(login => {
+ return LoginHelper.isOriginMatching(login.origin, TEST_ORIGIN, {
+ schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
+ });
+ });
+ // Make an array of menuitems for easier comparison.
+ let menuitems = [
+ ...CONTEXT_MENU.getElementsByClassName("context-login-item"),
+ ];
+ Assert.equal(
+ menuitems.length,
+ expectedCount,
+ "Expected number of menu items"
+ );
+ Assert.ok(
+ logins.every(l => menuitems.some(m => l.username == m.label)),
+ "Every login have an item at the menu."
+ );
+}
+
+/**
+ * Search for a login by it's username.
+ *
+ * Only unique login/origin combinations should be used at this test.
+ */
+function getLoginFromUsername(username) {
+ return loginList().find(login => login.username == username);
+}
+
+/**
+ * List of logins used for the test.
+ *
+ * We should only use unique usernames in this test,
+ * because we need to search logins by username. There is one duplicate u+p combo
+ * in order to test de-duping in the menu.
+ */
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ // Same as above but HTTP in order to test de-duping.
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.com",
+ formActionOrigin: "http://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.com",
+ formActionOrigin: "http://example.com",
+ username: "username1",
+ password: "password1",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.org",
+ formActionOrigin: "http://example.org",
+ username: "username-cross-origin",
+ password: "password-cross-origin",
+ }),
+ ];
+}