/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint-env node */ "use strict";
/** * This function obtains the perftest secret from Taskcluster. * * It will NOT work locally. Please see the get_logins function, you * will need to define a JSON file and set the RAPTOR_LOGINS * env variable to its path.
*/
async function get_tc_secrets(context) { const MOZ_AUTOMATION = process.env.MOZ_AUTOMATION; if (!MOZ_AUTOMATION) { throw Error( "Not running in CI. Set RAPTOR_LOGINS to a JSON file containing the logins."
);
}
let TASKCLUSTER_PROXY_URL = process.env.TASKCLUSTER_PROXY_URL
? process.env.TASKCLUSTER_PROXY_URL
: DEFAULT_SERVER;
let MOZ_SCM_LEVEL = process.env.MOZ_SCM_LEVEL ? process.env.MOZ_SCM_LEVEL : 1;
/** * This function gets the login information required. * * It starts by looking for a local file whose path is defined * within RAPTOR_LOGINS. If we don't find this file, then we'll * attempt to get the login information from our Taskcluster secret. * If MOZ_AUTOMATION is undefined, then the test will fail, Taskcluster * secrets can only be obtained in CI.
*/
async function get_logins(context) {
let logins;
let RAPTOR_LOGINS = process.env.RAPTOR_LOGINS; if (RAPTOR_LOGINS) { // Get logins from a local file if (!RAPTOR_LOGINS.endsWith(".json")) { throw Error(
`File given for logins does not end in '.json': ${RAPTOR_LOGINS}`
);
}
let logins_file = null; try {
logins_file = await fs.readFileSync(RAPTOR_LOGINS, "utf8");
} catch (err) { throw Error(`Failed to read the file ${RAPTOR_LOGINS}: ${err}`);
}
logins = await JSON.parse(logins_file);
} else { // Get logins from a perftest Taskcluster secret
logins = await get_tc_secrets(context);
}
return logins;
}
/** * This function returns the type of login to do. * * This function returns "single-form" when we find a single form. If we only * find a single input field, we assume that there is one page per input * and return "multi-page". Otherwise, we return null.
*/
async function get_login_type(context, commands) { /* Determine if there's a password field visible with this query selector. Some sites use `tabIndex` to hide the password field behind other elements. In this case, we are searching for any password-type field that has a tabIndex of 0 or undefined and is not hidden.
*/
let input_length = await commands.js.run(` return document.querySelectorAll( "input[type=password][tabIndex='0']:not([type=hidden])," + "input[type=password]:not([tabIndex]):not([type=hidden])"
).length;
`); if (input_length == 0) {
context.log.info("Found a multi-page login"); return multi_page_login;
} elseif (input_length == 1) {
context.log.info("Found a single-page login"); return single_page_login;
}
if (
(await commands.js.run(
`return document.querySelectorAll("form").length;`
)) >= 1
) {
context.log.info("Found a single-form login"); return single_form_login;
}
returnnull;
}
/** * This function sets up the login for a single form. * * The username field is defined as the field which immediately precedes * the password field. We have to do this in two steps because we need * to make sure that the event we emit from the change has the `isTrusted` * field set to `true`. Otherwise, some websites will ignore the input and * the form submission.
*/
async function single_page_login(login_info, context, commands, prefix = "") { // Get the first input field in the form that is not hidden and add the // username. Assumes that email/username is always the first input field.
await commands.addText.bySelector(
login_info.username,
`${prefix}input:not([type=hidden]):not([type=password])`
);
// Get the password field and ensure it's not hidden.
await commands.addText.bySelector(
login_info.password,
`${prefix}input[type=password]:not([type=hidden])`
);
return undefined;
}
/** * See single_page_login.
*/
async function single_form_login(login_info, context, commands) { return single_page_login(login_info, context, commands, "form ");
}
/** * Login to a website that uses multiple pages for the login. * * WARNING: Assumes that the first page is for the username.
*/
async function multi_page_login(login_info, context, commands) { const driver = context.selenium.driver; const webdriver = context.selenium.webdriver;
return async function () {
password_field.sendKeys(webdriver.Key.ENTER);
await commands.wait.byTime(5000);
};
}
/** * This function sets up the login. * * This is done by first the login type, and then performing the * actual login setup. The return is a possible button to click * to perform the login.
*/
async function setup_login(login_info, context, commands) {
let login_func = await get_login_type(context, commands); if (!login_func) { throw Error("Could not determine the type of login page.");
}
/** * This function performs the login. * * It does this by either clicking on a button with a type * of "sumbit", or running a final_button function that was * obtained from the setup_login function. Some pages also ask * questions about setting up 2FA or other information. Generally, * these contain the "skip" text.
*/
async function login(context, commands, final_button) { try { if (!final_button) { // The mouse double click emits an event with `evt.isTrusted=true`
await commands.mouse.doubleClick.bySelector("button[type=submit]");
await commands.wait.byTime(10000);
} else { // In some cases, it's preferable to be given a function for the final button
await final_button();
}
// Some pages ask to setup 2FA, skip this based on the text const XPATHS = [ "//a[contains(text(), 'skip')]", "//button[contains(text(), 'skip')]", "//input[contains(text(), 'skip')]", "//div[contains(text(), 'skip')]",
];
for (let xpath of XPATHS) { try {
await commands.mouse.doubleClick.byXpath(xpath);
} catch (err) { if (err.toString().includes("not double click")) {
context.log.info(`Can't find a button with the text: ${xpath}`);
} else { throw err;
}
}
}
} catch (err) { throw Error(
`Could not login to website as we could not find the submit button/input: ${err}`
);
}
}
/** * Grab the base URL from the browsertime url. * * This is a necessary step for getting the login values from the Taskcluster * secrets, which are hashed by the base URL. * * The first entry is the protocal, third is the top-level domain (or host)
*/ function get_base_URL(fullUrl) {
let pathAsArray = fullUrl.split("/"); return pathAsArray[0] + "//" + pathAsArray[2];
}
/** * This function attempts the login-login sequence for a live pageload recording
*/
async function perform_live_login(context, commands) {
let testUrl = context.options.browsertime.url;
let logins = await get_logins(context); const baseUrl = get_base_URL(testUrl);
async function dismissCookiePrompt(input_cmds, context, commands) {
context.log.info("Searching for cookie prompt elements...");
let cmds = input_cmds.split(";;;"); for (let cmdstr of cmds) {
let [cmd, ...args] = cmdstr.split(":::");
context.log.info(cmd, args);
let result = await commands.js.run(
`return document.evaluate("` +
args +
`", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;`
); if (result) {
context.log.info("Element found, clicking on it.");
await run_command(cmdstr, context, commands);
} else {
context.log.info( "Element not found! The cookie prompt may have not appeared, please check the screenshots."
);
}
}
}
async function pageload_test(context, commands) {
let testUrl = context.options.browsertime.url;
let secondaryUrl = context.options.browsertime.secondary_url;
let testName = context.options.browsertime.testName;
let dismissPrompt = context.options.browsertime.dismiss_cookie_prompt || "";
context.log.info(context.options.browsertime);
// Wait for browser to settle
await commands.wait.byTime(1000);
// If the user has RAPTOR_LOGINS configured correctly, a local login pageload // test can be attempted. Otherwise if attempting it in CI, only sites with the // associated MOZ_SCM_LEVEL will be attempted (e.g. Try = 1, autoland = 3) if (context.options.browsertime.login) { if (context.options.browsertime.manual_login) { // Perform a manual login using the value given in manual_login // as the amount of time to wait
await commands.navigate(testUrl);
context.log.info(
`Waiting ${context.options.browsertime.manual_login}ms for login...`
);
await commands.wait.byTime(context.options.browsertime.manual_login);
} elseif (
process.env.RAPTOR_LOGINS ||
process.env.MOZ_SCM_LEVEL == 3 ||
SCM_1_LOGIN_SITES.includes(testName)
) { try {
await perform_live_login(context, commands);
} catch (err) {
context.log.info( "Unable to login. Acquiring a recording without logging in"
);
context.log.info("Error:" + err);
}
} else {
context.log.info(`
NOTE: This is a login test but a manual login was not requested, and
we cannot find any logins defined in RAPTOR_LOGINS.
`);
}
}
// Wait for browser to settle
await commands.wait.byTime(1000);
}
/** * Converts a string such as `measure.start` into the * actual function that is found in the `commands` module. * * XX: Find a way to share this function between * perftest_record.js and browsertime_interactive.js
*/
async function get_command_function(cmd, commands) { if (cmd == "") { thrownew Error("A blank command was given.");
} elseif (cmd.endsWith(".")) { thrownew Error( "An extra `.` was found at the end of this command: " + cmd
);
}
// `func` will hold the actual method that needs to be called, // and the `parent_mod` is the context required to run the `func` // method. Without that context, `this` becomes undefined in the browsertime // classes.
let func = null;
let parent_mod = null; for (let func_part of cmd.split(".")) { if (func_part == "") { thrownew Error( "An empty function part was found in the command: " + cmd
);
}
if (func == undefined) { thrownew Error( "The given command could not be found as a function: " + cmd
);
}
return [func, parent_mod];
}
/** * Performs an interactive test. * * These tests are interactive as the entire test is defined * through a set of browsertime commands. This allows users * to build arbitrary tests. Furthermore, interactive tests * provide the ability to login to websites.
*/
async function interactive_test(input_cmds, context, commands) {
let cmds = input_cmds.split(";;;");
let logins; if (context.options.browsertime.login) {
logins = await get_logins(context);
}
await commands.navigate("about:blank");
let user_setup = false;
let final_button = null; for (let cmdstr of cmds) {
let [cmd, ...args] = cmdstr.split(":::");
if (cmd == "setup_login") { if (!logins) { throw Error( "This test is not specified as a `login` test so no login information is available."
);
} if (args.length < 1 || args[0] == "") { throw Error(
`No URL given, can't tell where to setup the login. We only accept: ${logins.keys()}`
);
} /* Structure for logins is: { "username": ..., "password": ..., "suspicious_answer": ..., "login_url": ..., }
*/
let login_info = logins.secret[args[0]];
final_button = await setup_login(login_info, context, commands);
user_setup = true;
} elseif (cmd == "login") { if (!user_setup) { throw Error("setup_login needs to be called before the login command");
}
await login(context, commands, final_button);
} else {
await run_command(cmdstr, context, commands);
}
}
}
async function run_command(cmdstr, context, commands) {
let [cmd, ...args] = cmdstr.split(":::");
let [func, parent_mod] = await get_command_function(cmd, commands);
try {
await func.call(parent_mod, ...args);
} catch (e) {
context.log.info(
`Exception found while running \`commands.${cmd}(${args})\`: ` + e
);
}
}
async function test(context, commands) {
let input_cmds = context.options.browsertime.commands;
let test_type = context.options.browsertime.testType; if (test_type == "interactive") {
await interactive_test(input_cmds, context, commands);
} else {
await pageload_test(context, commands);
} returntrue;
}
module.exports = {
test,
owner: "Bebe fstrugariu@mozilla.com",
name: "Mozproxy recording generator",
component: "raptor",
description: ` This test generates fresh MozProxy recordings. It iterates through a list of
websites provided in *_sites.json and for each one opens a browser and
records all the associated HTTP traffic`,
usage: "mach perftest --proxy --hooks testing/raptor/recorder/hooks.py testing/raptor/recorder/perftest_record.js",
};
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.