browsertime_pageload.js (13771B)
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/. */ 4 5 /* eslint-env node */ 6 7 const fs = require("fs"); 8 const http = require("http"); 9 const { logTest, logTask } = require("./utils/profiling"); 10 11 const URL = "/secrets/v1/secret/project/perftest/gecko/level-"; 12 const SECRET = "/perftest-login"; 13 const DEFAULT_SERVER = "https://firefox-ci-tc.services.mozilla.com"; 14 15 const SCM_LOGIN_SITES = ["facebook", "netflix"]; 16 17 /** 18 * This function obtains the perftest secret from Taskcluster. 19 * 20 * It will NOT work locally. Please see the get_logins function, you 21 * will need to define a JSON file and set the RAPTOR_LOGINS 22 * env variable to its path. 23 */ 24 async function get_tc_secrets(context) { 25 const MOZ_AUTOMATION = process.env.MOZ_AUTOMATION; 26 if (!MOZ_AUTOMATION) { 27 throw Error( 28 "Not running in CI. Set RAPTOR_LOGINS to a JSON file containing the logins." 29 ); 30 } 31 32 let TASKCLUSTER_PROXY_URL = process.env.TASKCLUSTER_PROXY_URL 33 ? process.env.TASKCLUSTER_PROXY_URL 34 : DEFAULT_SERVER; 35 36 let MOZ_SCM_LEVEL = process.env.MOZ_SCM_LEVEL ? process.env.MOZ_SCM_LEVEL : 1; 37 38 const url = TASKCLUSTER_PROXY_URL + URL + MOZ_SCM_LEVEL + SECRET; 39 40 const data = await new Promise((resolve, reject) => { 41 context.log.info("Obtaining secrets for login..."); 42 43 http.get( 44 url, 45 { 46 headers: { 47 "Content-Type": "application/json", 48 Accept: "application/json", 49 }, 50 }, 51 res => { 52 let data = ""; 53 context.log.info(`Secret status code: ${res.statusCode}`); 54 55 res.on("data", d => { 56 data += d.toString(); 57 }); 58 59 res.on("end", () => { 60 resolve(data); 61 }); 62 63 res.on("error", error => { 64 context.log.error(error); 65 reject(error); 66 }); 67 } 68 ); 69 }); 70 71 return JSON.parse(data); 72 } 73 74 /** 75 * This function gets the login information required. 76 * 77 * It starts by looking for a local file whose path is defined 78 * within RAPTOR_LOGINS. If we don't find this file, then we'll 79 * attempt to get the login information from our Taskcluster secret. 80 * If MOZ_AUTOMATION is undefined, then the test will fail, Taskcluster 81 * secrets can only be obtained in CI. 82 */ 83 async function get_logins(context) { 84 let logins; 85 86 let RAPTOR_LOGINS = process.env.RAPTOR_LOGINS; 87 if (RAPTOR_LOGINS) { 88 // Get logins from a local file 89 if (!RAPTOR_LOGINS.endsWith(".json")) { 90 throw Error( 91 `File given for logins does not end in '.json': ${RAPTOR_LOGINS}` 92 ); 93 } 94 95 let logins_file = null; 96 try { 97 logins_file = await fs.readFileSync(RAPTOR_LOGINS, "utf8"); 98 } catch (err) { 99 throw Error(`Failed to read the file ${RAPTOR_LOGINS}: ${err}`); 100 } 101 102 logins = await JSON.parse(logins_file); 103 } else { 104 // Get logins from a perftest Taskcluster secret 105 logins = await get_tc_secrets(context); 106 } 107 108 return logins; 109 } 110 111 /** 112 * This function returns the type of login to do. 113 * 114 * This function returns "single-form" when we find a single form. If we only 115 * find a single input field, we assume that there is one page per input 116 * and return "multi-page". Otherwise, we return null. 117 */ 118 async function get_login_type(context, commands) { 119 /* 120 Determine if there's a password field visible with this 121 query selector. Some sites use `tabIndex` to hide the password 122 field behind other elements. In this case, we are searching 123 for any password-type field that has a tabIndex of 0 or undefined and 124 is not hidden. 125 */ 126 let input_length = await commands.js.run(` 127 return document.querySelectorAll( 128 "input[type=password][tabIndex='0']:not([type=hidden])," + 129 "input[type=password]:not([tabIndex]):not([type=hidden])" 130 ).length; 131 `); 132 133 if (input_length == 0) { 134 context.log.info("Found a multi-page login"); 135 return multi_page_login; 136 } else if (input_length == 1) { 137 context.log.info("Found a single-page login"); 138 return single_page_login; 139 } 140 141 if ( 142 (await commands.js.run( 143 `return document.querySelectorAll("form").length;` 144 )) >= 1 145 ) { 146 context.log.info("Found a single-form login"); 147 return single_form_login; 148 } 149 150 return null; 151 } 152 153 /** 154 * This function sets up the login for a single form. 155 * 156 * The username field is defined as the field which immediately precedes 157 * the password field. We have to do this in two steps because we need 158 * to make sure that the event we emit from the change has the `isTrusted` 159 * field set to `true`. Otherwise, some websites will ignore the input and 160 * the form submission. 161 */ 162 async function single_page_login(login_info, context, commands, prefix = "") { 163 // Get the first input field in the form that is not hidden and add the 164 // username. Assumes that email/username is always the first input field. 165 await commands.addText.bySelector( 166 login_info.username, 167 `${prefix}input:not([type=hidden]):not([type=password])` 168 ); 169 170 // Get the password field and ensure it's not hidden. 171 await commands.addText.bySelector( 172 login_info.password, 173 `${prefix}input[type=password]:not([type=hidden])` 174 ); 175 176 return undefined; 177 } 178 179 /** 180 * See single_page_login. 181 */ 182 async function single_form_login(login_info, context, commands) { 183 return single_page_login(login_info, context, commands, "form "); 184 } 185 186 /** 187 * Login to a website that uses multiple pages for the login. 188 * 189 * WARNING: Assumes that the first page is for the username. 190 */ 191 // TODO cleanup comments 192 async function multi_page_login(login_info, context, commands) { 193 const driver = context.selenium.driver; 194 const webdriver = context.selenium.webdriver; 195 //TODO fails here in netflix for Try... 196 const username_field = await driver.findElement( 197 webdriver.By.css(`input:not([type=hidden]):not([type=password])`) 198 ); 199 await username_field.sendKeys(login_info.username); 200 await username_field.sendKeys(webdriver.Key.ENTER); 201 await commands.wait.byTime(5000); 202 let password_field; 203 try { 204 password_field = await driver.findElement( 205 webdriver.By.css(`input[type=password]:not([type=hidden])`) 206 ); 207 } catch (err) { 208 if (err.toString().includes("NoSuchElementError")) { 209 // Sometimes we're suspicious (i.e. they think we're a bot/bad-actor) 210 let name_field = await driver.findElement( 211 webdriver.By.css(`input:not([type=hidden]):not([type=password])`) 212 ); 213 await name_field.sendKeys(login_info.suspicious_answer); 214 await name_field.sendKeys(webdriver.Key.ENTER); 215 await commands.wait.byTime(5000); 216 217 // Try getting the password field again 218 password_field = await driver.findElement( 219 webdriver.By.css(`input[type=password]:not([type=hidden])`) 220 ); 221 } else { 222 throw err; 223 } 224 } 225 226 await password_field.sendKeys(login_info.password); 227 228 return async function () { 229 password_field.sendKeys(webdriver.Key.ENTER); 230 await commands.wait.byTime(5000); 231 }; 232 } 233 234 /** 235 * This function sets up the login. 236 * 237 * This is done by first the login type, and then performing the 238 * actual login setup. The return is a possible button to click 239 * to perform the login. 240 */ 241 async function setup_login(login_info, context, commands) { 242 let login_func = await get_login_type(context, commands); 243 if (!login_func) { 244 throw Error("Could not determine the type of login page."); 245 } 246 247 try { 248 return await login_func(login_info, context, commands); 249 } catch (err) { 250 throw Error(`Could not setup login information: ${err}`); 251 } 252 } 253 254 /** 255 * This function performs the login. 256 * 257 * It does this by either clicking on a button with a type 258 * of "sumbit", or running a final_button function that was 259 * obtained from the setup_login function. Some pages also ask 260 * questions about setting up 2FA or other information. Generally, 261 * these contain the "skip" text. 262 */ 263 async function login(context, commands, final_button) { 264 try { 265 if (!final_button) { 266 // The mouse double click emits an event with `evt.isTrusted=true` 267 await commands.mouse.doubleClick.bySelector("button[type=submit]"); 268 await commands.wait.byTime(10000); 269 } else { 270 // In some cases, it's preferable to be given a function for the final button 271 await final_button(); 272 } 273 274 // Some pages ask to setup 2FA, skip this based on the text 275 const XPATHS = [ 276 "//a[contains(text(), 'skip')]", 277 "//button[contains(text(), 'skip')]", 278 "//input[contains(text(), 'skip')]", 279 "//div[contains(text(), 'skip')]", 280 ]; 281 282 for (let xpath of XPATHS) { 283 try { 284 await commands.mouse.doubleClick.byXpath(xpath); 285 } catch (err) { 286 if (err.toString().includes("not double click")) { 287 context.log.info(`Can't find a button with the text: ${xpath}`); 288 } else { 289 throw err; 290 } 291 } 292 } 293 } catch (err) { 294 throw Error( 295 `Could not login to website as we could not find the submit button/input: ${err}` 296 ); 297 } 298 } 299 300 /** 301 * Grab the base URL from the browsertime url. 302 * 303 * This is a necessary step for getting the login values from the Taskcluster 304 * secrets, which are hashed by the base URL. 305 * 306 * The first entry is the protocal, third is the top-level domain (or host) 307 */ 308 function get_base_URL(fullUrl) { 309 let pathAsArray = fullUrl.split("/"); 310 return pathAsArray[0] + "//" + pathAsArray[2]; 311 } 312 313 /** 314 * This function attempts the login-login sequence for a live pageload test 315 */ 316 async function perform_live_login(context, commands) { 317 let testUrl = context.options.browsertime.url; 318 319 let logins = await get_logins(context); 320 const baseUrl = get_base_URL(testUrl); 321 322 await commands.navigate("about:blank"); 323 324 let login_info = logins.secret[baseUrl]; 325 try { 326 await commands.navigate(login_info.login_url); 327 } catch (err) { 328 context.log.info("Unable to acquire login information"); 329 throw err; 330 } 331 await commands.wait.byTime(10000); 332 333 let final_button = await setup_login(login_info, context, commands); 334 await login(context, commands, final_button); 335 } 336 337 module.exports = logTest( 338 "browsertime pageload", 339 async function (context, commands) { 340 context.log.info("Starting a browsertime pageload"); 341 let test_url = context.options.browsertime.url; 342 let secondary_url = context.options.browsertime.secondary_url; 343 let page_cycles = context.options.browsertime.page_cycles; 344 let page_cycle_delay = context.options.browsertime.page_cycle_delay; 345 let post_startup_delay = context.options.browsertime.post_startup_delay; 346 let chimera_mode = context.options.browsertime.chimera; 347 let test_bytecode_cache = context.options.browsertime.test_bytecode_cache; 348 let login_required = context.options.browsertime.loginRequired; 349 let live_site = context.options.browsertime.liveSite; 350 let test_name = context.options.browsertime.testName; 351 352 context.log.info( 353 "Waiting for %d ms (post_startup_delay)", 354 post_startup_delay 355 ); 356 await commands.wait.byTime(post_startup_delay); 357 let cached = false; 358 359 // Login once before testing the test_url/secondary_url cycles 360 // If the user has RAPTOR_LOGINS configured correctly, a local login pageload 361 // test can be attempted. Otherwise if attempting it in CI, only sites with the 362 // associated MOZ_SCM_LEVEL will be attempted (e.g. Try = 1, autoland = 3). 363 // In addition, ensure login sequence is only attempted on live sites and for sites 364 // that we have Taskcluster secrets for. 365 if ( 366 login_required == "True" && 367 live_site == "True" && 368 SCM_LOGIN_SITES.includes(test_name) 369 ) { 370 await perform_live_login(context, commands); 371 } 372 373 for (let count = 0; count < page_cycles; count++) { 374 await logTask(context, "cycle " + count, async function () { 375 if (count !== 0 && secondary_url !== undefined) { 376 context.log.info("Navigating to secondary url:" + secondary_url); 377 await commands.navigate(secondary_url); 378 await commands.wait.byTime(1000); 379 await commands.js.runAndWait(` 380 (function() { 381 const white = document.createElement('div'); 382 white.id = 'raptor-white'; 383 white.style.position = 'absolute'; 384 white.style.top = '0'; 385 white.style.left = '0'; 386 white.style.width = Math.max(document.documentElement.clientWidth, document.body.clientWidth) + 'px'; 387 white.style.height = Math.max(document.documentElement.clientHeight,document.body.clientHeight) + 'px'; 388 white.style.backgroundColor = 'white'; 389 white.style.zIndex = '2147483647'; 390 document.body.appendChild(white); 391 document.body.style.display = ''; 392 })();`); 393 await commands.wait.byTime(1000); 394 } else { 395 context.log.info("Navigating to about:blank, count: " + count); 396 await commands.navigate("about:blank"); 397 } 398 399 context.log.info("Navigating to primary url:" + test_url); 400 context.log.info( 401 "Cycle %d, waiting for %d ms", 402 count, 403 page_cycle_delay 404 ); 405 await commands.wait.byTime(page_cycle_delay); 406 407 context.log.info("Cycle %d, starting the measure", count); 408 await commands.measure.start(test_url); 409 410 // Wait 20 seconds to populate bytecode cache 411 if ( 412 test_bytecode_cache == "true" && 413 chimera_mode == "true" && 414 !cached 415 ) { 416 context.log.info("Waiting 20s to populate bytecode cache..."); 417 await commands.wait.byTime(20000); 418 cached = true; 419 } 420 }); 421 } 422 423 context.log.info("Browsertime pageload ended."); 424 return true; 425 } 426 );