diff options
Diffstat (limited to 'toolkit/components/passwordmgr/test/browser/browser_context_menu.js')
-rw-r--r-- | toolkit/components/passwordmgr/test/browser/browser_context_menu.js | 678 |
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..5f97a91324 --- /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-password", + "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", + }), + ]; +} |