1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 const fs = require("fs");
8 const http = require("http");
10 const URL = "/secrets/v1/secret/project/perftest/gecko/level-";
11 const SECRET = "/perftest-login";
12 const DEFAULT_SERVER = "https://firefox-ci-tc.services.mozilla.com";
14 const SCM_1_LOGIN_SITES = ["facebook", "netflix"];
17 * This function obtains the perftest secret from Taskcluster.
19 * It will NOT work locally. Please see the get_logins function, you
20 * will need to define a JSON file and set the RAPTOR_LOGINS
21 * env variable to its path.
23 async function get_tc_secrets(context) {
24 const MOZ_AUTOMATION = process.env.MOZ_AUTOMATION;
25 if (!MOZ_AUTOMATION) {
27 "Not running in CI. Set RAPTOR_LOGINS to a JSON file containing the logins."
31 let TASKCLUSTER_PROXY_URL = process.env.TASKCLUSTER_PROXY_URL
32 ? process.env.TASKCLUSTER_PROXY_URL
35 let MOZ_SCM_LEVEL = process.env.MOZ_SCM_LEVEL ? process.env.MOZ_SCM_LEVEL : 1;
37 const url = TASKCLUSTER_PROXY_URL + URL + MOZ_SCM_LEVEL + SECRET;
39 const data = await new Promise((resolve, reject) => {
40 context.log.info("Obtaining secrets for login...");
46 "Content-Type": "application/json",
47 Accept: "application/json",
52 context.log.info(`Secret status code: ${res.statusCode}`);
62 res.on("error", error => {
63 context.log.error(error);
70 return JSON.parse(data);
74 * This function gets the login information required.
76 * It starts by looking for a local file whose path is defined
77 * within RAPTOR_LOGINS. If we don't find this file, then we'll
78 * attempt to get the login information from our Taskcluster secret.
79 * If MOZ_AUTOMATION is undefined, then the test will fail, Taskcluster
80 * secrets can only be obtained in CI.
82 async function get_logins(context) {
85 let RAPTOR_LOGINS = process.env.RAPTOR_LOGINS;
87 // Get logins from a local file
88 if (!RAPTOR_LOGINS.endsWith(".json")) {
90 `File given for logins does not end in '.json': ${RAPTOR_LOGINS}`
94 let logins_file = null;
96 logins_file = await fs.readFileSync(RAPTOR_LOGINS, "utf8");
98 throw Error(`Failed to read the file ${RAPTOR_LOGINS}: ${err}`);
101 logins = await JSON.parse(logins_file);
103 // Get logins from a perftest Taskcluster secret
104 logins = await get_tc_secrets(context);
111 * This function returns the type of login to do.
113 * This function returns "single-form" when we find a single form. If we only
114 * find a single input field, we assume that there is one page per input
115 * and return "multi-page". Otherwise, we return null.
117 async function get_login_type(context, commands) {
119 Determine if there's a password field visible with this
120 query selector. Some sites use `tabIndex` to hide the password
121 field behind other elements. In this case, we are searching
122 for any password-type field that has a tabIndex of 0 or undefined and
125 let input_length = await commands.js.run(`
126 return document.querySelectorAll(
127 "input[type=password][tabIndex='0']:not([type=hidden])," +
128 "input[type=password]:not([tabIndex]):not([type=hidden])"
131 if (input_length == 0) {
132 context.log.info("Found a multi-page login");
133 return multi_page_login;
134 } else if (input_length == 1) {
135 context.log.info("Found a single-page login");
136 return single_page_login;
140 (await commands.js.run(
141 `return document.querySelectorAll("form").length;`
144 context.log.info("Found a single-form login");
145 return single_form_login;
152 * This function sets up the login for a single form.
154 * The username field is defined as the field which immediately precedes
155 * the password field. We have to do this in two steps because we need
156 * to make sure that the event we emit from the change has the `isTrusted`
157 * field set to `true`. Otherwise, some websites will ignore the input and
158 * the form submission.
160 async function single_page_login(login_info, context, commands, prefix = "") {
161 // Get the first input field in the form that is not hidden and add the
162 // username. Assumes that email/username is always the first input field.
163 await commands.addText.bySelector(
165 `${prefix}input:not([type=hidden]):not([type=password])`
168 // Get the password field and ensure it's not hidden.
169 await commands.addText.bySelector(
171 `${prefix}input[type=password]:not([type=hidden])`
178 * See single_page_login.
180 async function single_form_login(login_info, context, commands) {
181 return single_page_login(login_info, context, commands, "form ");
185 * Login to a website that uses multiple pages for the login.
187 * WARNING: Assumes that the first page is for the username.
189 async function multi_page_login(login_info, context, commands) {
190 const driver = context.selenium.driver;
191 const webdriver = context.selenium.webdriver;
193 const username_field = await driver.findElement(
194 webdriver.By.css(`input:not([type=hidden]):not([type=password])`)
196 await username_field.sendKeys(login_info.username);
197 await username_field.sendKeys(webdriver.Key.ENTER);
198 await commands.wait.byTime(5000);
202 password_field = await driver.findElement(
203 webdriver.By.css(`input[type=password]:not([type=hidden])`)
206 if (err.toString().includes("NoSuchElementError")) {
207 // Sometimes we're suspicious (i.e. they think we're a bot/bad-actor)
208 let name_field = await driver.findElement(
209 webdriver.By.css(`input:not([type=hidden]):not([type=password])`)
211 await name_field.sendKeys(login_info.suspicious_answer);
212 await name_field.sendKeys(webdriver.Key.ENTER);
213 await commands.wait.byTime(5000);
215 // Try getting the password field again
216 password_field = await driver.findElement(
217 webdriver.By.css(`input[type=password]:not([type=hidden])`)
224 await password_field.sendKeys(login_info.password);
226 return async function () {
227 password_field.sendKeys(webdriver.Key.ENTER);
228 await commands.wait.byTime(5000);
233 * This function sets up the login.
235 * This is done by first the login type, and then performing the
236 * actual login setup. The return is a possible button to click
237 * to perform the login.
239 async function setup_login(login_info, context, commands) {
240 let login_func = await get_login_type(context, commands);
242 throw Error("Could not determine the type of login page.");
246 return await login_func(login_info, context, commands);
248 throw Error(`Could not setup login information: ${err}`);
253 * This function performs the login.
255 * It does this by either clicking on a button with a type
256 * of "sumbit", or running a final_button function that was
257 * obtained from the setup_login function. Some pages also ask
258 * questions about setting up 2FA or other information. Generally,
259 * these contain the "skip" text.
261 async function login(context, commands, final_button) {
264 // The mouse double click emits an event with `evt.isTrusted=true`
265 await commands.mouse.doubleClick.bySelector("button[type=submit]");
266 await commands.wait.byTime(10000);
268 // In some cases, it's preferable to be given a function for the final button
269 await final_button();
272 // Some pages ask to setup 2FA, skip this based on the text
274 "//a[contains(text(), 'skip')]",
275 "//button[contains(text(), 'skip')]",
276 "//input[contains(text(), 'skip')]",
277 "//div[contains(text(), 'skip')]",
280 for (let xpath of XPATHS) {
282 await commands.mouse.doubleClick.byXpath(xpath);
284 if (err.toString().includes("not double click")) {
285 context.log.info(`Can't find a button with the text: ${xpath}`);
293 `Could not login to website as we could not find the submit button/input: ${err}`
299 * Grab the base URL from the browsertime url.
301 * This is a necessary step for getting the login values from the Taskcluster
302 * secrets, which are hashed by the base URL.
304 * The first entry is the protocal, third is the top-level domain (or host)
306 function get_base_URL(fullUrl) {
307 let pathAsArray = fullUrl.split("/");
308 return pathAsArray[0] + "//" + pathAsArray[2];
312 * This function attempts the login-login sequence for a live pageload recording
314 async function perform_live_login(context, commands) {
315 let testUrl = context.options.browsertime.url;
317 let logins = await get_logins(context);
318 const baseUrl = get_base_URL(testUrl);
320 await commands.navigate("about:blank");
322 let login_info = logins.secret[baseUrl];
324 await commands.navigate(login_info.login_url);
326 context.log.info("Unable to acquire login information");
329 await commands.wait.byTime(5000);
331 let final_button = await setup_login(login_info, context, commands);
332 await login(context, commands, final_button);
335 async function dismissCookiePrompt(input_cmds, context, commands) {
336 context.log.info("Searching for cookie prompt elements...");
337 let cmds = input_cmds.split(";;;");
338 for (let cmdstr of cmds) {
339 let [cmd, ...args] = cmdstr.split(":::");
340 context.log.info(cmd, args);
341 let result = await commands.js.run(
342 `return document.evaluate("` +
344 `", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;`
347 context.log.info("Element found, clicking on it.");
348 await run_command(cmdstr, context, commands);
351 "Element not found! The cookie prompt may have not appeared, please check the screenshots."
357 async function pageload_test(context, commands) {
358 let testUrl = context.options.browsertime.url;
359 let secondaryUrl = context.options.browsertime.secondary_url;
360 let testName = context.options.browsertime.testName;
361 let dismissPrompt = context.options.browsertime.dismiss_cookie_prompt || "";
362 context.log.info(context.options.browsertime);
364 // Wait for browser to settle
365 await commands.wait.byTime(1000);
367 // If the user has RAPTOR_LOGINS configured correctly, a local login pageload
368 // test can be attempted. Otherwise if attempting it in CI, only sites with the
369 // associated MOZ_SCM_LEVEL will be attempted (e.g. Try = 1, autoland = 3)
370 if (context.options.browsertime.login) {
371 if (context.options.browsertime.manual_login) {
372 // Perform a manual login using the value given in manual_login
373 // as the amount of time to wait
374 await commands.navigate(testUrl);
376 `Waiting ${context.options.browsertime.manual_login}ms for login...`
378 await commands.wait.byTime(context.options.browsertime.manual_login);
380 process.env.RAPTOR_LOGINS ||
381 process.env.MOZ_SCM_LEVEL == 3 ||
382 SCM_1_LOGIN_SITES.includes(testName)
385 await perform_live_login(context, commands);
388 "Unable to login. Acquiring a recording without logging in"
390 context.log.info("Error:" + err);
394 NOTE: This is a login test but a manual login was not requested, and
395 we cannot find any logins defined in RAPTOR_LOGINS.
400 await commands.measure.start(testUrl);
401 await commands.wait.byTime(40000);
403 await dismissCookiePrompt(dismissPrompt, context, commands);
405 commands.screenshot.take("test_url_" + testName);
407 if (secondaryUrl !== null) {
408 // Wait for browser to settle
409 await commands.wait.byTime(1000);
411 await commands.measure.start(secondaryUrl);
412 commands.screenshot.take("secondary_url_" + testName);
415 // Wait for browser to settle
416 await commands.wait.byTime(1000);
420 * Converts a string such as `measure.start` into the
421 * actual function that is found in the `commands` module.
423 * XX: Find a way to share this function between
424 * perftest_record.js and browsertime_interactive.js
426 async function get_command_function(cmd, commands) {
428 throw new Error("A blank command was given.");
429 } else if (cmd.endsWith(".")) {
431 "An extra `.` was found at the end of this command: " + cmd
435 // `func` will hold the actual method that needs to be called,
436 // and the `parent_mod` is the context required to run the `func`
437 // method. Without that context, `this` becomes undefined in the browsertime
440 let parent_mod = null;
441 for (let func_part of cmd.split(".")) {
442 if (func_part == "") {
444 "An empty function part was found in the command: " + cmd
449 parent_mod = commands;
450 func = commands[func_part];
451 } else if (func !== undefined) {
453 func = func[func_part];
459 if (func == undefined) {
461 "The given command could not be found as a function: " + cmd
465 return [func, parent_mod];
469 * Performs an interactive test.
471 * These tests are interactive as the entire test is defined
472 * through a set of browsertime commands. This allows users
473 * to build arbitrary tests. Furthermore, interactive tests
474 * provide the ability to login to websites.
476 async function interactive_test(input_cmds, context, commands) {
477 let cmds = input_cmds.split(";;;");
480 if (context.options.browsertime.login) {
481 logins = await get_logins(context);
484 await commands.navigate("about:blank");
486 let user_setup = false;
487 let final_button = null;
488 for (let cmdstr of cmds) {
489 let [cmd, ...args] = cmdstr.split(":::");
491 if (cmd == "setup_login") {
494 "This test is not specified as a `login` test so no login information is available."
497 if (args.length < 1 || args[0] == "") {
499 `No URL given, can't tell where to setup the login. We only accept: ${logins.keys()}`
502 /* Structure for logins is:
506 "suspicious_answer": ...,
510 let login_info = logins.secret[args[0]];
512 await commands.navigate(login_info.login_url);
513 await commands.wait.byTime(5000);
515 final_button = await setup_login(login_info, context, commands);
517 } else if (cmd == "login") {
519 throw Error("setup_login needs to be called before the login command");
521 await login(context, commands, final_button);
523 await run_command(cmdstr, context, commands);
528 async function run_command(cmdstr, context, commands) {
529 let [cmd, ...args] = cmdstr.split(":::");
530 let [func, parent_mod] = await get_command_function(cmd, commands);
533 await func.call(parent_mod, ...args);
536 `Exception found while running \`commands.${cmd}(${args})\`: ` + e
541 async function test(context, commands) {
542 let input_cmds = context.options.browsertime.commands;
543 let test_type = context.options.browsertime.testType;
544 if (test_type == "interactive") {
545 await interactive_test(input_cmds, context, commands);
547 await pageload_test(context, commands);
554 owner: "Bebe fstrugariu@mozilla.com",
555 name: "Mozproxy recording generator",
557 description: ` This test generates fresh MozProxy recordings. It iterates through a list of
558 websites provided in *_sites.json and for each one opens a browser and
559 records all the associated HTTP traffic`,
561 "mach perftest --proxy --hooks testing/raptor/recorder/hooks.py testing/raptor/recorder/perftest_record.js",