/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Test the ResourceCommand API around DOCUMENT_EVENT add_task(async function () { await testDocumentEventResources(); await testDocumentEventResourcesWithIgnoreExistingResources(); await testDomCompleteWithOverloadedConsole(); await testIframeNavigation(); await testBfCacheNavigation(); await testDomCompleteWithWindowStop(); await testCrossOriginNavigation(); }); async function testDocumentEventResources() { info("Test ResourceCommand for DOCUMENT_EVENT"); // Open a test tab const title = "DocumentEventsTitle"; const url = `data:text/html,${title}Document Events`; const tab = await addTab(url); const listener = new ResourceListener(); const { commands } = await initResourceCommand(tab); info( "Check whether the document events are fired correctly even when the document was already loaded" ); const onLoadingAtInit = listener.once("dom-loading"); const onInteractiveAtInit = listener.once("dom-interactive"); const onCompleteAtInit = listener.once("dom-complete"); await commands.resourceCommand.watchResources( [commands.resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: parameters => listener.dispatch(parameters), } ); await assertPromises( commands, // targetBeforeNavigation is only used when there is a will-navigate and a navigate, but there is none here null, // As we started watching on an already loaded document, and no navigation happened since we called watchResources, // we don't have any will-navigate event null, onLoadingAtInit, onInteractiveAtInit, onCompleteAtInit ); ok( true, "Document events are fired even when the document was already loaded" ); let domLoadingResource = await onLoadingAtInit; is( domLoadingResource.url, url, `resource ${domLoadingResource.name} has expected url` ); is( domLoadingResource.title, undefined, `resource ${domLoadingResource.name} does not have a title property` ); let domInteractiveResource = await onInteractiveAtInit; is( domInteractiveResource.url, url, `resource ${domInteractiveResource.name} has expected url` ); is( domInteractiveResource.title, title, `resource ${domInteractiveResource.name} has expected title` ); let domCompleteResource = await onCompleteAtInit; is( domCompleteResource.url, undefined, `resource ${domCompleteResource.name} does not have a url property` ); is( domCompleteResource.title, undefined, `resource ${domCompleteResource.name} does not have a title property` ); info("Check whether the document events are fired correctly when reloading"); const onWillNavigate = listener.once("will-navigate"); const onLoadingAtReloaded = listener.once("dom-loading"); const onInteractiveAtReloaded = listener.once("dom-interactive"); const onCompleteAtReloaded = listener.once("dom-complete"); const targetBeforeNavigation = commands.targetCommand.targetFront; gBrowser.reloadTab(tab); await assertPromises( commands, targetBeforeNavigation, onWillNavigate, onLoadingAtReloaded, onInteractiveAtReloaded, onCompleteAtReloaded ); ok(true, "Document events are fired after reloading"); domLoadingResource = await onLoadingAtReloaded; is( domLoadingResource.url, url, `resource ${domLoadingResource.name} has expected url after reloading` ); is( domLoadingResource.title, undefined, `resource ${domLoadingResource.name} does not have a title property after reloading` ); domInteractiveResource = await onInteractiveAtInit; is( domInteractiveResource.url, url, `resource ${domInteractiveResource.name} has url property after reloading` ); is( domInteractiveResource.title, title, `resource ${domInteractiveResource.name} has expected title after reloading` ); domCompleteResource = await onCompleteAtInit; is( domCompleteResource.url, undefined, `resource ${domCompleteResource.name} does not have a url property after reloading` ); is( domCompleteResource.title, undefined, `resource ${domCompleteResource.name} does not have a title property after reloading` ); await commands.destroy(); } async function testDocumentEventResourcesWithIgnoreExistingResources() { info("Test ignoreExistingResources option for DOCUMENT_EVENT"); const tab = await addTab("data:text/html,Document Events"); const { commands } = await initResourceCommand(tab); info("Check whether the existing document events will not be fired"); const documentEvents = []; await commands.resourceCommand.watchResources( [commands.resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: resources => documentEvents.push(...resources), ignoreExistingResources: true, } ); is(documentEvents.length, 0, "Existing document events are not fired"); info("Check whether the future document events are fired"); const targetBeforeNavigation = commands.targetCommand.targetFront; gBrowser.reloadTab(tab); info( "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" ); await waitFor(() => documentEvents.length === 4); assertEvents({ commands, targetBeforeNavigation, documentEvents }); await commands.destroy(); } async function testIframeNavigation() { info("Test iframe navigations for DOCUMENT_EVENT"); const tab = await addTab( 'https://example.com/document-builder.sjs?html=' ); const secondPageUrl = "https://example.org/document-builder.sjs?html=org"; const { commands } = await initResourceCommand(tab); let documentEvents = []; await commands.resourceCommand.watchResources( [commands.resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: resources => documentEvents.push(...resources), } ); let iframeTarget; if (isFissionEnabled() || isEveryFrameTargetEnabled()) { is( documentEvents.length, 6, "With fission/EFT, we get two targets and two sets of events: dom-loading, dom-interactive, dom-complete" ); [, iframeTarget] = await commands.targetCommand.getAllTargets([ commands.targetCommand.TYPES.FRAME, ]); // Filter out each target events as their order to be random between the two targets const topTargetEvents = documentEvents.filter( r => r.targetFront == commands.targetCommand.targetFront ); const iframeTargetEvents = documentEvents.filter( r => r.targetFront != commands.targetCommand.targetFront ); assertEvents({ commands, documentEvents: [null /* no will-navigate */, ...topTargetEvents], }); assertEvents({ commands, documentEvents: [null /* no will-navigate */, ...iframeTargetEvents], expectedTargetFront: iframeTarget, }); } else { assertEvents({ commands, documentEvents: [null /* no will-navigate */, ...documentEvents], }); } info("Navigate the iframe to another process (if fission is enabled)"); documentEvents = []; await SpecialPowers.spawn( gBrowser.selectedBrowser, [secondPageUrl], function (url) { const iframe = content.document.querySelector("iframe"); iframe.src = url; } ); // We are switching to a new target only when fission is enabled... if (isFissionEnabled() || isEveryFrameTargetEnabled()) { await waitFor(() => documentEvents.length >= 3); is( documentEvents.length, 3, "With fission/EFT, 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)" ); const [, newIframeTarget] = await commands.targetCommand.getAllTargets([ commands.targetCommand.TYPES.FRAME, ]); assertEvents({ commands, targetBeforeNavigation: iframeTarget, documentEvents: [null /* no will-navigate */, ...documentEvents], expectedTargetFront: newIframeTarget, expectedNewURI: secondPageUrl, }); } else { // Wait for some time in order to let a chance to receive some unexpected events await wait(250); is( documentEvents.length, 0, "If fission is disabled, we navigate within the same process, we get no new target and no new resource" ); } await commands.destroy(); } function isBfCacheInParentEnabled() { return ( Services.appinfo.sessionHistoryInParent && Services.prefs.getBoolPref("fission.bfcacheInParent", false) ); } async function testBfCacheNavigation() { info("Test bfcache navigations for DOCUMENT_EVENT"); info("Open a first document and navigate to a second one"); const firstLocation = "data:text/html,firstfirst page"; const secondLocation = "data:text/html,secondsecond page"; const tab = await addTab(firstLocation); const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, secondLocation); await onLoaded; const { commands } = await initResourceCommand(tab); const documentEvents = []; await commands.resourceCommand.watchResources( [commands.resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: resources => { documentEvents.push(...resources); }, ignoreExistingResources: true, } ); // Wait for some time for extra safety await wait(250); is(documentEvents.length, 0, "Existing document events are not fired"); info("Navigate back to the first page"); const onSwitched = commands.targetCommand.once("switched-target"); const targetBeforeNavigation = commands.targetCommand.targetFront; gBrowser.goBack(); // We are switching to a new target only when fission/EFT is enabled... if ( (isFissionEnabled() || isEveryFrameTargetEnabled()) && isBfCacheInParentEnabled() ) { await onSwitched; } info( "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" ); await waitFor(() => documentEvents.length >= 4); /* Ignore will-navigate timestamp as all other DOCUMENT_EVENTS will be set at the original load date, which is when we loaded from the network, and not when we loaded from bfcache */ assertEvents({ commands, targetBeforeNavigation, documentEvents, ignoreWillNavigateTimestamp: true, }); // Wait for some time in order to let a chance to have duplicated dom-loading events await wait(250); is( documentEvents.length, 4, "There is no duplicated event and only the 4 expected DOCUMENT_EVENT states" ); const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] = documentEvents; is( willNavigateEvent.name, "will-navigate", "The first DOCUMENT_EVENT is will-navigate" ); is( loadingEvent.name, "dom-loading", "The second DOCUMENT_EVENT is dom-loading" ); is( interactiveEvent.name, "dom-interactive", "The third DOCUMENT_EVENT is dom-interactive" ); is( completeEvent.name, "dom-complete", "The fourth DOCUMENT_EVENT is dom-complete" ); is( loadingEvent.url, firstLocation, `resource ${loadingEvent.name} has expected url after navigation back` ); is( loadingEvent.title, undefined, `resource ${loadingEvent.name} does not have a title property after navigating back` ); is( interactiveEvent.url, firstLocation, `resource ${interactiveEvent.name} has expected url property after navigating back` ); is( interactiveEvent.title, "first", `resource ${interactiveEvent.name} has expected title after navigating back` ); is( completeEvent.url, undefined, `resource ${completeEvent.name} does not have a url property after navigating back` ); is( completeEvent.title, undefined, `resource ${completeEvent.name} does not have a title property after navigating back` ); await commands.destroy(); } async function testCrossOriginNavigation() { info("Test cross origin navigations for DOCUMENT_EVENT"); const tab = await addTab("https://example.com/document-builder.sjs?html=com"); const { commands } = await initResourceCommand(tab); const documentEvents = []; await commands.resourceCommand.watchResources( [commands.resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: resources => documentEvents.push(...resources), ignoreExistingResources: true, } ); // Wait for some time for extra safety await wait(250); is(documentEvents.length, 0, "Existing document events are not fired"); info("Navigate to another process"); const onSwitched = commands.targetCommand.once("switched-target"); const netUrl = "https://example.net/document-builder.sjs?html=titleNetnet"; const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); const targetBeforeNavigation = commands.targetCommand.targetFront; BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, netUrl); await onLoaded; // We are switching to a new target only when fission is enabled... if (isFissionEnabled() || isEveryFrameTargetEnabled()) { await onSwitched; } info( "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" ); await waitFor(() => documentEvents.length >= 4); assertEvents({ commands, targetBeforeNavigation, documentEvents }); // Wait for some time in order to let a chance to have duplicated dom-loading events await wait(250); is( documentEvents.length, 4, "There is no duplicated event and only the 4 expected DOCUMENT_EVENT states" ); const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] = documentEvents; is( willNavigateEvent.name, "will-navigate", "The first DOCUMENT_EVENT is will-navigate" ); is( loadingEvent.name, "dom-loading", "The second DOCUMENT_EVENT is dom-loading" ); is( interactiveEvent.name, "dom-interactive", "The third DOCUMENT_EVENT is dom-interactive" ); is( completeEvent.name, "dom-complete", "The fourth DOCUMENT_EVENT is dom-complete" ); is( loadingEvent.url, encodeURI(netUrl), `resource ${loadingEvent.name} has expected url after reloading` ); is( loadingEvent.title, undefined, `resource ${loadingEvent.name} does not have a title property after reloading` ); is( interactiveEvent.url, encodeURI(netUrl), `resource ${interactiveEvent.name} has expected url property after reloading` ); is( interactiveEvent.title, "titleNet", `resource ${interactiveEvent.name} has expected title after reloading` ); is( completeEvent.url, undefined, `resource ${completeEvent.name} does not have a url property after reloading` ); is( completeEvent.title, undefined, `resource ${completeEvent.name} does not have a title property after reloading` ); await commands.destroy(); } async function testDomCompleteWithOverloadedConsole() { info("Test dom-complete with an overloaded console object"); const tab = await addTab( "data:text/html," ); const { client, resourceCommand, targetCommand } = await initResourceCommand( tab ); info("Check that all DOCUMENT_EVENTS are fired for the already loaded page"); const documentEvents = []; await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: resources => documentEvents.push(...resources), }); is(documentEvents.length, 3, "Existing document events are fired"); const domComplete = documentEvents[2]; is(domComplete.name, "dom-complete", "the last resource is the dom-complete"); is( domComplete.hasNativeConsoleAPI, false, "the console object is reported to be overloaded" ); targetCommand.destroy(); await client.close(); } async function testDomCompleteWithWindowStop() { info("Test dom-complete with a page calling window.stop()"); const tab = await addTab("data:text/html,foo"); const { commands, client, resourceCommand, targetCommand } = await initResourceCommand(tab); info("Check that all DOCUMENT_EVENTS are fired for the already loaded page"); let documentEvents = []; await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { onAvailable: resources => documentEvents.push(...resources), }); is(documentEvents.length, 3, "Existing document events are fired"); documentEvents = []; const html = ` stopped page Page content that shouldn't be displayed `; const secondLocation = "data:text/html," + encodeURIComponent(html); const targetBeforeNavigation = commands.targetCommand.targetFront; BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, secondLocation); info( "Wait for will-navigate, dom-loading, dom-interactive and dom-complete events" ); await waitFor(() => documentEvents.length === 4); assertEvents({ commands, targetBeforeNavigation, documentEvents }); targetCommand.destroy(); await client.close(); } async function assertPromises( commands, targetBeforeNavigation, onWillNavigate, onLoading, onInteractive, onComplete ) { const willNavigateEvent = await onWillNavigate; const loadingEvent = await onLoading; const interactiveEvent = await onInteractive; const completeEvent = await onComplete; assertEvents({ commands, targetBeforeNavigation, documentEvents: [ willNavigateEvent, loadingEvent, interactiveEvent, completeEvent, ], }); } function assertEvents({ commands, targetBeforeNavigation, documentEvents, expectedTargetFront = commands.targetCommand.targetFront, expectedNewURI = gBrowser.selectedBrowser.currentURI.spec, ignoreWillNavigateTimestamp = false, }) { const [willNavigateEvent, loadingEvent, interactiveEvent, completeEvent] = documentEvents; if (willNavigateEvent) { is(willNavigateEvent.name, "will-navigate", "Received the will-navigate"); is( willNavigateEvent.newURI, expectedNewURI, "will-navigate newURI is set to the current tab new location" ); } is( loadingEvent.name, "dom-loading", "loading received in the exepected order" ); is( interactiveEvent.name, "dom-interactive", "interactive received in the expected order" ); is(completeEvent.name, "dom-complete", "complete received last"); if (willNavigateEvent) { is( typeof willNavigateEvent.time, "number", `Type of time attribute for will-navigate event is correct (${willNavigateEvent.time})` ); } is( typeof loadingEvent.time, "number", `Type of time attribute for loading event is correct (${loadingEvent.time})` ); is( typeof interactiveEvent.time, "number", `Type of time attribute for interactive event is correct (${interactiveEvent.time})` ); is( typeof completeEvent.time, "number", `Type of time attribute for complete event is correct (${completeEvent.time})` ); if (willNavigateEvent && !ignoreWillNavigateTimestamp) { ok( willNavigateEvent.time <= loadingEvent.time, `Timestamp for dom-loading event is greater than will-navigate event (${willNavigateEvent.time} <= ${loadingEvent.time})` ); } ok( loadingEvent.time <= interactiveEvent.time, `Timestamp for interactive event is greater than loading event (${loadingEvent.time} <= ${interactiveEvent.time})` ); ok( interactiveEvent.time <= completeEvent.time, `Timestamp for complete event is greater than interactive event (${interactiveEvent.time} <= ${completeEvent.time}).` ); if (willNavigateEvent) { // If we switched to a new target, this target will be different from currentTargetFront. // This only happen if we navigate to another process or if server target switching is enabled. is( willNavigateEvent.targetFront, targetBeforeNavigation, "will-navigate target was the one before the navigation" ); } is( loadingEvent.targetFront, expectedTargetFront, "loading target is the expected one" ); is( interactiveEvent.targetFront, expectedTargetFront, "interactive target is the expected one" ); is( completeEvent.targetFront, expectedTargetFront, "complete target is the expected one" ); is( completeEvent.hasNativeConsoleAPI, true, "None of the tests (except the dedicated one) overload the console object" ); } class ResourceListener { _listeners = new Map(); dispatch(resources) { for (const resource of resources) { const resolve = this._listeners.get(resource.name); if (resolve) { resolve(resource); this._listeners.delete(resource.name); } } } once(resourceName) { return new Promise(r => this._listeners.set(resourceName, r)); } }