browser_resources_console_messages.js (17748B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Test the ResourceCommand API around CONSOLE_MESSAGE 7 // 8 // Reproduces assertions from: devtools/shared/webconsole/test/chrome/test_cached_messages.html 9 // And now more. Once we remove the console actor's startListeners in favor of watcher class 10 // We could remove that other old test. 11 12 const FISSION_TEST_URL = URL_ROOT_SSL + "fission_document.html"; 13 const IFRAME_URL = URL_ROOT_ORG_SSL + "fission_iframe.html"; 14 15 add_task(async function () { 16 info("Execute test in top level document"); 17 await testTabConsoleMessagesResources(false); 18 await testTabConsoleMessagesResourcesWithIgnoreExistingResources(false); 19 20 info("Execute test in an iframe document, possibly remote with fission"); 21 await testTabConsoleMessagesResources(true); 22 await testTabConsoleMessagesResourcesWithIgnoreExistingResources(true); 23 }); 24 25 async function testTabConsoleMessagesResources(executeInIframe) { 26 const tab = await addTab(FISSION_TEST_URL); 27 28 const { client, resourceCommand, targetCommand } = 29 await initResourceCommand(tab); 30 31 info( 32 "Log some messages *before* calling ResourceCommand.watchResources in order to " + 33 "assert the behavior of already existing messages." 34 ); 35 await logExistingMessages(tab.linkedBrowser, executeInIframe); 36 37 const targetDocumentUrl = executeInIframe ? IFRAME_URL : FISSION_TEST_URL; 38 39 let runtimeDoneResolve; 40 const expectedExistingCalls = 41 getExpectedExistingConsoleCalls(targetDocumentUrl); 42 const expectedRuntimeCalls = 43 getExpectedRuntimeConsoleCalls(targetDocumentUrl); 44 const onRuntimeDone = new Promise(resolve => (runtimeDoneResolve = resolve)); 45 const onAvailable = resources => { 46 for (const resource of resources) { 47 is( 48 resource.resourceType, 49 resourceCommand.TYPES.CONSOLE_MESSAGE, 50 "Received a message" 51 ); 52 const isCachedMessage = !!expectedExistingCalls.length; 53 const expected = ( 54 isCachedMessage ? expectedExistingCalls : expectedRuntimeCalls 55 ).shift(); 56 checkConsoleAPICall(resource, expected); 57 is( 58 resource.isAlreadyExistingResource, 59 isCachedMessage, 60 "isAlreadyExistingResource has the expected value" 61 ); 62 63 if (!expectedRuntimeCalls.length) { 64 runtimeDoneResolve(); 65 } 66 } 67 }; 68 69 await resourceCommand.watchResources( 70 [resourceCommand.TYPES.CONSOLE_MESSAGE], 71 { 72 onAvailable, 73 } 74 ); 75 is( 76 expectedExistingCalls.length, 77 0, 78 "Got the expected number of existing messages" 79 ); 80 81 info( 82 "Now log messages *after* the call to ResourceCommand.watchResources and after having received all existing messages" 83 ); 84 await logRuntimeMessages(tab.linkedBrowser, executeInIframe); 85 86 info("Waiting for all runtime messages"); 87 await onRuntimeDone; 88 89 is( 90 expectedRuntimeCalls.length, 91 0, 92 "Got the expected number of runtime messages" 93 ); 94 95 targetCommand.destroy(); 96 await client.close(); 97 } 98 99 async function testTabConsoleMessagesResourcesWithIgnoreExistingResources( 100 executeInIframe 101 ) { 102 info("Test ignoreExistingResources option for console messages"); 103 const tab = await addTab(FISSION_TEST_URL); 104 105 const { client, resourceCommand, targetCommand } = 106 await initResourceCommand(tab); 107 108 info( 109 "Check whether onAvailable will not be called with existing console messages" 110 ); 111 await logExistingMessages(tab.linkedBrowser, executeInIframe); 112 113 const availableResources = []; 114 await resourceCommand.watchResources( 115 [resourceCommand.TYPES.CONSOLE_MESSAGE], 116 { 117 onAvailable: resources => availableResources.push(...resources), 118 ignoreExistingResources: true, 119 } 120 ); 121 is( 122 availableResources.length, 123 0, 124 "onAvailable wasn't called for existing console messages" 125 ); 126 127 info( 128 "Check whether onAvailable will be called with the future console messages" 129 ); 130 await logRuntimeMessages(tab.linkedBrowser, executeInIframe); 131 const targetDocumentUrl = executeInIframe ? IFRAME_URL : FISSION_TEST_URL; 132 const expectedRuntimeConsoleCalls = 133 getExpectedRuntimeConsoleCalls(targetDocumentUrl); 134 await waitUntil( 135 () => availableResources.length === expectedRuntimeConsoleCalls.length 136 ); 137 const expectedTargetFront = executeInIframe 138 ? targetCommand 139 .getAllTargets([targetCommand.TYPES.FRAME]) 140 .find(target => target.url == IFRAME_URL) 141 : targetCommand.targetFront; 142 for (let i = 0; i < expectedRuntimeConsoleCalls.length; i++) { 143 const resource = availableResources[i]; 144 is( 145 resource.targetFront, 146 expectedTargetFront, 147 "The targetFront property is the expected one" 148 ); 149 const expected = expectedRuntimeConsoleCalls[i]; 150 checkConsoleAPICall(resource, expected); 151 is( 152 resource.isAlreadyExistingResource, 153 false, 154 "isAlreadyExistingResource is false since we're ignoring existing resources" 155 ); 156 } 157 158 targetCommand.destroy(); 159 await client.close(); 160 } 161 162 async function logExistingMessages(browser, executeInIframe) { 163 let browsingContext = browser.browsingContext; 164 if (executeInIframe) { 165 browsingContext = await SpecialPowers.spawn( 166 browser, 167 [], 168 function frameScript() { 169 return content.document.querySelector("iframe").browsingContext; 170 } 171 ); 172 } 173 return evalInBrowsingContext(browsingContext, function pageScript() { 174 console.log("foobarBaz-log", undefined); 175 console.info("foobarBaz-info", null); 176 console.warn("foobarBaz-warn", document.body); 177 }); 178 } 179 180 /** 181 * Helper function similar to spawn, but instead of executing the script 182 * as a Frame Script, with privileges and including test harness in stacktraces, 183 * execute the script as a regular page script, without privileges and without any 184 * preceding stack. 185 * 186 * @param {BrowsingContext} The browsing context into which the script should be evaluated 187 * @param {Function | string} The JS to execute in the browsing context 188 * 189 * @return {Promise} Which resolves once the JS is done executing in the page 190 */ 191 function evalInBrowsingContext(browsingContext, script) { 192 return SpecialPowers.spawn(browsingContext, [String(script)], expr => { 193 const document = content.document; 194 const scriptEl = document.createElement("script"); 195 document.body.appendChild(scriptEl); 196 // Force the immediate execution of the stringified JS function passed in `expr` 197 scriptEl.textContent = "new " + expr; 198 scriptEl.remove(); 199 }); 200 } 201 202 // For both existing and runtime messages, we execute console API 203 // from a page script evaluated via evalInBrowsingContext. 204 // Records here the function used to execute the script in the page. 205 const EXPECTED_FUNCTION_NAME = "pageScript"; 206 207 const NUMBER_REGEX = /^\d+$/; 208 // timeStamp are the result of a number in microsecond divided by 1000. 209 // so we can't expect a precise number of decimals, or even if there would 210 // be decimals at all. 211 const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/; 212 213 function getExpectedExistingConsoleCalls(documentFilename) { 214 const defaultProperties = { 215 filename: documentFilename, 216 columnNumber: NUMBER_REGEX, 217 lineNumber: NUMBER_REGEX, 218 timeStamp: FRACTIONAL_NUMBER_REGEX, 219 innerWindowID: NUMBER_REGEX, 220 chromeContext: undefined, 221 counter: undefined, 222 prefix: undefined, 223 private: undefined, 224 stacktrace: undefined, 225 styles: undefined, 226 timer: undefined, 227 }; 228 229 return [ 230 { 231 ...defaultProperties, 232 level: "log", 233 arguments: ["foobarBaz-log", { type: "undefined" }], 234 }, 235 { 236 ...defaultProperties, 237 level: "info", 238 arguments: ["foobarBaz-info", { type: "null" }], 239 }, 240 { 241 ...defaultProperties, 242 level: "warn", 243 arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }], 244 }, 245 ]; 246 } 247 248 const longString = new Array(DevToolsServer.LONG_STRING_LENGTH + 2).join("a"); 249 function getExpectedRuntimeConsoleCalls(documentFilename) { 250 const defaultStackFrames = [ 251 // This is the usage of "new " + expr from `evalInBrowsingContext` 252 { 253 filename: documentFilename, 254 lineNumber: NUMBER_REGEX, 255 columnNumber: NUMBER_REGEX, 256 }, 257 ]; 258 259 const defaultProperties = { 260 filename: documentFilename, 261 columnNumber: NUMBER_REGEX, 262 lineNumber: NUMBER_REGEX, 263 timeStamp: FRACTIONAL_NUMBER_REGEX, 264 innerWindowID: NUMBER_REGEX, 265 chromeContext: undefined, 266 counter: undefined, 267 prefix: undefined, 268 private: undefined, 269 stacktrace: undefined, 270 styles: undefined, 271 timer: undefined, 272 }; 273 274 return [ 275 { 276 ...defaultProperties, 277 level: "log", 278 arguments: ["foobarBaz-log", { type: "undefined" }], 279 }, 280 { 281 ...defaultProperties, 282 level: "log", 283 arguments: ["Float from not a number: NaN"], 284 }, 285 { 286 ...defaultProperties, 287 level: "log", 288 arguments: ["Float from string: 1.200000"], 289 }, 290 { 291 ...defaultProperties, 292 level: "log", 293 arguments: ["Float from number: 1.300000"], 294 }, 295 { 296 ...defaultProperties, 297 level: "log", 298 arguments: ["Float from number with precision: 1.00"], 299 }, 300 { 301 ...defaultProperties, 302 level: "log", 303 arguments: [ 304 // Even if a precision of 200 was requested, it's capped at 15 305 `Float from number with high precision: 2.${"0".repeat(15)}`, 306 ], 307 }, 308 { 309 ...defaultProperties, 310 level: "log", 311 arguments: ["Integer from number: 3"], 312 }, 313 { 314 ...defaultProperties, 315 level: "log", 316 arguments: ["Integer from number with precision: 04"], 317 }, 318 { 319 ...defaultProperties, 320 level: "log", 321 arguments: [ 322 // The precision is not capped for integers 323 `Integer from number with high precision: ${"5".padStart(200, "0")}`, 324 ], 325 }, 326 { 327 ...defaultProperties, 328 level: "log", 329 arguments: ["BigInt 123 and 456"], 330 }, 331 { 332 ...defaultProperties, 333 level: "log", 334 arguments: ["message with ", "style"], 335 styles: ["color: blue;", "background: red; font-size: 2em;"], 336 }, 337 { 338 ...defaultProperties, 339 level: "info", 340 arguments: ["foobarBaz-info", { type: "null" }], 341 }, 342 { 343 ...defaultProperties, 344 level: "warn", 345 arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }], 346 }, 347 { 348 ...defaultProperties, 349 level: "debug", 350 arguments: [{ type: "null" }], 351 }, 352 { 353 ...defaultProperties, 354 level: "trace", 355 stacktrace: [ 356 { 357 filename: documentFilename, 358 functionName: EXPECTED_FUNCTION_NAME, 359 }, 360 ...defaultStackFrames, 361 ], 362 }, 363 { 364 ...defaultProperties, 365 level: "dir", 366 arguments: [ 367 { 368 type: "object", 369 actor: /[a-z]/, 370 class: "HTMLDocument", 371 }, 372 { 373 type: "object", 374 actor: /[a-z]/, 375 class: "Location", 376 }, 377 ], 378 }, 379 { 380 ...defaultProperties, 381 level: "log", 382 arguments: [ 383 "foo", 384 { 385 type: "longString", 386 initial: longString.substring( 387 0, 388 DevToolsServer.LONG_STRING_INITIAL_LENGTH 389 ), 390 length: longString.length, 391 actor: /[a-z]/, 392 }, 393 ], 394 }, 395 { 396 ...defaultProperties, 397 level: "count", 398 arguments: ["myCounter"], 399 counter: { 400 count: 1, 401 label: "myCounter", 402 }, 403 }, 404 { 405 ...defaultProperties, 406 level: "count", 407 arguments: ["myCounter"], 408 counter: { 409 count: 2, 410 label: "myCounter", 411 }, 412 }, 413 { 414 ...defaultProperties, 415 level: "count", 416 arguments: ["default"], 417 counter: { 418 count: 1, 419 label: "default", 420 }, 421 }, 422 { 423 ...defaultProperties, 424 level: "countReset", 425 arguments: ["myCounter"], 426 counter: { 427 count: 0, 428 label: "myCounter", 429 }, 430 }, 431 { 432 ...defaultProperties, 433 level: "countReset", 434 arguments: ["unknownCounter"], 435 counter: { 436 error: "counterDoesntExist", 437 label: "unknownCounter", 438 }, 439 }, 440 { 441 ...defaultProperties, 442 level: "time", 443 arguments: ["myTimer"], 444 timer: { 445 name: "myTimer", 446 }, 447 }, 448 { 449 ...defaultProperties, 450 level: "time", 451 arguments: ["myTimer"], 452 timer: { 453 name: "myTimer", 454 error: "timerAlreadyExists", 455 }, 456 }, 457 { 458 ...defaultProperties, 459 level: "timeLog", 460 arguments: ["myTimer"], 461 timer: { 462 name: "myTimer", 463 duration: NUMBER_REGEX, 464 }, 465 }, 466 { 467 ...defaultProperties, 468 level: "timeEnd", 469 arguments: ["myTimer"], 470 timer: { 471 name: "myTimer", 472 duration: NUMBER_REGEX, 473 }, 474 }, 475 { 476 ...defaultProperties, 477 level: "time", 478 arguments: ["default"], 479 timer: { 480 name: "default", 481 }, 482 }, 483 { 484 ...defaultProperties, 485 level: "timeLog", 486 arguments: ["default"], 487 timer: { 488 name: "default", 489 duration: NUMBER_REGEX, 490 }, 491 }, 492 { 493 ...defaultProperties, 494 level: "timeEnd", 495 arguments: ["default"], 496 timer: { 497 name: "default", 498 duration: NUMBER_REGEX, 499 }, 500 }, 501 { 502 ...defaultProperties, 503 level: "timeLog", 504 arguments: ["unknownTimer"], 505 timer: { 506 name: "unknownTimer", 507 error: "timerDoesntExist", 508 }, 509 }, 510 { 511 ...defaultProperties, 512 level: "timeEnd", 513 arguments: ["unknownTimer"], 514 timer: { 515 name: "unknownTimer", 516 error: "timerDoesntExist", 517 }, 518 }, 519 { 520 ...defaultProperties, 521 level: "error", 522 arguments: ["foobarBaz-asmjs-error", { type: "undefined" }], 523 524 stacktrace: [ 525 { 526 filename: documentFilename, 527 functionName: "fromAsmJS", 528 }, 529 { 530 filename: documentFilename, 531 functionName: "inAsmJS2", 532 }, 533 { 534 filename: documentFilename, 535 functionName: "inAsmJS1", 536 }, 537 { 538 filename: documentFilename, 539 functionName: EXPECTED_FUNCTION_NAME, 540 }, 541 ...defaultStackFrames, 542 ], 543 }, 544 { 545 ...defaultProperties, 546 level: "log", 547 filename: 548 "chrome://mochitests/content/browser/devtools/shared/commands/resource/tests/browser_resources_console_messages.js", 549 arguments: [ 550 { 551 type: "object", 552 actor: /[a-z]/, 553 class: "Restricted", 554 }, 555 ], 556 chromeContext: true, 557 }, 558 ]; 559 } 560 561 async function logRuntimeMessages(browser, executeInIframe) { 562 let browsingContext = browser.browsingContext; 563 if (executeInIframe) { 564 browsingContext = await SpecialPowers.spawn( 565 browser, 566 [], 567 function frameScript() { 568 return content.document.querySelector("iframe").browsingContext; 569 } 570 ); 571 } 572 // First inject LONG_STRING_LENGTH in global scope it order to easily use it after 573 await evalInBrowsingContext( 574 browsingContext, 575 `function () {window.LONG_STRING_LENGTH = ${DevToolsServer.LONG_STRING_LENGTH};}` 576 ); 577 await evalInBrowsingContext(browsingContext, function pageScript() { 578 const _longString = new Array(window.LONG_STRING_LENGTH + 2).join("a"); 579 580 console.log("foobarBaz-log", undefined); 581 582 console.log("Float from not a number: %f", "foo"); 583 console.log("Float from string: %f", "1.2"); 584 console.log("Float from number: %f", 1.3); 585 console.log("Float from number with precision: %.2f", 1); 586 console.log("Float from number with high precision: %.200f", 2); 587 console.log("Integer from number: %i", 3.14); 588 console.log("Integer from number with precision: %.2i", 4); 589 console.log("Integer from number with high precision: %.200i", 5); 590 console.log("BigInt %d and %i", 123n, 456n); 591 console.log( 592 "%cmessage with %cstyle", 593 "color: blue;", 594 "background: red; font-size: 2em;" 595 ); 596 597 console.info("foobarBaz-info", null); 598 console.warn("foobarBaz-warn", document.documentElement); 599 console.debug(null); 600 console.trace(); 601 console.dir(document, location); 602 console.log("foo", _longString); 603 604 console.count("myCounter"); 605 console.count("myCounter"); 606 console.count(); 607 console.countReset("myCounter"); 608 // will cause warnings because unknownCounter doesn't exist 609 console.countReset("unknownCounter"); 610 611 console.time("myTimer"); 612 // will cause warning because myTimer already exist 613 console.time("myTimer"); 614 console.timeLog("myTimer"); 615 console.timeEnd("myTimer"); 616 console.time(); 617 console.timeLog(); 618 console.timeEnd(); 619 // // will cause warnings because unknownTimer doesn't exist 620 console.timeLog("unknownTimer"); 621 console.timeEnd("unknownTimer"); 622 623 function fromAsmJS() { 624 console.error("foobarBaz-asmjs-error", undefined); 625 } 626 627 (function (global, foreign) { 628 "use asm"; 629 function inAsmJS2() { 630 foreign.fromAsmJS(); 631 } 632 function inAsmJS1() { 633 inAsmJS2(); 634 } 635 return inAsmJS1; 636 })(null, { fromAsmJS })(); 637 }); 638 await SpecialPowers.spawn(browsingContext, [], function frameScript() { 639 const sandbox = new Cu.Sandbox(null, { invisibleToDebugger: true }); 640 const sandboxObj = sandbox.eval("new Object"); 641 content.console.log(sandboxObj); 642 }); 643 } 644 645 // Copied from devtools/shared/webconsole/test/chrome/common.js 646 function checkConsoleAPICall(call, expected) { 647 is( 648 call.arguments?.length || 0, 649 expected.arguments?.length || 0, 650 "number of arguments" 651 ); 652 653 checkObject(call, expected); 654 }