browser_resources_document_events.js (22223B)
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 DOCUMENT_EVENT 7 8 add_task(async function () { 9 await testDocumentEventResources(); 10 await testDocumentEventResourcesWithIgnoreExistingResources(); 11 await testDomCompleteWithOverloadedConsole(); 12 await testIframeNavigation(); 13 await testBfCacheNavigation(); 14 await testDomCompleteWithWindowStop(); 15 await testCrossOriginNavigation(); 16 await testDomCompleteWithOfflineDocument(); 17 }); 18 19 async function testDocumentEventResources() { 20 info("Test ResourceCommand for DOCUMENT_EVENT"); 21 22 // Open a test tab 23 const title = "DocumentEventsTitle"; 24 const url = `data:text/html,<title>${title}</title>Document Events`; 25 const tab = await addTab(url); 26 27 const listener = new ResourceListener(); 28 const { commands } = await initResourceCommand(tab); 29 30 info( 31 "Check whether the document events are fired correctly even when the document was already loaded" 32 ); 33 const onLoadingAtInit = listener.once("dom-loading"); 34 const onInteractiveAtInit = listener.once("dom-interactive"); 35 const onCompleteAtInit = listener.once("dom-complete"); 36 await commands.resourceCommand.watchResources( 37 [commands.resourceCommand.TYPES.DOCUMENT_EVENT], 38 { 39 onAvailable: parameters => listener.dispatch(parameters), 40 } 41 ); 42 await assertPromises( 43 commands, 44 // targetBeforeNavigation is only used when there is a will-navigate and a navigate, but there is none here 45 null, 46 // As we started watching on an already loaded document, and no navigation happened since we called watchResources, 47 // we don't have any will-navigate event 48 null, 49 onLoadingAtInit, 50 onInteractiveAtInit, 51 onCompleteAtInit 52 ); 53 ok( 54 true, 55 "Document events are fired even when the document was already loaded" 56 ); 57 let domLoadingResource = await onLoadingAtInit; 58 59 is( 60 domLoadingResource.url, 61 url, 62 `resource ${domLoadingResource.name} has expected url` 63 ); 64 is( 65 domLoadingResource.title, 66 undefined, 67 `resource ${domLoadingResource.name} does not have a title property` 68 ); 69 70 let domInteractiveResource = await onInteractiveAtInit; 71 is( 72 domInteractiveResource.url, 73 url, 74 `resource ${domInteractiveResource.name} has expected url` 75 ); 76 is( 77 domInteractiveResource.title, 78 title, 79 `resource ${domInteractiveResource.name} has expected title` 80 ); 81 let domCompleteResource = await onCompleteAtInit; 82 is( 83 domCompleteResource.url, 84 undefined, 85 `resource ${domCompleteResource.name} does not have a url property` 86 ); 87 is( 88 domCompleteResource.title, 89 undefined, 90 `resource ${domCompleteResource.name} does not have a title property` 91 ); 92 93 info("Check whether the document events are fired correctly when reloading"); 94 const onWillNavigate = listener.once("will-navigate"); 95 const onLoadingAtReloaded = listener.once("dom-loading"); 96 const onInteractiveAtReloaded = listener.once("dom-interactive"); 97 const onCompleteAtReloaded = listener.once("dom-complete"); 98 const targetBeforeNavigation = commands.targetCommand.targetFront; 99 gBrowser.reloadTab(tab); 100 await assertPromises( 101 commands, 102 targetBeforeNavigation, 103 onWillNavigate, 104 onLoadingAtReloaded, 105 onInteractiveAtReloaded, 106 onCompleteAtReloaded 107 ); 108 ok(true, "Document events are fired after reloading"); 109 110 domLoadingResource = await onLoadingAtReloaded; 111 is( 112 domLoadingResource.url, 113 url, 114 `resource ${domLoadingResource.name} has expected url after reloading` 115 ); 116 is( 117 domLoadingResource.title, 118 undefined, 119 `resource ${domLoadingResource.name} does not have a title property after reloading` 120 ); 121 122 domInteractiveResource = await onInteractiveAtInit; 123 is( 124 domInteractiveResource.url, 125 url, 126 `resource ${domInteractiveResource.name} has url property after reloading` 127 ); 128 is( 129 domInteractiveResource.title, 130 title, 131 `resource ${domInteractiveResource.name} has expected title after reloading` 132 ); 133 domCompleteResource = await onCompleteAtInit; 134 is( 135 domCompleteResource.url, 136 undefined, 137 `resource ${domCompleteResource.name} does not have a url property after reloading` 138 ); 139 is( 140 domCompleteResource.title, 141 undefined, 142 `resource ${domCompleteResource.name} does not have a title property after reloading` 143 ); 144 145 await commands.destroy(); 146 } 147 148 async function testDocumentEventResourcesWithIgnoreExistingResources() { 149 info("Test ignoreExistingResources option for DOCUMENT_EVENT"); 150 151 const tab = await addTab("data:text/html,Document Events"); 152 153 const { commands } = await initResourceCommand(tab); 154 155 info("Check whether the existing document events will not be fired"); 156 const documentEvents = []; 157 await commands.resourceCommand.watchResources( 158 [commands.resourceCommand.TYPES.DOCUMENT_EVENT], 159 { 160 onAvailable: resources => documentEvents.push(...resources), 161 ignoreExistingResources: true, 162 } 163 ); 164 is(documentEvents.length, 0, "Existing document events are not fired"); 165 166 info("Check whether the future document events are fired"); 167 const targetBeforeNavigation = commands.targetCommand.targetFront; 168 gBrowser.reloadTab(tab); 169 info( 170 "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" 171 ); 172 await waitFor(() => documentEvents.length === 4); 173 assertEvents({ commands, targetBeforeNavigation, documentEvents }); 174 175 await commands.destroy(); 176 } 177 178 async function testIframeNavigation() { 179 info("Test iframe navigations for DOCUMENT_EVENT"); 180 181 const tab = await addTab( 182 'https://example.com/document-builder.sjs?html=<iframe src="https://example.net/document-builder.sjs?html=net"></iframe>' 183 ); 184 const secondPageUrl = "https://example.org/document-builder.sjs?html=org"; 185 186 const { commands } = await initResourceCommand(tab); 187 188 let documentEvents = []; 189 await commands.resourceCommand.watchResources( 190 [commands.resourceCommand.TYPES.DOCUMENT_EVENT], 191 { 192 onAvailable: resources => documentEvents.push(...resources), 193 } 194 ); 195 is( 196 documentEvents.length, 197 6, 198 "We get two targets and two sets of events: dom-loading, dom-interactive, dom-complete" 199 ); 200 const [, iframeTarget] = await commands.targetCommand.getAllTargets([ 201 commands.targetCommand.TYPES.FRAME, 202 ]); 203 // Filter out each target events as their order to be random between the two targets 204 const topTargetEvents = documentEvents.filter( 205 r => r.targetFront == commands.targetCommand.targetFront 206 ); 207 const iframeTargetEvents = documentEvents.filter( 208 r => r.targetFront != commands.targetCommand.targetFront 209 ); 210 assertEvents({ 211 commands, 212 documentEvents: [null /* no will-navigate */, ...topTargetEvents], 213 }); 214 assertEvents({ 215 commands, 216 documentEvents: [null /* no will-navigate */, ...iframeTargetEvents], 217 expectedTargetFront: iframeTarget, 218 }); 219 220 info("Navigate the iframe to another process (if fission is enabled)"); 221 documentEvents = []; 222 await SpecialPowers.spawn( 223 gBrowser.selectedBrowser, 224 [secondPageUrl], 225 function (url) { 226 const iframe = content.document.querySelector("iframe"); 227 iframe.src = url; 228 } 229 ); 230 231 await waitFor(() => documentEvents.length >= 3); 232 is( 233 documentEvents.length, 234 3, 235 "We switch to a new target and get: dom-loading, dom-interactive, dom-complete (but no will-navigate as that's only for the top BrowsingContext)" 236 ); 237 const [, newIframeTarget] = await commands.targetCommand.getAllTargets([ 238 commands.targetCommand.TYPES.FRAME, 239 ]); 240 assertEvents({ 241 commands, 242 targetBeforeNavigation: iframeTarget, 243 documentEvents: [null /* no will-navigate */, ...documentEvents], 244 expectedTargetFront: newIframeTarget, 245 expectedNewURI: secondPageUrl, 246 }); 247 248 await commands.destroy(); 249 } 250 251 function isBfCacheInParentEnabled() { 252 return ( 253 Services.appinfo.sessionHistoryInParent && 254 Services.prefs.getBoolPref("fission.bfcacheInParent", false) 255 ); 256 } 257 258 async function testBfCacheNavigation() { 259 info("Test bfcache navigations for DOCUMENT_EVENT"); 260 261 info("Open a first document and navigate to a second one"); 262 const firstLocation = "data:text/html,<title>first</title>first page"; 263 const secondLocation = "data:text/html,<title>second</title>second page"; 264 const tab = await addTab(firstLocation); 265 const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 266 BrowserTestUtils.startLoadingURIString( 267 gBrowser.selectedBrowser, 268 secondLocation 269 ); 270 await onLoaded; 271 272 const { commands } = await initResourceCommand(tab); 273 274 const documentEvents = []; 275 await commands.resourceCommand.watchResources( 276 [commands.resourceCommand.TYPES.DOCUMENT_EVENT], 277 { 278 onAvailable: resources => { 279 documentEvents.push(...resources); 280 }, 281 ignoreExistingResources: true, 282 } 283 ); 284 // Wait for some time for extra safety 285 await wait(250); 286 is(documentEvents.length, 0, "Existing document events are not fired"); 287 288 info("Navigate back to the first page"); 289 const onSwitched = commands.targetCommand.once("switched-target"); 290 const targetBeforeNavigation = commands.targetCommand.targetFront; 291 gBrowser.goBack(); 292 293 if (isBfCacheInParentEnabled()) { 294 await onSwitched; 295 } 296 297 info( 298 "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" 299 ); 300 await waitFor(() => documentEvents.length >= 4); 301 /* Ignore will-navigate timestamp as all other DOCUMENT_EVENTS will be set at the original load date, 302 which is when we loaded from the network, and not when we loaded from bfcache */ 303 assertEvents({ 304 commands, 305 targetBeforeNavigation, 306 documentEvents, 307 ignoreWillNavigateTimestamp: true, 308 }); 309 310 // Wait for some time in order to let a chance to have duplicated dom-loading events 311 await wait(250); 312 313 is( 314 documentEvents.length, 315 4, 316 "There is no duplicated event and only the 4 expected DOCUMENT_EVENT states" 317 ); 318 const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] = 319 documentEvents; 320 321 is( 322 willNavigateEvent.name, 323 "will-navigate", 324 "The first DOCUMENT_EVENT is will-navigate" 325 ); 326 is( 327 loadingEvent.name, 328 "dom-loading", 329 "The second DOCUMENT_EVENT is dom-loading" 330 ); 331 is( 332 interactiveEvent.name, 333 "dom-interactive", 334 "The third DOCUMENT_EVENT is dom-interactive" 335 ); 336 is( 337 completeEvent.name, 338 "dom-complete", 339 "The fourth DOCUMENT_EVENT is dom-complete" 340 ); 341 342 is( 343 loadingEvent.url, 344 firstLocation, 345 `resource ${loadingEvent.name} has expected url after navigation back` 346 ); 347 is( 348 loadingEvent.title, 349 undefined, 350 `resource ${loadingEvent.name} does not have a title property after navigating back` 351 ); 352 353 is( 354 interactiveEvent.url, 355 firstLocation, 356 `resource ${interactiveEvent.name} has expected url property after navigating back` 357 ); 358 is( 359 interactiveEvent.title, 360 "first", 361 `resource ${interactiveEvent.name} has expected title after navigating back` 362 ); 363 364 is( 365 completeEvent.url, 366 undefined, 367 `resource ${completeEvent.name} does not have a url property after navigating back` 368 ); 369 is( 370 completeEvent.title, 371 undefined, 372 `resource ${completeEvent.name} does not have a title property after navigating back` 373 ); 374 375 await commands.destroy(); 376 } 377 378 async function testCrossOriginNavigation() { 379 info("Test cross origin navigations for DOCUMENT_EVENT"); 380 381 const tab = await addTab("https://example.com/document-builder.sjs?html=com"); 382 383 const { commands } = await initResourceCommand(tab); 384 385 const documentEvents = []; 386 await commands.resourceCommand.watchResources( 387 [commands.resourceCommand.TYPES.DOCUMENT_EVENT], 388 { 389 onAvailable: resources => documentEvents.push(...resources), 390 ignoreExistingResources: true, 391 } 392 ); 393 // Wait for some time for extra safety 394 await wait(250); 395 is(documentEvents.length, 0, "Existing document events are not fired"); 396 397 info("Navigate to another process"); 398 const onSwitched = commands.targetCommand.once("switched-target"); 399 const netUrl = 400 "https://example.net/document-builder.sjs?html=<head><title>titleNet</title></head>net"; 401 const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); 402 const targetBeforeNavigation = commands.targetCommand.targetFront; 403 BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, netUrl); 404 await onLoaded; 405 await onSwitched; 406 407 info( 408 "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" 409 ); 410 await waitFor(() => documentEvents.length >= 4); 411 assertEvents({ commands, targetBeforeNavigation, documentEvents }); 412 413 // Wait for some time in order to let a chance to have duplicated dom-loading events 414 await wait(250); 415 416 is( 417 documentEvents.length, 418 4, 419 "There is no duplicated event and only the 4 expected DOCUMENT_EVENT states" 420 ); 421 const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] = 422 documentEvents; 423 424 is( 425 willNavigateEvent.name, 426 "will-navigate", 427 "The first DOCUMENT_EVENT is will-navigate" 428 ); 429 is( 430 loadingEvent.name, 431 "dom-loading", 432 "The second DOCUMENT_EVENT is dom-loading" 433 ); 434 is( 435 interactiveEvent.name, 436 "dom-interactive", 437 "The third DOCUMENT_EVENT is dom-interactive" 438 ); 439 is( 440 completeEvent.name, 441 "dom-complete", 442 "The fourth DOCUMENT_EVENT is dom-complete" 443 ); 444 445 is( 446 loadingEvent.url, 447 encodeURI(netUrl), 448 `resource ${loadingEvent.name} has expected url after reloading` 449 ); 450 is( 451 loadingEvent.title, 452 undefined, 453 `resource ${loadingEvent.name} does not have a title property after reloading` 454 ); 455 456 is( 457 interactiveEvent.url, 458 encodeURI(netUrl), 459 `resource ${interactiveEvent.name} has expected url property after reloading` 460 ); 461 is( 462 interactiveEvent.title, 463 "titleNet", 464 `resource ${interactiveEvent.name} has expected title after reloading` 465 ); 466 467 is( 468 completeEvent.url, 469 undefined, 470 `resource ${completeEvent.name} does not have a url property after reloading` 471 ); 472 is( 473 completeEvent.title, 474 undefined, 475 `resource ${completeEvent.name} does not have a title property after reloading` 476 ); 477 478 await commands.destroy(); 479 } 480 481 async function testDomCompleteWithOverloadedConsole() { 482 info("Test dom-complete with an overloaded console object"); 483 484 const tab = await addTab( 485 "data:text/html,<script>window.console = {};</script>" 486 ); 487 488 const { client, resourceCommand, targetCommand } = 489 await initResourceCommand(tab); 490 491 info("Check that all DOCUMENT_EVENTS are fired for the already loaded page"); 492 const documentEvents = []; 493 await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { 494 onAvailable: resources => documentEvents.push(...resources), 495 }); 496 is(documentEvents.length, 3, "Existing document events are fired"); 497 498 const domComplete = documentEvents[2]; 499 is(domComplete.name, "dom-complete", "the last resource is the dom-complete"); 500 is( 501 domComplete.hasNativeConsoleAPI, 502 false, 503 "the console object is reported to be overloaded" 504 ); 505 506 targetCommand.destroy(); 507 await client.close(); 508 } 509 510 async function testDomCompleteWithWindowStop() { 511 info("Test dom-complete with a page calling window.stop()"); 512 513 const tab = await addTab("data:text/html,foo"); 514 515 const { commands, client, resourceCommand, targetCommand } = 516 await initResourceCommand(tab); 517 518 info("Check that all DOCUMENT_EVENTS are fired for the already loaded page"); 519 let documentEvents = []; 520 await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { 521 onAvailable: resources => documentEvents.push(...resources), 522 }); 523 is(documentEvents.length, 3, "Existing document events are fired"); 524 documentEvents = []; 525 526 const html = `<!DOCTYPE html><html> 527 <head> 528 <title>stopped page</title> 529 <script>window.stop();</script> 530 </head> 531 <body>Page content that shouldn't be displayed</body> 532 </html>`; 533 const secondLocation = "data:text/html," + encodeURIComponent(html); 534 const targetBeforeNavigation = commands.targetCommand.targetFront; 535 BrowserTestUtils.startLoadingURIString( 536 gBrowser.selectedBrowser, 537 secondLocation 538 ); 539 info( 540 "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" 541 ); 542 await waitFor(() => documentEvents.length === 4); 543 544 assertEvents({ commands, targetBeforeNavigation, documentEvents }); 545 546 targetCommand.destroy(); 547 await client.close(); 548 } 549 550 async function testDomCompleteWithOfflineDocument() { 551 info("Test dom-complete with an offline page"); 552 553 const tab = await addTab(`${URL_ROOT_SSL}empty.html`); 554 555 const { commands, client, resourceCommand, targetCommand } = 556 await initResourceCommand(tab); 557 558 info("Check that all DOCUMENT_EVENTS are fired for the already loaded page"); 559 let documentEvents = []; 560 await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { 561 onAvailable: resources => documentEvents.push(...resources), 562 }); 563 is(documentEvents.length, 3, "Existing document events are fired"); 564 documentEvents = []; 565 566 const targetBeforeNavigation = commands.targetCommand.targetFront; 567 tab.linkedBrowser.browsingContext.forceOffline = true; 568 gBrowser.reloadTab(tab); 569 570 // The offline mode may break Document Event Watcher and we would miss some of the expected events 571 info( 572 "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" 573 ); 574 await waitFor(() => documentEvents.length === 4); 575 576 // Only will-navigate will have a valid timestamp, as the page is failing loading 577 // and we get the offline notice page, the other events will be set to 0 578 assertEvents({ 579 commands, 580 targetBeforeNavigation, 581 documentEvents, 582 ignoreAllTimestamps: true, 583 }); 584 585 targetCommand.destroy(); 586 await client.close(); 587 } 588 589 async function assertPromises( 590 commands, 591 targetBeforeNavigation, 592 onWillNavigate, 593 onLoading, 594 onInteractive, 595 onComplete 596 ) { 597 const willNavigateEvent = await onWillNavigate; 598 const loadingEvent = await onLoading; 599 const interactiveEvent = await onInteractive; 600 const completeEvent = await onComplete; 601 assertEvents({ 602 commands, 603 targetBeforeNavigation, 604 documentEvents: [ 605 willNavigateEvent, 606 loadingEvent, 607 interactiveEvent, 608 completeEvent, 609 ], 610 }); 611 } 612 613 const isSlowPlatform = 614 AppConstants.ASAN || AppConstants.DEBUG || AppConstants.TSAN; 615 function assertEvents({ 616 commands, 617 targetBeforeNavigation, 618 documentEvents, 619 expectedTargetFront = commands.targetCommand.targetFront, 620 expectedNewURI = gBrowser.selectedBrowser.currentURI.spec, 621 // The will-navigate timestamp has a hardcoded offset of 20ms to be "earlier" 622 // than dom-content-loaded, but it frequently fails on debug/asan/tsan. 623 // The observed failures are within < 5ms. 624 ignoreWillNavigateTimestamp = isSlowPlatform, 625 ignoreAllTimestamps = false, 626 }) { 627 const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] = 628 documentEvents; 629 if (willNavigateEvent) { 630 is(willNavigateEvent.name, "will-navigate", "Received the will-navigate"); 631 is( 632 willNavigateEvent.newURI, 633 expectedNewURI, 634 "will-navigate newURI is set to the current tab new location" 635 ); 636 } 637 is( 638 loadingEvent.name, 639 "dom-loading", 640 "loading received in the exepected order" 641 ); 642 is( 643 interactiveEvent.name, 644 "dom-interactive", 645 "interactive received in the expected order" 646 ); 647 is(completeEvent.name, "dom-complete", "complete received last"); 648 649 if (willNavigateEvent) { 650 is( 651 typeof willNavigateEvent.time, 652 "number", 653 `Type of time attribute for will-navigate event is correct (${willNavigateEvent.time})` 654 ); 655 } 656 is( 657 typeof loadingEvent.time, 658 "number", 659 `Type of time attribute for loading event is correct (${loadingEvent.time})` 660 ); 661 is( 662 typeof interactiveEvent.time, 663 "number", 664 `Type of time attribute for interactive event is correct (${interactiveEvent.time})` 665 ); 666 is( 667 typeof completeEvent.time, 668 "number", 669 `Type of time attribute for complete event is correct (${completeEvent.time})` 670 ); 671 672 // In case of errors the timestamps may be set to 0. 673 if (!ignoreAllTimestamps) { 674 if (willNavigateEvent && !ignoreWillNavigateTimestamp) { 675 Assert.lessOrEqual( 676 willNavigateEvent.time, 677 loadingEvent.time, 678 `Timestamp for dom-loading event is greater than will-navigate event (${willNavigateEvent.time} <= ${loadingEvent.time})` 679 ); 680 } 681 Assert.lessOrEqual( 682 loadingEvent.time, 683 interactiveEvent.time, 684 `Timestamp for interactive event is greater than loading event (${loadingEvent.time} <= ${interactiveEvent.time})` 685 ); 686 Assert.lessOrEqual( 687 interactiveEvent.time, 688 completeEvent.time, 689 `Timestamp for complete event is greater than interactive event (${interactiveEvent.time} <= ${completeEvent.time}).` 690 ); 691 } 692 693 if (willNavigateEvent) { 694 // If we switched to a new target, this target will be different from currentTargetFront. 695 // This only happen if we navigate to another process or if server target switching is enabled. 696 is( 697 willNavigateEvent.targetFront, 698 targetBeforeNavigation, 699 "will-navigate target was the one before the navigation" 700 ); 701 } 702 is( 703 loadingEvent.targetFront, 704 expectedTargetFront, 705 "loading target is the expected one" 706 ); 707 is( 708 interactiveEvent.targetFront, 709 expectedTargetFront, 710 "interactive target is the expected one" 711 ); 712 is( 713 completeEvent.targetFront, 714 expectedTargetFront, 715 "complete target is the expected one" 716 ); 717 718 is( 719 completeEvent.hasNativeConsoleAPI, 720 true, 721 "None of the tests (except the dedicated one) overload the console object" 722 ); 723 } 724 725 class ResourceListener { 726 _listeners = new Map(); 727 728 dispatch(resources) { 729 for (const resource of resources) { 730 const resolve = this._listeners.get(resource.name); 731 if (resolve) { 732 resolve(resource); 733 this._listeners.delete(resource.name); 734 } 735 } 736 } 737 738 once(resourceName) { 739 return new Promise(r => this._listeners.set(resourceName, r)); 740 } 741 }