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 --- .../components/passwordmgr/test/browser/head.js | 965 +++++++++++++++++++++ 1 file changed, 965 insertions(+) create mode 100644 toolkit/components/passwordmgr/test/browser/head.js (limited to 'toolkit/components/passwordmgr/test/browser/head.js') diff --git a/toolkit/components/passwordmgr/test/browser/head.js b/toolkit/components/passwordmgr/test/browser/head.js new file mode 100644 index 0000000000..70a1f685e2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/head.js @@ -0,0 +1,965 @@ +const DIRECTORY_PATH = "/browser/toolkit/components/passwordmgr/test/browser/"; + +var { LoginTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" +); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +add_setup(async function common_initialize() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["signon.rememberSignons", true], + ["signon.testOnlyUserHasInteractedByPrefValue", true], + ["signon.testOnlyUserHasInteractedWithDocument", true], + ["toolkit.telemetry.ipcBatchTimeout", 0], + ], + }); + if (LoginHelper.relatedRealmsEnabled) { + await LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials(); + registerCleanupFunction(async function () { + await LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials(); + }); + } +}); + +registerCleanupFunction( + async function cleanup_removeAllLoginsAndResetRecipes() { + await SpecialPowers.popPrefEnv(); + + LoginTestUtils.clearData(); + LoginTestUtils.resetGeneratedPasswordsCache(); + clearHttpAuths(); + Services.telemetry.clearEvents(); + + let recipeParent = LoginTestUtils.recipes.getRecipeParent(); + if (!recipeParent) { + // No need to reset the recipes if the recipe module wasn't even loaded. + return; + } + await recipeParent.then(recipeParentResult => recipeParentResult.reset()); + + await cleanupDoorhanger(); + await cleanupPasswordNotifications(); + await closePopup(document.getElementById("contentAreaContextMenu")); + await closePopup(document.getElementById("PopupAutoComplete")); + } +); + +/** + * Compared logins in storage to expected values + * + * @param {array} expectedLogins + * An array of expected login properties + * @return {nsILoginInfo[]} - All saved logins sorted by timeCreated + */ +function verifyLogins(expectedLogins = []) { + let allLogins = Services.logins.getAllLogins(); + allLogins.sort((a, b) => a.timeCreated > b.timeCreated); + Assert.equal( + allLogins.length, + expectedLogins.length, + "Check actual number of logins matches the number of provided expected property-sets" + ); + for (let i = 0; i < expectedLogins.length; i++) { + // if the test doesn't care about comparing properties for this login, just pass false/null. + let expected = expectedLogins[i]; + if (expected) { + let login = allLogins[i]; + if (typeof expected.timesUsed !== "undefined") { + Assert.equal(login.timesUsed, expected.timesUsed, "Check timesUsed"); + } + if (typeof expected.passwordLength !== "undefined") { + Assert.equal( + login.password.length, + expected.passwordLength, + "Check passwordLength" + ); + } + if (typeof expected.username !== "undefined") { + Assert.equal(login.username, expected.username, "Check username"); + } + if (typeof expected.password !== "undefined") { + Assert.equal(login.password, expected.password, "Check password"); + } + if (typeof expected.usedSince !== "undefined") { + Assert.ok( + login.timeLastUsed > expected.usedSince, + "Check timeLastUsed" + ); + } + if (typeof expected.passwordChangedSince !== "undefined") { + Assert.ok( + login.timePasswordChanged > expected.passwordChangedSince, + "Check timePasswordChanged" + ); + } + if (typeof expected.timeCreated !== "undefined") { + Assert.equal( + login.timeCreated, + expected.timeCreated, + "Check timeCreated" + ); + } + } + } + return allLogins; +} + +/** + * Submit the content form and return a promise resolving to the username and + * password values echoed out in the response + * + * @param {Object} [browser] - browser with the form + * @param {String = ""} formAction - Optional url to set the form's action to before submitting + * @param {Object = null} selectorValues - Optional object with field values to set before form submission + * @param {Object = null} responseSelectors - Optional object with selectors to find the username and password in the response + */ +async function submitFormAndGetResults( + browser, + formAction = "", + selectorValues, + responseSelectors +) { + async function contentSubmitForm([contentFormAction, contentSelectorValues]) { + const { WrapPrivileged } = ChromeUtils.importESModule( + "resource://testing-common/WrapPrivileged.sys.mjs" + ); + let doc = content.document; + let form = doc.querySelector("form"); + if (contentFormAction) { + form.action = contentFormAction; + } + for (let [sel, value] of Object.entries(contentSelectorValues)) { + try { + let field = doc.querySelector(sel); + let gotInput = ContentTaskUtils.waitForEvent( + field, + "input", + "Got input event on " + sel + ); + // we don't get an input event if the new value == the old + field.value = "###"; + WrapPrivileged.wrap(field, this).setUserInput(value); + await gotInput; + } catch (ex) { + throw new Error( + `submitForm: Couldn't set value of field at: ${sel}: ${ex.message}` + ); + } + } + form.submit(); + } + + let loadPromise = BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn( + browser, + [[formAction, selectorValues]], + contentSubmitForm + ); + await loadPromise; + + let result = await getFormSubmitResponseResult( + browser, + formAction, + responseSelectors + ); + return result; +} + +/** + * Wait for a given result page to load and return a promise resolving to an object with the parsed-out + * username/password values from the response + * + * @param {Object} [browser] - browser which is loading this page + * @param {String} resultURL - the path or filename to look for in the content.location + * @param {Object = null} - Optional object with selectors to find the username and password in the response + */ +async function getFormSubmitResponseResult( + browser, + resultURL = "/formsubmit.sjs", + { username = "#user", password = "#pass" } = {} +) { + // default selectors are for the response page produced by formsubmit.sjs + let fieldValues = await ContentTask.spawn( + browser, + { + resultURL, + usernameSelector: username, + passwordSelector: password, + }, + async function ({ resultURL, usernameSelector, passwordSelector }) { + await ContentTaskUtils.waitForCondition(() => { + return ( + content.location.pathname.endsWith(resultURL) && + content.document.readyState == "complete" + ); + }, `Wait for form submission load (${resultURL})`); + let username = + content.document.querySelector(usernameSelector).textContent; + // Bug 1686071: Since generated passwords can have special characters in them, + // we need to unescape the characters. These special characters are automatically escaped + // when we submit a form in `submitFormAndGetResults`. + // Otherwise certain tests will intermittently fail when these special characters are present in the passwords. + let password = unescape( + content.document.querySelector(passwordSelector).textContent + ); + return { + username, + password, + }; + } + ); + return fieldValues; +} + +/** + * Loads a test page in `DIRECTORY_URL` which automatically submits to formsubmit.sjs and returns a + * promise resolving with the field values when the optional `aTaskFn` is done. + * + * @param {String} aPageFile - test page file name which auto-submits to formsubmit.sjs + * @param {Function} aTaskFn - task which can be run before the tab closes. + * @param {String} [aOrigin="https://example.com"] - origin of the server to use + * to load `aPageFile`. + */ +function testSubmittingLoginForm( + aPageFile, + aTaskFn, + aOrigin = "https://example.com" +) { + return BrowserTestUtils.withNewTab( + { + gBrowser, + url: aOrigin + DIRECTORY_PATH + aPageFile, + }, + async function (browser) { + Assert.ok(true, "loaded " + aPageFile); + let fieldValues = await getFormSubmitResponseResult( + browser, + "/formsubmit.sjs" + ); + Assert.ok(true, "form submission loaded"); + if (aTaskFn) { + await aTaskFn(fieldValues, browser); + } + return fieldValues; + } + ); +} +/** + * Loads a test page in `DIRECTORY_URL` which automatically submits to formsubmit.sjs and returns a + * promise resolving with the field values when the optional `aTaskFn` is done. + * + * @param {String} aPageFile - test page file name which auto-submits to formsubmit.sjs + * @param {Function} aTaskFn - task which can be run before the tab closes. + * @param {String} [aOrigin="http://example.com"] - origin of the server to use + * to load `aPageFile`. + */ +function testSubmittingLoginFormHTTP( + aPageFile, + aTaskFn, + aOrigin = "http://example.com" +) { + return testSubmittingLoginForm(aPageFile, aTaskFn, aOrigin); +} + +function checkOnlyLoginWasUsedTwice({ justChanged }) { + // Check to make sure we updated the timestamps and use count on the + // existing login that was submitted for the test. + let logins = Services.logins.getAllLogins(); + Assert.equal(logins.length, 1, "Should only have 1 login"); + Assert.ok(logins[0] instanceof Ci.nsILoginMetaInfo, "metainfo QI"); + Assert.equal( + logins[0].timesUsed, + 2, + "check .timesUsed for existing login submission" + ); + Assert.ok( + logins[0].timeCreated < logins[0].timeLastUsed, + "timeLastUsed bumped" + ); + if (justChanged) { + Assert.equal( + logins[0].timeLastUsed, + logins[0].timePasswordChanged, + "timeLastUsed == timePasswordChanged" + ); + } else { + Assert.equal( + logins[0].timeCreated, + logins[0].timePasswordChanged, + "timeChanged not updated" + ); + } +} + +function clearHttpAuths() { + let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.clearAll(); +} + +// Begin popup notification (doorhanger) functions // + +const REMEMBER_BUTTON = "button"; +const NEVER_MENUITEM = 0; + +const CHANGE_BUTTON = "button"; +const DONT_CHANGE_BUTTON = "secondaryButton"; +const REMOVE_LOGIN_MENUITEM = 0; + +/** + * Checks if we have a password capture popup notification + * of the right type and with the right label. + * + * @param {String} aKind The desired `passwordNotificationType` ("any" for any type) + * @param {Object} [popupNotifications = PopupNotifications] + * @param {Object} [browser = null] Optional browser whose notifications should be searched. + * @return the found password popup notification. + */ +function getCaptureDoorhanger( + aKind, + popupNotifications = PopupNotifications, + browser = null +) { + Assert.ok(true, "Looking for " + aKind + " popup notification"); + let notification = popupNotifications.getNotification("password", browser); + if (!aKind) { + throw new Error( + "getCaptureDoorhanger needs aKind to be a non-empty string" + ); + } + if (aKind !== "any" && notification) { + Assert.equal( + notification.options.passwordNotificationType, + aKind, + "Notification type matches." + ); + if (aKind == "password-change") { + Assert.equal( + notification.mainAction.label, + "Update", + "Main action label matches update doorhanger." + ); + } else if (aKind == "password-save") { + Assert.equal( + notification.mainAction.label, + "Save", + "Main action label matches save doorhanger." + ); + } + } + return notification; +} + +async function getCaptureDoorhangerThatMayOpen( + aKind, + popupNotifications = PopupNotifications, + browser = null +) { + let notif = getCaptureDoorhanger(aKind, popupNotifications, browser); + if (notif && !notif.dismissed) { + if (popupNotifications.panel.state !== "open") { + await BrowserTestUtils.waitForEvent( + popupNotifications.panel, + "popupshown" + ); + } + } + return notif; +} + +async function waitForDoorhanger(browser, type) { + let notif; + await TestUtils.waitForCondition(() => { + notif = PopupNotifications.getNotification("password", browser); + if (notif && type !== "any") { + return ( + notif.options.passwordNotificationType == type && + notif.anchorElement && + BrowserTestUtils.is_visible(notif.anchorElement) + ); + } + return notif; + }, `Waiting for a ${type} notification`); + return notif; +} + +async function hideDoorhangerPopup() { + info("hideDoorhangerPopup"); + if (!PopupNotifications.isPanelOpen) { + return; + } + let { panel } = PopupNotifications; + let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + panel.hidePopup(); + await promiseHidden; + info("got popuphidden from notification panel"); +} + +function getDoorhangerButton(aPopup, aButtonIndex) { + let notifications = aPopup.owner.panel.children; + Assert.ok(!!notifications.length, "at least one notification displayed"); + Assert.ok(true, notifications.length + " notification(s)"); + let notification = notifications[0]; + + if (aButtonIndex == "button") { + return notification.button; + } else if (aButtonIndex == "secondaryButton") { + return notification.secondaryButton; + } + return notification.menupopup.querySelectorAll("menuitem")[aButtonIndex]; +} + +/** + * Clicks the specified popup notification button. + * + * @param {Element} aPopup Popup Notification element + * @param {Number} aButtonIndex Number indicating which button to click. + * See the constants in this file. + */ +function clickDoorhangerButton(aPopup, aButtonIndex) { + Assert.ok(true, "Looking for action at index " + aButtonIndex); + + let button = getDoorhangerButton(aPopup, aButtonIndex); + if (aButtonIndex == "button") { + Assert.ok(true, "Triggering main action"); + } else if (aButtonIndex == "secondaryButton") { + Assert.ok(true, "Triggering secondary action"); + } else { + Assert.ok(true, "Triggering menuitem # " + aButtonIndex); + } + button.doCommand(); +} + +async function cleanupDoorhanger(notif) { + let PN = notif ? notif.owner : PopupNotifications; + if (notif) { + notif.remove(); + } + let promiseHidden = PN.isPanelOpen + ? BrowserTestUtils.waitForEvent(PN.panel, "popuphidden") + : Promise.resolve(); + PN.panel.hidePopup(); + await promiseHidden; +} + +async function cleanupPasswordNotifications( + popupNotifications = PopupNotifications +) { + let notif; + while ((notif = popupNotifications.getNotification("password"))) { + notif.remove(); + } +} + +async function clearMessageCache(browser) { + await SpecialPowers.spawn(browser, [], async () => { + const { LoginManagerChild } = ChromeUtils.importESModule( + "resource://gre/modules/LoginManagerChild.sys.mjs" + ); + let docState = LoginManagerChild.forWindow(content).stateForDocument( + content.document + ); + docState.lastSubmittedValuesByRootElement = new content.WeakMap(); + }); +} + +/** + * Checks the doorhanger's username and password. + * + * @param {String} username The username. + * @param {String} password The password. + */ +async function checkDoorhangerUsernamePassword(username, password) { + await BrowserTestUtils.waitForCondition(() => { + return ( + document.getElementById("password-notification-username").value == + username && + document.getElementById("password-notification-password").value == + password + ); + }, "Wait for nsLoginManagerPrompter writeDataToUI() to update to the correct username/password values"); +} + +/** + * Change the doorhanger's username and password input values. + * + * @param {object} newValues + * named values to update + * @param {string} [newValues.password = undefined] + * An optional string value to replace whatever is in the password field + * @param {string} [newValues.username = undefined] + * An optional string value to replace whatever is in the username field + * @param {Object} [popupNotifications = PopupNotifications] + */ +async function updateDoorhangerInputValues( + newValues, + popupNotifications = PopupNotifications +) { + let { panel } = popupNotifications; + if (popupNotifications.panel.state !== "open") { + await BrowserTestUtils.waitForEvent(popupNotifications.panel, "popupshown"); + } + Assert.equal(panel.state, "open", "Check the doorhanger is already open"); + + let notifElem = panel.childNodes[0]; + + // Note: setUserInput does not reliably dispatch input events from chrome elements? + async function setInputValue(target, value) { + info(`setInputValue: on target: ${target.id}, value: ${value}`); + target.focus(); + target.select(); + info( + `setInputValue: current value: '${target.value}', setting new value '${value}'` + ); + await EventUtils.synthesizeKey("KEY_Backspace"); + await EventUtils.sendString(value); + await EventUtils.synthesizeKey("KEY_Tab"); + return Promise.resolve(); + } + + let passwordField = notifElem.querySelector( + "#password-notification-password" + ); + let usernameField = notifElem.querySelector( + "#password-notification-username" + ); + + if (typeof newValues.password !== "undefined") { + if (passwordField.value !== newValues.password) { + await setInputValue(passwordField, newValues.password); + } + } + if (typeof newValues.username !== "undefined") { + if (usernameField.value !== newValues.username) { + await setInputValue(usernameField, newValues.username); + } + } +} + +/** + * Open doorhanger autocomplete popup and select a username value. + * + * @param {string} text the text value of the username that should be selected. + * Noop if `text` is falsy. + */ +async function selectDoorhangerUsername(text) { + await _selectDoorhanger( + text, + "#password-notification-username", + "#password-notification-username-dropmarker" + ); +} + +/** + * Open doorhanger autocomplete popup and select a password value. + * + * @param {string} text the text value of the password that should be selected. + * Noop if `text` is falsy. + */ +async function selectDoorhangerPassword(text) { + await _selectDoorhanger( + text, + "#password-notification-password", + "#password-notification-password-dropmarker" + ); +} + +async function _selectDoorhanger(text, inputSelector, dropmarkerSelector) { + if (!text) { + return; + } + + info("Opening doorhanger suggestion popup"); + + let doorhangerPopup = document.getElementById("password-notification"); + let dropmarker = doorhangerPopup.querySelector(dropmarkerSelector); + + let autocompletePopup = document.getElementById("PopupAutoComplete"); + let popupShown = BrowserTestUtils.waitForEvent( + autocompletePopup, + "popupshown" + ); + // the dropmarker gets un-hidden async when looking up username suggestions + await TestUtils.waitForCondition(() => !dropmarker.hidden); + + EventUtils.synthesizeMouseAtCenter(dropmarker, {}); + + await popupShown; + + let suggestions = [ + ...document + .getElementById("PopupAutoComplete") + .getElementsByTagName("richlistitem"), + ].filter(richlistitem => !richlistitem.collapsed); + + let suggestionText = suggestions.map( + richlistitem => richlistitem.querySelector(".ac-title-text").innerHTML + ); + + let targetIndex = suggestionText.indexOf(text); + Assert.ok(targetIndex != -1, "Suggestions include expected text"); + + let promiseHidden = BrowserTestUtils.waitForEvent( + autocompletePopup, + "popuphidden" + ); + + info("Selecting doorhanger suggestion"); + + EventUtils.synthesizeMouseAtCenter(suggestions[targetIndex], {}); + + await promiseHidden; +} + +// End popup notification (doorhanger) functions // + +async function openPasswordManager(openingFunc, waitForFilter) { + info("waiting for new tab to open"); + let tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + url => url.includes("about:logins") && !url.includes("entryPoint="), + true + ); + await openingFunc(); + let tab = await tabPromise; + Assert.ok(tab, "got password management tab"); + let filterValue; + if (waitForFilter) { + filterValue = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + let loginFilter = Cu.waiveXrays( + content.document + .querySelector("login-list") + .shadowRoot.querySelector("login-filter") + ); + await ContentTaskUtils.waitForCondition( + () => !!loginFilter.value, + "wait for login-filter to have a value" + ); + return loginFilter.value; + }); + } + return { + filterValue, + close() { + BrowserTestUtils.removeTab(tab); + }, + }; +} + +// Autocomplete popup related functions // + +async function openACPopup( + popup, + browser, + inputSelector, + iframeBrowsingContext = null +) { + let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + + await SimpleTest.promiseFocus(browser); + info("content window focused"); + + // Focus the username field to open the popup. + let target = iframeBrowsingContext || browser; + await SpecialPowers.spawn( + target, + [[inputSelector]], + function openAutocomplete(sel) { + content.document.querySelector(sel).focus(); + } + ); + + let shown = await promiseShown; + Assert.ok(shown, "autocomplete popup shown"); + return shown; +} + +async function closePopup(popup) { + if (popup.state == "closed") { + await Promise.resolve(); + } else { + let promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + popup.hidePopup(); + await promiseHidden; + } +} + +async function fillGeneratedPasswordFromOpenACPopup( + browser, + passwordInputSelector +) { + let popup = browser.ownerDocument.getElementById("PopupAutoComplete"); + let item; + + await new Promise(requestAnimationFrame); + await TestUtils.waitForCondition(() => { + item = popup.querySelector(`[originaltype="generatedPassword"]`); + return item && !EventUtils.isHidden(item); + }, "Waiting for item to become visible"); + + let inputEventPromise = ContentTask.spawn( + browser, + [passwordInputSelector], + async function waitForInput(inputSelector) { + let passwordInput = content.document.querySelector(inputSelector); + await ContentTaskUtils.waitForEvent( + passwordInput, + "input", + "Password input value changed" + ); + } + ); + + let passwordGeneratedPromise = listenForTestNotification( + "PasswordEditedOrGenerated" + ); + + info("Clicking the generated password AC item"); + EventUtils.synthesizeMouseAtCenter(item, {}); + info("Waiting for the content input value to change"); + await inputEventPromise; + info("Waiting for the passwordGeneratedPromise"); + await passwordGeneratedPromise; +} + +// Contextmenu functions // + +/** + * 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. + */ +async function openPasswordContextMenu( + browser, + input, + assertCallback = null, + browsingContext = null, + openFillMenu = null +) { + const doc = browser.ownerDocument; + const CONTEXT_MENU = doc.getElementById("contentAreaContextMenu"); + const POPUP_HEADER = doc.getElementById("fill-login"); + const LOGIN_POPUP = doc.getElementById("fill-login-popup"); + + if (!browsingContext) { + browsingContext = browser.browsingContext; + } + + let contextMenuShownPromise = BrowserTestUtils.waitForEvent( + CONTEXT_MENU, + "popupshown" + ); + + // 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 BrowserTestUtils.synthesizeMouseAtCenter( + input, + eventDetails, + browsingContext + ); + // Synthesize a contextmenu event to actually open the context menu. + eventDetails = { type: "contextmenu", button: 2 }; + await BrowserTestUtils.synthesizeMouseAtCenter( + input, + eventDetails, + browsingContext + ); + + await contextMenuShownPromise; + + if (assertCallback) { + let shouldContinue = await assertCallback(); + if (!shouldContinue) { + return; + } + } + + if (openFillMenu) { + // Open the fill login menu. + let popupShownPromise = BrowserTestUtils.waitForEvent( + LOGIN_POPUP, + "popupshown" + ); + POPUP_HEADER.openMenu(true); + await popupShownPromise; + } +} + +/** + * Listen for the login manager test notification specified by + * expectedMessage. Possible messages: + * FormProcessed - a form was processed after page load. + * FormSubmit - a form was just submitted. + * PasswordEditedOrGenerated - a password was filled in or modified. + * + * The count is the number of that messages to wait for. This should + * typically be used when waiting for the FormProcessed message for a page + * that has subframes to ensure all have been handled. + * + * Returns a promise that will passed additional data specific to the message. + */ +function listenForTestNotification(expectedMessage, count = 1) { + return new Promise(resolve => { + LoginManagerParent.setListenerForTests((msg, data) => { + if (msg == expectedMessage && --count == 0) { + LoginManagerParent.setListenerForTests(null); + info("listenForTestNotification, resolving for message: " + msg); + resolve(data); + } + }); + }); +} + +/** + * Use the contextmenu to fill a field with a generated password + */ +async function doFillGeneratedPasswordContextMenuItem(browser, passwordInput) { + await SimpleTest.promiseFocus(browser); + await openPasswordContextMenu(browser, passwordInput); + + let generatedPasswordItem = document.getElementById( + "fill-login-generated-password" + ); + let generatedPasswordSeparator = document.getElementById( + "passwordmgr-items-separator" + ); + + Assert.ok( + BrowserTestUtils.is_visible(generatedPasswordItem), + "generated password item is visible" + ); + Assert.ok( + BrowserTestUtils.is_visible(generatedPasswordSeparator), + "separator is visible" + ); + + let popup = document.getElementById("PopupAutoComplete"); + Assert.ok(popup, "Got popup"); + let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + + await new Promise(resolve => { + SimpleTest.executeSoon(resolve); + }); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + contextMenu.activateItem(generatedPasswordItem); + + await promiseShown; + await fillGeneratedPasswordFromOpenACPopup(browser, passwordInput); +} + +// Content form helpers +async function changeContentFormValues( + browser, + selectorValues, + shouldBlur = true +) { + for (let [sel, value] of Object.entries(selectorValues)) { + info("changeContentFormValues, update: " + sel + ", to: " + value); + await changeContentInputValue(browser, sel, value, shouldBlur); + await TestUtils.waitForTick(); + } +} + +async function changeContentInputValue( + browser, + selector, + str, + shouldBlur = true +) { + await SimpleTest.promiseFocus(browser.ownerGlobal); + let oldValue = await ContentTask.spawn(browser, [selector], function (sel) { + return content.document.querySelector(sel).value; + }); + + if (str === oldValue) { + info("no change needed to value of " + selector + ": " + oldValue); + return; + } + info(`changeContentInputValue: from "${oldValue}" to "${str}"`); + await ContentTask.spawn( + browser, + { selector, str, shouldBlur }, + async function ({ selector, str, shouldBlur }) { + const EventUtils = ContentTaskUtils.getEventUtils(content); + let input = content.document.querySelector(selector); + + input.focus(); + if (!str) { + input.select(); + await EventUtils.synthesizeKey("KEY_Backspace", {}, content); + } else if (input.value.startsWith(str)) { + info( + `New string is substring of value: ${str.length}, ${input.value.length}` + ); + input.setSelectionRange(str.length, input.value.length); + await EventUtils.synthesizeKey("KEY_Backspace", {}, content); + } else if (str.startsWith(input.value)) { + info( + `New string appends to value: ${input.value}, ${str.substring( + input.value.length + )}` + ); + input.setSelectionRange(input.value.length, input.value.length); + await EventUtils.sendString(str.substring(input.value.length), content); + } else { + input.select(); + await EventUtils.sendString(str, content); + } + + if (shouldBlur) { + let changedPromise = ContentTaskUtils.waitForEvent(input, "change"); + input.blur(); + await changedPromise; + } + + Assert.equal(str, input.value, `Expected value '${str}' is set on input`); + } + ); + info("Input value changed"); + await TestUtils.waitForTick(); +} + +async function verifyConfirmationHint( + browser, + forceClose, + anchorID = "password-notification-icon" +) { + let hintElem = browser.ownerGlobal.ConfirmationHint._panel; + await BrowserTestUtils.waitForPopupEvent(hintElem, "shown"); + try { + Assert.equal(hintElem.state, "open", "hint popup is open"); + Assert.ok( + BrowserTestUtils.is_visible(hintElem.anchorNode), + "hint anchorNode is visible" + ); + Assert.equal( + hintElem.anchorNode.id, + anchorID, + "Hint should be anchored on the expected notification icon" + ); + info("verifyConfirmationHint, hint is shown and has its anchorNode"); + if (forceClose) { + await closePopup(hintElem); + } else { + info("verifyConfirmationHint, assertion ok, wait for poopuphidden"); + await BrowserTestUtils.waitForPopupEvent(hintElem, "hidden"); + info("verifyConfirmationHint, hintElem popup is hidden"); + } + } catch (ex) { + Assert.ok(false, "Confirmation hint not shown: " + ex.message); + } finally { + info("verifyConfirmationHint promise finalized"); + } +} -- cgit v1.2.3