diff --git a/remote/domains/content/Page.jsm b/remote/domains/content/Page.jsm index 52488cc9f243..ace0ac67b3b6 100644 --- a/remote/domains/content/Page.jsm +++ b/remote/domains/content/Page.jsm @@ -157,8 +157,16 @@ class Page extends ContentProcessDomain { }); } + emitLifecycleEvent(frameId, loaderId, name, timestamp) { + if (this.lifecycleEnabled) { + this.emit("Page.lifecycleEvent", { frameId, loaderId, name, timestamp }); + } + } + handleEvent({ type, target }) { - if (target.defaultView != this.content) { + const isFrame = target.defaultView != this.content; + + if (isFrame) { // Ignore iframes for now return; } @@ -170,18 +178,44 @@ class Page extends ContentProcessDomain { switch (type) { case "DOMContentLoaded": this.emit("Page.domContentEventFired", { timestamp }); + if (!isFrame) { + this.emitLifecycleEvent( + frameId, + /* loaderId */ null, + "DOMContentLoaded", + timestamp + ); + } break; case "pagehide": // Maybe better to bound to "unload" once we can register for this event this.emit("Page.frameStartedLoading", { frameId }); + if (!isFrame) { + this.emitLifecycleEvent( + frameId, + /* loaderId */ null, + "init", + timestamp + ); + } break; case "pageshow": this.emit("Page.loadEventFired", { timestamp }); + if (!isFrame) { + this.emitLifecycleEvent( + frameId, + /* loaderId */ null, + "load", + timestamp + ); + } + // XXX this should most likely be sent differently this.emit("Page.navigatedWithinDocument", { frameId, url }); this.emit("Page.frameStoppedLoading", { frameId }); + break; } } diff --git a/remote/test/browser/head.js b/remote/test/browser/head.js index 7b6e713ac4ba..f1c73ac31039 100644 --- a/remote/test/browser/head.js +++ b/remote/test/browser/head.js @@ -178,3 +178,12 @@ function getContentProperty(prop) { _prop => content[_prop] ); } + +/** + * Return a new promise, which resolves after ms have been elapsed + */ +function timeoutPromise(ms) { + return new Promise(resolve => { + window.setTimeout(resolve, ms); + }); +} diff --git a/remote/test/browser/page/browser.ini b/remote/test/browser/page/browser.ini index bebc40a1913c..7d087617a1b7 100644 --- a/remote/test/browser/page/browser.ini +++ b/remote/test/browser/page/browser.ini @@ -14,5 +14,6 @@ support-files = [browser_javascriptDialog_confirm.js] [browser_javascriptDialog_otherTarget.js] [browser_javascriptDialog_prompt.js] +[browser_lifecycleEvent.js] [browser_reload.js] [browser_runtimeEvents.js] diff --git a/remote/test/browser/page/browser_lifecycleEvent.js b/remote/test/browser/page/browser_lifecycleEvent.js new file mode 100644 index 000000000000..3617ad6f2708 --- /dev/null +++ b/remote/test/browser/page/browser_lifecycleEvent.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Page lifecycle events + +const DOC = toDataURL("default-test-page"); + +add_task(async function noInitialEvents({ Page }) { + await Page.enable(); + info("Page domain has been enabled"); + + const promise = recordPromises(Page, ["init", "DOMContentLoaded", "load"]); + info("Lifecycle events are not enabled"); + + let pageLoaded = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await pageLoaded; + info("A new page has been loaded"); + + await assertNavigationLifecycleEvents({ promise, frameId, timeout: 1000 }); +}); + +add_task(async function noEventsAfterDisable({ Page }) { + await Page.enable(); + info("Page domain has been enabled"); + + await Page.setLifecycleEventsEnabled({ enabled: true }); + await Page.setLifecycleEventsEnabled({ enabled: false }); + const promise = recordPromises(Page, ["init", "DOMContentLoaded", "load"]); + info("Lifecycle events are not enabled"); + + let pageLoaded = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await pageLoaded; + info("A new page has been loaded"); + + await assertNavigationLifecycleEvents({ promise, frameId, timeout: 1000 }); +}); + +add_task(async function navigateEvents({ Page }) { + await Page.enable(); + info("Page domain has been enabled"); + + await Page.setLifecycleEventsEnabled({ enabled: true }); + const promise = recordPromises(Page, ["init", "DOMContentLoaded", "load"]); + info("Lifecycle events have been enabled"); + + let pageLoaded = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await pageLoaded; + info("A new page has been loaded"); + + await assertNavigationLifecycleEvents({ promise, frameId }); +}); + +add_task(async function navigateEventsOnReload({ Page }) { + await Page.enable(); + info("Page domain has been enabled"); + + let pageLoaded = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await pageLoaded; + info("Initial page has been loaded"); + + await Page.setLifecycleEventsEnabled({ enabled: true }); + const promise = recordPromises(Page, ["init", "DOMContentLoaded", "load"]); + info("Lifecycle events have been enabled"); + + pageLoaded = Page.loadEventFired(); + await Page.reload(); + await pageLoaded; + info("The page has been reloaded"); + + await assertNavigationLifecycleEvents({ promise, frameId }); +}); + +add_task(async function navigateEventsOnNavigateToSameURL({ Page }) { + await Page.enable(); + info("Page domain has been enabled"); + + let pageLoaded = Page.loadEventFired(); + const { frameId } = await Page.navigate({ url: DOC }); + await pageLoaded; + info("Initial page has been loaded"); + + await Page.setLifecycleEventsEnabled({ enabled: true }); + const promise = recordPromises(Page, ["init", "DOMContentLoaded", "load"]); + info("Lifecycle events have been enabled"); + + pageLoaded = Page.loadEventFired(); + await Page.navigate({ url: DOC }); + await pageLoaded; + info("The page has been reloaded"); + + await assertNavigationLifecycleEvents({ promise, frameId }); +}); + +function recordPromises(Page, names) { + return new Promise(resolve => { + const resolutions = new Map(); + + const unsubscribe = Page.lifecycleEvent(event => { + info(`Received Page.lifecycleEvent for ${event.name}`); + resolutions.set(event.name, event); + + if (event.name == names[names.length - 1]) { + unsubscribe(); + resolve(resolutions); + } + }); + }); +} + +async function assertNavigationLifecycleEvents({ promise, frameId, timeout }) { + // Wait for all the promises to resolve + const promises = [promise]; + + if (timeout) { + promises.push(timeoutPromise(timeout)); + } + + const resolutions = await Promise.race(promises); + + if (timeout) { + is(resolutions, undefined, "No lifecycle events have been recorded"); + return; + } + + // Assert the order in which they resolved + const expectedResolutions = ["init", "DOMContentLoaded", "load"]; + Assert.deepEqual( + [...resolutions.keys()], + expectedResolutions, + "Received various lifecycle events in the expected order" + ); + + // Now assert the data exposed by each of these events + const frameStartedLoading = resolutions.get("init"); + is(frameStartedLoading.frameId, frameId, "init frameId is the same one"); + + const DOMContentLoaded = resolutions.get("DOMContentLoaded"); + is( + DOMContentLoaded.frameId, + frameId, + "DOMContentLoaded frameId is the same one" + ); + + const load = resolutions.get("load"); + is(load.frameId, frameId, "load frameId is the same one"); +}