diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/shared/commands/resource/tests/browser_resources_document_events.js | 711 |
1 files changed, 711 insertions, 0 deletions
diff --git a/devtools/shared/commands/resource/tests/browser_resources_document_events.js b/devtools/shared/commands/resource/tests/browser_resources_document_events.js new file mode 100644 index 0000000000..2bd70b9272 --- /dev/null +++ b/devtools/shared/commands/resource/tests/browser_resources_document_events.js @@ -0,0 +1,711 @@ +/* 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>${title}</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=<iframe src="https://example.net/document-builder.sjs?html=net"></iframe>' + ); + 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,<title>first</title>first page"; + const secondLocation = "data:text/html,<title>second</title>second 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=<head><title>titleNet</title></head>net"; + 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,<script>window.console = {};</script>" + ); + + 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 = `<!DOCTYPE html><html> + <head> + <title>stopped page</title> + <script>window.stop();</script> + </head> + <body>Page content that shouldn't be displayed</body> +</html>`; + 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)); + } +} |