972 lines
30 KiB
JavaScript
972 lines
30 KiB
JavaScript
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
|
|
*/
|
|
async function verifyLogins(expectedLogins = []) {
|
|
let allLogins = await 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);
|
|
}
|
|
|
|
async 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 = await 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.isVisible(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.isVisible(generatedPasswordItem),
|
|
"generated password item is visible"
|
|
);
|
|
Assert.ok(
|
|
BrowserTestUtils.isVisible(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",
|
|
expectedL10nMessageId = null
|
|
) {
|
|
let hintElem = browser.ownerGlobal.ConfirmationHint._panel;
|
|
await BrowserTestUtils.waitForPopupEvent(hintElem, "shown");
|
|
try {
|
|
Assert.equal(hintElem.state, "open", "hint popup is open");
|
|
Assert.ok(
|
|
BrowserTestUtils.isVisible(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 (expectedL10nMessageId) {
|
|
const l10nMessageId = hintElem
|
|
.querySelector("#confirmation-hint-message")
|
|
.getAttribute("data-l10n-id");
|
|
Assert.equal(l10nMessageId, expectedL10nMessageId);
|
|
}
|
|
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");
|
|
}
|
|
}
|