mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-09 12:51:09 +02:00
626 lines
18 KiB
JavaScript
626 lines
18 KiB
JavaScript
/**
|
|
* Helpers for password manager mochitest-plain tests.
|
|
*/
|
|
|
|
/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
|
|
|
|
const { LoginTestUtils } = SpecialPowers.Cu.import(
|
|
"resource://testing-common/LoginTestUtils.jsm",
|
|
{}
|
|
);
|
|
|
|
// Setup LoginTestUtils to report assertions to the mochitest harness.
|
|
LoginTestUtils.setAssertReporter(
|
|
SpecialPowers.wrapCallback((err, message, stack) => {
|
|
SimpleTest.record(!err, err ? err.message : message, null, stack);
|
|
})
|
|
);
|
|
|
|
const { LoginHelper } = SpecialPowers.Cu.import(
|
|
"resource://gre/modules/LoginHelper.jsm",
|
|
{}
|
|
);
|
|
const { Services } = SpecialPowers.Cu.import(
|
|
"resource://gre/modules/Services.jsm",
|
|
{}
|
|
);
|
|
|
|
const {
|
|
LENGTH: GENERATED_PASSWORD_LENGTH,
|
|
REGEX: GENERATED_PASSWORD_REGEX,
|
|
} = LoginTestUtils.generation;
|
|
const LOGIN_FIELD_UTILS = LoginTestUtils.loginField;
|
|
const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/";
|
|
|
|
/**
|
|
* Returns the element with the specified |name| attribute.
|
|
*/
|
|
function $_(formNum, name) {
|
|
var form = document.getElementById("form" + formNum);
|
|
if (!form) {
|
|
ok(false, "$_ couldn't find requested form " + formNum);
|
|
return null;
|
|
}
|
|
|
|
var element = form.children.namedItem(name);
|
|
if (!element) {
|
|
ok(false, "$_ couldn't find requested element " + name);
|
|
return null;
|
|
}
|
|
|
|
// Note that namedItem is a bit stupid, and will prefer an
|
|
// |id| attribute over a |name| attribute when looking for
|
|
// the element. Login Mananger happens to use .namedItem
|
|
// anyway, but let's rigorously check it here anyway so
|
|
// that we don't end up with tests that mistakenly pass.
|
|
|
|
if (element.getAttribute("name") != name) {
|
|
ok(false, "$_ got confused.");
|
|
return null;
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* Recreate a DOM tree using the outerHTML to ensure that any event listeners
|
|
* and internal state for the elements are removed.
|
|
*/
|
|
function recreateTree(element) {
|
|
// eslint-disable-next-line no-unsanitized/property, no-self-assign
|
|
element.outerHTML = element.outerHTML;
|
|
}
|
|
|
|
/**
|
|
* Check autocomplete popup results to ensure that expected
|
|
* *labels* are being shown correctly as items in the popup.
|
|
*/
|
|
function checkAutoCompleteResults(actualValues, expectedValues, hostname, msg) {
|
|
if (hostname === null) {
|
|
checkArrayValues(actualValues, expectedValues, msg);
|
|
return;
|
|
}
|
|
|
|
is(
|
|
typeof hostname,
|
|
"string",
|
|
"checkAutoCompleteResults: hostname must be a string"
|
|
);
|
|
|
|
isnot(
|
|
actualValues.length,
|
|
0,
|
|
"There should be items in the autocomplete popup: " +
|
|
JSON.stringify(actualValues)
|
|
);
|
|
|
|
// Check the footer first.
|
|
let footerResult = actualValues[actualValues.length - 1];
|
|
is(footerResult, "View Saved Logins", "the footer text is shown correctly");
|
|
|
|
if (actualValues.length == 1) {
|
|
is(
|
|
expectedValues.length,
|
|
0,
|
|
"If only the footer is present then there should be no expectedValues"
|
|
);
|
|
info("Only the footer is present in the popup");
|
|
return;
|
|
}
|
|
|
|
// Check the rest of the autocomplete item values.
|
|
checkArrayValues(actualValues.slice(0, -1), expectedValues, msg);
|
|
}
|
|
|
|
function getIframeBrowsingContext(window, iframeNumber = 0) {
|
|
let bc = SpecialPowers.wrap(window).windowGlobalChild.browsingContext;
|
|
return SpecialPowers.unwrap(bc.children[iframeNumber]);
|
|
}
|
|
|
|
/**
|
|
* Set input values via setUserInput to emulate user input
|
|
* and distinguish them from declarative or script-assigned values
|
|
*/
|
|
function setUserInputValues(parentNode, selectorValues) {
|
|
for (let [selector, newValue] of Object.entries(selectorValues)) {
|
|
info(`setUserInputValues, selector: ${selector}`);
|
|
try {
|
|
let field = SpecialPowers.wrap(parentNode.querySelector(selector));
|
|
if (field.value == newValue) {
|
|
// we don't get an input event if the new value == the old
|
|
field.value += "#";
|
|
}
|
|
field.setUserInput(newValue);
|
|
} catch (ex) {
|
|
info(ex.message);
|
|
info(ex.stack);
|
|
ok(
|
|
false,
|
|
`setUserInputValues: Couldn't set value of field: ${ex.message}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Function} [aFilterFn = undefined] Function to filter out irrelevant submissions.
|
|
* @return {Promise} resolving when a relevant form submission was processed.
|
|
*/
|
|
function getSubmitMessage(aFilterFn = undefined) {
|
|
info("getSubmitMessage");
|
|
return new Promise((resolve, reject) => {
|
|
PWMGR_COMMON_PARENT.addMessageListener(
|
|
"formSubmissionProcessed",
|
|
function processed(...args) {
|
|
if (aFilterFn && !aFilterFn(...args)) {
|
|
// This submission isn't the one we're waiting for.
|
|
return;
|
|
}
|
|
|
|
info("got formSubmissionProcessed");
|
|
PWMGR_COMMON_PARENT.removeMessageListener(
|
|
"formSubmissionProcessed",
|
|
processed
|
|
);
|
|
resolve(args[0]);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check for expected username/password in form.
|
|
* @see `checkForm` below for a similar function.
|
|
*/
|
|
function checkLoginForm(
|
|
usernameField,
|
|
expectedUsername,
|
|
passwordField,
|
|
expectedPassword
|
|
) {
|
|
let formID = usernameField.parentNode.id;
|
|
is(
|
|
usernameField.value,
|
|
expectedUsername,
|
|
"Checking " + formID + " username is: " + expectedUsername
|
|
);
|
|
is(
|
|
passwordField.value,
|
|
expectedPassword,
|
|
"Checking " + formID + " password is: " + expectedPassword
|
|
);
|
|
}
|
|
|
|
function checkLoginFormInChildFrame(
|
|
iframeBC,
|
|
usernameFieldId,
|
|
expectedUsername,
|
|
passwordFieldId,
|
|
expectedPassword
|
|
) {
|
|
return SpecialPowers.spawn(
|
|
iframeBC,
|
|
[usernameFieldId, expectedUsername, passwordFieldId, expectedPassword],
|
|
(
|
|
usernameFieldIdF,
|
|
expectedUsernameF,
|
|
passwordFieldIdF,
|
|
expectedPasswordF
|
|
) => {
|
|
let usernameField = this.content.document.getElementById(
|
|
usernameFieldIdF
|
|
);
|
|
let passwordField = this.content.document.getElementById(
|
|
passwordFieldIdF
|
|
);
|
|
|
|
let formID = usernameField.parentNode.id;
|
|
Assert.equal(
|
|
usernameField.value,
|
|
expectedUsernameF,
|
|
"Checking " + formID + " username is: " + expectedUsernameF
|
|
);
|
|
Assert.equal(
|
|
passwordField.value,
|
|
expectedPasswordF,
|
|
"Checking " + formID + " password is: " + expectedPasswordF
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check a form for expected values. If an argument is null, a field's
|
|
* expected value will be the default value.
|
|
*
|
|
* <form id="form#">
|
|
* checkForm(#, "foo");
|
|
*/
|
|
function checkForm(formNum, val1, val2, val3) {
|
|
var e,
|
|
form = document.getElementById("form" + formNum);
|
|
ok(form, "Locating form " + formNum);
|
|
|
|
var numToCheck = arguments.length - 1;
|
|
|
|
if (!numToCheck--) {
|
|
return;
|
|
}
|
|
e = form.elements[0];
|
|
if (val1 == null) {
|
|
is(
|
|
e.value,
|
|
e.defaultValue,
|
|
"Test default value of field " + e.name + " in form " + formNum
|
|
);
|
|
} else {
|
|
is(e.value, val1, "Test value of field " + e.name + " in form " + formNum);
|
|
}
|
|
|
|
if (!numToCheck--) {
|
|
return;
|
|
}
|
|
e = form.elements[1];
|
|
if (val2 == null) {
|
|
is(
|
|
e.value,
|
|
e.defaultValue,
|
|
"Test default value of field " + e.name + " in form " + formNum
|
|
);
|
|
} else {
|
|
is(e.value, val2, "Test value of field " + e.name + " in form " + formNum);
|
|
}
|
|
|
|
if (!numToCheck--) {
|
|
return;
|
|
}
|
|
e = form.elements[2];
|
|
if (val3 == null) {
|
|
is(
|
|
e.value,
|
|
e.defaultValue,
|
|
"Test default value of field " + e.name + " in form " + formNum
|
|
);
|
|
} else {
|
|
is(e.value, val3, "Test value of field " + e.name + " in form " + formNum);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check a form for unmodified values from when page was loaded.
|
|
*
|
|
* <form id="form#">
|
|
* checkUnmodifiedForm(#);
|
|
*/
|
|
function checkUnmodifiedForm(formNum) {
|
|
var form = document.getElementById("form" + formNum);
|
|
ok(form, "Locating form " + formNum);
|
|
|
|
for (var i = 0; i < form.elements.length; i++) {
|
|
var ele = form.elements[i];
|
|
|
|
// No point in checking form submit/reset buttons.
|
|
if (ele.type == "submit" || ele.type == "reset") {
|
|
continue;
|
|
}
|
|
|
|
is(
|
|
ele.value,
|
|
ele.defaultValue,
|
|
"Test to default value of field " + ele.name + " in form " + formNum
|
|
);
|
|
}
|
|
}
|
|
|
|
function registerRunTests() {
|
|
return new Promise(resolve => {
|
|
function onDOMContentLoaded() {
|
|
var form = document.createElement("form");
|
|
form.id = "observerforcer";
|
|
var username = document.createElement("input");
|
|
username.name = "testuser";
|
|
form.appendChild(username);
|
|
var password = document.createElement("input");
|
|
password.name = "testpass";
|
|
password.type = "password";
|
|
form.appendChild(password);
|
|
|
|
var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
|
|
if (data !== "observerforcer") {
|
|
return;
|
|
}
|
|
|
|
SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
|
|
form.remove();
|
|
SimpleTest.executeSoon(() => {
|
|
var runTestEvent = new Event("runTests");
|
|
window.dispatchEvent(runTestEvent);
|
|
resolve();
|
|
});
|
|
});
|
|
SpecialPowers.addObserver(observer, "passwordmgr-processed-form");
|
|
|
|
document.body.appendChild(form);
|
|
}
|
|
// We provide a general mechanism for our tests to know when they can
|
|
// safely run: we add a final form that we know will be filled in, wait
|
|
// for the login manager to tell us that it's filled in and then continue
|
|
// with the rest of the tests.
|
|
if (
|
|
document.readyState == "complete" ||
|
|
document.readyState == "loaded" ||
|
|
document.readyState == "interactive"
|
|
) {
|
|
onDOMContentLoaded();
|
|
} else {
|
|
window.addEventListener("DOMContentLoaded", onDOMContentLoaded);
|
|
}
|
|
});
|
|
}
|
|
|
|
function enableMasterPassword() {
|
|
setMasterPassword(true);
|
|
}
|
|
|
|
function disableMasterPassword() {
|
|
setMasterPassword(false);
|
|
}
|
|
|
|
function setMasterPassword(enable) {
|
|
PWMGR_COMMON_PARENT.sendAsyncMessage("setMasterPassword", { enable });
|
|
}
|
|
|
|
function isLoggedIn() {
|
|
return PWMGR_COMMON_PARENT.sendQuery("isLoggedIn");
|
|
}
|
|
|
|
function logoutMasterPassword() {
|
|
runInParent(function parent_logoutMasterPassword() {
|
|
var sdr = Cc["@mozilla.org/security/sdr;1"].getService(
|
|
Ci.nsISecretDecoderRing
|
|
);
|
|
sdr.logoutAndTeardown();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resolves when a specified number of forms have been processed for (potential) filling.
|
|
*/
|
|
function promiseFormsProcessed(expectedCount = 1) {
|
|
var processedCount = 0;
|
|
return new Promise((resolve, reject) => {
|
|
function onProcessedForm(subject, topic, data) {
|
|
processedCount++;
|
|
if (processedCount == expectedCount) {
|
|
info(`${processedCount} form(s) processed`);
|
|
SpecialPowers.removeObserver(
|
|
onProcessedForm,
|
|
"passwordmgr-processed-form"
|
|
);
|
|
resolve(SpecialPowers.Cu.waiveXrays(subject), data);
|
|
}
|
|
}
|
|
SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form");
|
|
});
|
|
}
|
|
|
|
function getTelemetryEvents(options) {
|
|
return new Promise(resolve => {
|
|
PWMGR_COMMON_PARENT.addMessageListener(
|
|
"getTelemetryEvents",
|
|
function gotResult(events) {
|
|
info(
|
|
"CONTENT: getTelemetryEvents gotResult: " + JSON.stringify(events)
|
|
);
|
|
PWMGR_COMMON_PARENT.removeMessageListener(
|
|
"getTelemetryEvents",
|
|
gotResult
|
|
);
|
|
resolve(events);
|
|
}
|
|
);
|
|
PWMGR_COMMON_PARENT.sendAsyncMessage("getTelemetryEvents", options);
|
|
});
|
|
}
|
|
|
|
function loadRecipes(recipes) {
|
|
info("Loading recipes");
|
|
return new Promise(resolve => {
|
|
PWMGR_COMMON_PARENT.addMessageListener("loadedRecipes", function loaded() {
|
|
PWMGR_COMMON_PARENT.removeMessageListener("loadedRecipes", loaded);
|
|
resolve(recipes);
|
|
});
|
|
PWMGR_COMMON_PARENT.sendAsyncMessage("loadRecipes", recipes);
|
|
});
|
|
}
|
|
|
|
function resetRecipes() {
|
|
info("Resetting recipes");
|
|
return new Promise(resolve => {
|
|
PWMGR_COMMON_PARENT.addMessageListener("recipesReset", function reset() {
|
|
PWMGR_COMMON_PARENT.removeMessageListener("recipesReset", reset);
|
|
resolve();
|
|
});
|
|
PWMGR_COMMON_PARENT.sendAsyncMessage("resetRecipes");
|
|
});
|
|
}
|
|
|
|
function promiseStorageChanged(expectedChangeTypes) {
|
|
return new Promise((resolve, reject) => {
|
|
function onStorageChanged({ topic, data }) {
|
|
let changeType = expectedChangeTypes.shift();
|
|
is(data, changeType, "Check expected passwordmgr-storage-changed type");
|
|
if (expectedChangeTypes.length === 0) {
|
|
PWMGR_COMMON_PARENT.removeMessageListener(
|
|
"storageChanged",
|
|
onStorageChanged
|
|
);
|
|
resolve();
|
|
}
|
|
}
|
|
PWMGR_COMMON_PARENT.addMessageListener("storageChanged", onStorageChanged);
|
|
});
|
|
}
|
|
|
|
function promisePromptShown(expectedTopic) {
|
|
return new Promise((resolve, reject) => {
|
|
function onPromptShown({ topic, data }) {
|
|
is(topic, expectedTopic, "Check expected prompt topic");
|
|
PWMGR_COMMON_PARENT.removeMessageListener("promptShown", onPromptShown);
|
|
resolve();
|
|
}
|
|
PWMGR_COMMON_PARENT.addMessageListener("promptShown", onPromptShown);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Run a function synchronously in the parent process and destroy it in the test cleanup function.
|
|
* @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
|
|
* or the URL to a JS file.
|
|
* @return {Object} - the return value of loadChromeScript providing message-related methods.
|
|
* @see loadChromeScript in specialpowersAPI.js
|
|
*/
|
|
function runInParent(aFunctionOrURL) {
|
|
let chromeScript = SpecialPowers.loadChromeScript(aFunctionOrURL);
|
|
SimpleTest.registerCleanupFunction(() => {
|
|
chromeScript.destroy();
|
|
});
|
|
return chromeScript;
|
|
}
|
|
|
|
/*
|
|
* gTestDependsOnDeprecatedLogin Set this global to true if your test relies
|
|
* on the testuser/testpass login that is created in pwmgr_common.js. New tests
|
|
* should not rely on this login.
|
|
*/
|
|
var gTestDependsOnDeprecatedLogin = false;
|
|
|
|
/**
|
|
* Replace the content innerHTML with the provided form and wait for autofill to fill in the form.
|
|
*
|
|
* @param {string} form The form to be appended to the #content element.
|
|
* @param {string} fieldSelector The CSS selector for the field to-be-filled
|
|
* @param {string} fieldValue The value expected to be filled
|
|
* @param {string} formId The ID (excluding the # character) of the form
|
|
*/
|
|
function setFormAndWaitForFieldFilled(
|
|
form,
|
|
{ fieldSelector, fieldValue, formId }
|
|
) {
|
|
// eslint-disable-next-line no-unsanitized/property
|
|
document.querySelector("#content").innerHTML = form;
|
|
return SimpleTest.promiseWaitForCondition(() => {
|
|
let ancestor = formId
|
|
? document.querySelector("#" + formId)
|
|
: document.documentElement;
|
|
return ancestor.querySelector(fieldSelector).value == fieldValue;
|
|
}, "Wait for password manager to fill form");
|
|
}
|
|
|
|
/**
|
|
* Run commonInit synchronously in the parent then run the test function after the runTests event.
|
|
*
|
|
* @param {Function} aFunction The test function to run
|
|
*/
|
|
function runChecksAfterCommonInit(aFunction = null) {
|
|
SimpleTest.waitForExplicitFinish();
|
|
if (aFunction) {
|
|
window.addEventListener("runTests", aFunction);
|
|
PWMGR_COMMON_PARENT.addMessageListener("registerRunTests", () =>
|
|
registerRunTests()
|
|
);
|
|
}
|
|
PWMGR_COMMON_PARENT.sendAsyncMessage("setupParent", {
|
|
testDependsOnDeprecatedLogin: gTestDependsOnDeprecatedLogin,
|
|
});
|
|
return PWMGR_COMMON_PARENT;
|
|
}
|
|
|
|
// Begin code that runs immediately for all tests that include this file.
|
|
|
|
const PWMGR_COMMON_PARENT = runInParent(
|
|
SimpleTest.getTestFileURL("pwmgr_common_parent.js")
|
|
);
|
|
|
|
SimpleTest.registerCleanupFunction(() => {
|
|
SpecialPowers.flushPrefEnv();
|
|
|
|
PWMGR_COMMON_PARENT.sendAsyncMessage("cleanup");
|
|
|
|
runInParent(function cleanupParent() {
|
|
// eslint-disable-next-line no-shadow
|
|
const { Services } = ChromeUtils.import(
|
|
"resource://gre/modules/Services.jsm"
|
|
);
|
|
// eslint-disable-next-line no-shadow
|
|
const { LoginManagerParent } = ChromeUtils.import(
|
|
"resource://gre/modules/LoginManagerParent.jsm"
|
|
);
|
|
|
|
// Remove all logins and disabled hosts
|
|
Services.logins.removeAllLogins();
|
|
|
|
let disabledHosts = Services.logins.getAllDisabledHosts();
|
|
disabledHosts.forEach(host =>
|
|
Services.logins.setLoginSavingEnabled(host, true)
|
|
);
|
|
|
|
let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
|
|
Ci.nsIHttpAuthManager
|
|
);
|
|
authMgr.clearAll();
|
|
|
|
// Check that it's not null, instead of truthy to catch it becoming undefined
|
|
// in a refactoring.
|
|
if (LoginManagerParent._recipeManager !== null) {
|
|
LoginManagerParent._recipeManager.reset();
|
|
}
|
|
|
|
// Cleanup PopupNotifications (if on a relevant platform)
|
|
let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
|
|
if (chromeWin && chromeWin.PopupNotifications) {
|
|
let notes = chromeWin.PopupNotifications._currentNotifications;
|
|
if (notes.length) {
|
|
dump("Removing " + notes.length + " popup notifications.\n");
|
|
}
|
|
for (let note of notes) {
|
|
note.remove();
|
|
}
|
|
}
|
|
|
|
// Clear events last in case the above cleanup records events.
|
|
Services.telemetry.clearEvents();
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Proxy for Services.logins (nsILoginManager).
|
|
* Only supports arguments which support structured clone plus {nsILoginInfo}
|
|
* Assumes properties are methods.
|
|
*/
|
|
this.LoginManager = new Proxy(
|
|
{},
|
|
{
|
|
get(target, prop, receiver) {
|
|
return (...args) => {
|
|
let loginInfoIndices = [];
|
|
let cloneableArgs = args.map((val, index) => {
|
|
if (
|
|
SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)
|
|
) {
|
|
loginInfoIndices.push(index);
|
|
return LoginHelper.loginToVanillaObject(val);
|
|
}
|
|
|
|
return val;
|
|
});
|
|
|
|
return PWMGR_COMMON_PARENT.sendQuery("proxyLoginManager", {
|
|
args: cloneableArgs,
|
|
loginInfoIndices,
|
|
methodName: prop,
|
|
});
|
|
};
|
|
},
|
|
}
|
|
);
|