/* * Test the password manager context menu. */ /* eslint no-shadow:"off" */ "use strict"; // The hostname for the test URIs. const TEST_HOSTNAME = "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(function* test_initialize() { Services.prefs.setBoolPref("signon.autofillForms", false); registerCleanupFunction(() => { Services.prefs.clearUserPref("signon.autofillForms"); Services.prefs.clearUserPref("signon.schemeUpgrades"); }); for (let login of loginList()) { Services.logins.addLogin(login); } }); /** * Check if the context menu is populated with the right * menuitems for the target password input field. */ add_task(function* test_context_menu_populate_password_noSchemeUpgrades() { Services.prefs.setBoolPref("signon.schemeUpgrades", false); yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, }, function* (browser) { yield openPasswordContextMenu(browser, "#test-password-1"); // Check the content of the password manager popup let popupMenu = document.getElementById("fill-login-popup"); checkMenu(popupMenu, 2); CONTEXT_MENU.hidePopup(); }); }); /** * Check if the context menu is populated with the right * menuitems for the target password input field. */ add_task(function* test_context_menu_populate_password_schemeUpgrades() { Services.prefs.setBoolPref("signon.schemeUpgrades", true); yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, }, function* (browser) { yield openPasswordContextMenu(browser, "#test-password-1"); // Check the content of the password manager popup let popupMenu = document.getElementById("fill-login-popup"); checkMenu(popupMenu, 3); CONTEXT_MENU.hidePopup(); }); }); /** * Check if the context menu is populated with the right menuitems * for the target username field with a password field present. */ add_task(function* test_context_menu_populate_username_with_password_noSchemeUpgrades() { Services.prefs.setBoolPref("signon.schemeUpgrades", false); yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_HOSTNAME + "/browser/toolkit/components/" + "passwordmgr/test/browser/multiple_forms.html", }, function* (browser) { yield openPasswordContextMenu(browser, "#test-username-2"); // Check the content of the password manager popup let popupMenu = document.getElementById("fill-login-popup"); checkMenu(popupMenu, 2); CONTEXT_MENU.hidePopup(); }); }); /** * Check if the context menu is populated with the right menuitems * for the target username field with a password field present. */ add_task(function* test_context_menu_populate_username_with_password_schemeUpgrades() { Services.prefs.setBoolPref("signon.schemeUpgrades", true); yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_HOSTNAME + "/browser/toolkit/components/" + "passwordmgr/test/browser/multiple_forms.html", }, function* (browser) { yield openPasswordContextMenu(browser, "#test-username-2"); // Check the content of the password manager popup let popupMenu = document.getElementById("fill-login-popup"); checkMenu(popupMenu, 3); CONTEXT_MENU.hidePopup(); }); }); /** * Check if the password field is correctly filled when one * login menuitem is clicked. */ add_task(function* test_context_menu_password_fill() { Services.prefs.setBoolPref("signon.schemeUpgrades", true); yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, }, function* (browser) { let formDescriptions = yield ContentTask.spawn(browser, {}, 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 = yield ContentTask.spawn(browser, {description}, function*({description}) { let formElement = content.document.querySelector(`[description="${description}"]`); let passwords = Array.from(formElement.querySelectorAll("input[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 username input element. yield openPasswordContextMenu(browser, "#" + inputId, function*() { let inputDisabled = yield ContentTask .spawn(browser, {inputId}, 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."); CONTEXT_MENU.hidePopup(); } return !inputDisabled; }); if (CONTEXT_MENU.state != "open") { continue; } // The only field affected by the password fill // should be the target password field itself. yield assertContextMenuFill(browser, description, null, inputId, 1); yield ContentTask.spawn(browser, {inputId}, function*({inputId}) { let passwordField = content.document.getElementById(inputId); Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used"); }); CONTEXT_MENU.hidePopup(); } } }); }); /** * Check if the form is correctly filled when one * username context menu login menuitem is clicked. */ add_task(function* test_context_menu_username_login_fill() { Services.prefs.setBoolPref("signon.schemeUpgrades", true); yield BrowserTestUtils.withNewTab({ gBrowser, url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, }, function* (browser) { let formDescriptions = yield ContentTask.spawn(browser, {}, 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 = yield ContentTask .spawn(browser, {description}, function*({description}) { let formElement = content.document.querySelector(`[description="${description}"]`); let inputs = Array.from(formElement.querySelectorAll("input[type='text']")); 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. yield openPasswordContextMenu(browser, "#" + inputId, function*() { let headerHidden = POPUP_HEADER.hidden; let headerDisabled = POPUP_HEADER.disabled; let data = {description, inputId, headerHidden, headerDisabled}; let shouldContinue = yield ContentTask.spawn(browser, data, function*(data) { let {description, inputId, headerHidden, headerDisabled} = 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']"); // 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) { 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; } return true; }); if (!shouldContinue) { CONTEXT_MENU.hidePopup(); } return shouldContinue; }); if (CONTEXT_MENU.state != "open") { continue; } let passwordFieldId = yield ContentTask .spawn(browser, {description}, function*({description}) { let formElement = content.document.querySelector(`[description="${description}"]`); return formElement.querySelector("input[type='password']").id; }); // We shouldn't change any field that's not the target username field or the first password field yield assertContextMenuFill(browser, description, inputId, passwordFieldId, 1); yield ContentTask.spawn(browser, {passwordFieldId}, function*({passwordFieldId}) { let passwordField = content.document.getElementById(passwordFieldId); if (!passwordField.hasAttribute("expectedFail")) { Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used"); } }); CONTEXT_MENU.hidePopup(); } } }); }); /** * Synthesize mouse clicks to open the password manager context menu popup * for a target password input element. * * assertCallback should return true if we should continue or else false. */ function* openPasswordContextMenu(browser, passwordInput, assertCallback = null) { // Synthesize a right mouse click over the password input element. let contextMenuShownPromise = BrowserTestUtils.waitForEvent(CONTEXT_MENU, "popupshown"); let eventDetails = {type: "contextmenu", button: 2}; BrowserTestUtils.synthesizeMouseAtCenter(passwordInput, eventDetails, browser); yield contextMenuShownPromise; if (assertCallback) { let shouldContinue = yield assertCallback(); if (!shouldContinue) { return; } } // Synthesize a mouse click over the fill login menu header. let popupShownPromise = BrowserTestUtils.waitForEvent(POPUP_HEADER, "popupshown"); EventUtils.synthesizeMouseAtCenter(POPUP_HEADER, {}); yield popupShownPromise; } /** * Verify that only the expected form fields are filled. */ 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})`; } yield ContentTask.spawn(browser, {unchangedSelector}, 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, function*(data) { let {username, password, usernameFieldId, passwordFieldId, formId, unchangedSelector} = data; let form = content.document.querySelector(`[description="${formId}"]`); yield 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 hostname 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.hostname, TEST_HOSTNAME, { 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/hostname 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({ hostname: "https://example.com", formSubmitURL: "https://example.com", username: "username", password: "password", }), // Same as above but HTTP in order to test de-duping. LoginTestUtils.testData.formLogin({ hostname: "http://example.com", formSubmitURL: "http://example.com", username: "username", password: "password", }), LoginTestUtils.testData.formLogin({ hostname: "http://example.com", formSubmitURL: "http://example.com", username: "username1", password: "password1", }), LoginTestUtils.testData.formLogin({ hostname: "https://example.com", formSubmitURL: "https://example.com", username: "username2", password: "password2", }), LoginTestUtils.testData.formLogin({ hostname: "http://example.org", formSubmitURL: "http://example.org", username: "username-cross-origin", password: "password-cross-origin", }), ]; }