diff --git a/remote/domains/content/Page.jsm b/remote/domains/content/Page.jsm index 80057967625a..cffe0867fe3f 100644 --- a/remote/domains/content/Page.jsm +++ b/remote/domains/content/Page.jsm @@ -37,6 +37,7 @@ class Page extends ContentProcessDomain { this.scriptsToEvaluateOnLoad = new Map(); this.worldsToEvaluateOnLoad = new Set(); + this._onFrameAttached = this._onFrameAttached.bind(this); this._onFrameNavigated = this._onFrameNavigated.bind(this); this._onScriptLoaded = this._onScriptLoaded.bind(this); @@ -55,7 +56,7 @@ class Page extends ContentProcessDomain { async enable() { if (!this.enabled) { - this.enabled = true; + this.session.contextObserver.on("frame-attached", this._onFrameAttached); this.session.contextObserver.on( "frame-navigated", this._onFrameNavigated @@ -82,11 +83,14 @@ class Page extends ContentProcessDomain { this.chromeEventHandler.addEventListener("pageshow", this, { mozSystemGroup: true, }); + + this.enabled = true; } } disable() { if (this.enabled) { + this.session.contextObserver.off("frame-attached", this._onFrameAttached); this.session.contextObserver.off( "frame-navigated", this._onFrameNavigated @@ -242,6 +246,14 @@ class Page extends ContentProcessDomain { return this.content.location.href; } + _onFrameAttached(name, { frameId, parentFrameId }) { + this.emit("Page.frameAttached", { + frameId, + parentFrameId, + stack: null, + }); + } + _onFrameNavigated(name, { frameId, window }) { const url = window.location.href; this.emit("Page.frameNavigated", { diff --git a/remote/observers/ContextObserver.jsm b/remote/observers/ContextObserver.jsm index e8665d8155ec..74756b58dfb9 100644 --- a/remote/observers/ContextObserver.jsm +++ b/remote/observers/ContextObserver.jsm @@ -32,6 +32,8 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { executeSoon } = ChromeUtils.import("chrome://remote/content/Sync.jsm"); +// Temporary flag to not emit frame related events until everything +// has been completely implemented, and Puppeteer is no longer busted. const FRAMES_ENABLED = Services.prefs.getBoolPref( "remote.frames.enabled", false @@ -55,6 +57,10 @@ class ContextObserver { }); Services.obs.addObserver(this, "inner-window-destroyed"); + + if (FRAMES_ENABLED) { + Services.obs.addObserver(this, "webnavigation-create"); + } } destructor() { @@ -67,7 +73,12 @@ class ContextObserver { this.chromeEventHandler.removeEventListener("pagehide", this, { mozSystemGroup: true, }); + Services.obs.removeObserver(this, "inner-window-destroyed"); + + if (FRAMES_ENABLED) { + Services.obs.removeObserver(this, "webnavigation-create"); + } } handleEvent({ type, target, persisted }) { @@ -114,9 +125,26 @@ class ContextObserver { } } - // "inner-window-destroyed" observer service listener observe(subject, topic, data) { - const innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; - this.emit("context-destroyed", { windowId: innerWindowID }); + switch (topic) { + case "inner-window-destroyed": + const windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + this.emit("context-destroyed", { windowId }); + break; + case "webnavigation-create": + subject.QueryInterface(Ci.nsIDocShell); + this.onDocShellCreated(subject); + break; + } + } + + onDocShellCreated(docShell) { + const parent = docShell.browsingContext.parent; + + // TODO: Use a unique identifier for frames (bug 1605359) + this.emit("frame-attached", { + frameId: docShell.browsingContext.id.toString(), + parentFrameId: parent ? parent.id.toString() : null, + }); } } diff --git a/remote/test/browser/head.js b/remote/test/browser/head.js index 9dfcdda6cd54..d601294aea20 100644 --- a/remote/test/browser/head.js +++ b/remote/test/browser/head.js @@ -264,6 +264,48 @@ function toDataURL(src, doctype = "html") { return `data:${mime},${encodeURIComponent(doc)}`; } +function convertArgument(arg) { + if (typeof arg === "bigint") { + return { unserializableValue: `${arg.toString()}n` }; + } + if (Object.is(arg, -0)) { + return { unserializableValue: "-0" }; + } + if (Object.is(arg, Infinity)) { + return { unserializableValue: "Infinity" }; + } + if (Object.is(arg, -Infinity)) { + return { unserializableValue: "-Infinity" }; + } + if (Object.is(arg, NaN)) { + return { unserializableValue: "NaN" }; + } + + return { value: arg }; +} + +async function evaluate(client, contextId, pageFunction, ...args) { + const { Runtime } = client; + + if (typeof pageFunction === "string") { + return Runtime.evaluate({ + expression: pageFunction, + contextId, + returnByValue: true, + awaitPromise: true, + }); + } else if (typeof pageFunction === "function") { + return Runtime.callFunctionOn({ + functionDeclaration: pageFunction.toString(), + executionContextId: contextId, + arguments: args.map(convertArgument), + returnByValue: true, + awaitPromise: true, + }); + } + throw new Error("pageFunction: expected 'string' or 'function'"); +} + /** * Load a given URL in the currently selected tab */ @@ -271,7 +313,7 @@ async function loadURL(url, expectedURL = undefined) { expectedURL = expectedURL || url; const browser = gBrowser.selectedTab.linkedBrowser; - const loaded = BrowserTestUtils.browserLoaded(browser, false, expectedURL); + const loaded = BrowserTestUtils.browserLoaded(browser, true, expectedURL); BrowserTestUtils.loadURI(browser, url); await loaded; diff --git a/remote/test/browser/page/browser.ini b/remote/test/browser/page/browser.ini index d33dfedf3a8f..6a29adef3b81 100644 --- a/remote/test/browser/page/browser.ini +++ b/remote/test/browser/page/browser.ini @@ -14,6 +14,7 @@ support-files = [browser_bringToFront.js] [browser_captureScreenshot.js] [browser_createIsolatedWorld.js] +[browser_frameAttached.js] [browser_frameNavigated.js] [browser_getFrameTree.js] [browser_getLayoutMetrics.js] diff --git a/remote/test/browser/page/browser_frameAttached.js b/remote/test/browser/page/browser_frameAttached.js new file mode 100644 index 000000000000..4f63ef9f7fe2 --- /dev/null +++ b/remote/test/browser/page/browser_frameAttached.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("
foo
"); +const DOC_IFRAME_MULTI = toDataURL(` + + +`); +const DOC_IFRAME_NESTED = toDataURL(` + +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with an iframe"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + await Page.enable(); + await Page.disable(); + + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with an iframe"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function noEventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page without an iframe"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 2, async () => { + info("Navigate to a page with an iframe"); + await loadURL(DOC_IFRAME_MULTI); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 2, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventWhenAttachingFrame({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 1, async () => { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const frame = content.document.createElement("iframe"); + frame.src = "data:text/html,frame content"; + const loaded = new Promise(resolve => (frame.onload = resolve)); + content.document.body.appendChild(frame); + await loaded; + }); + }); +}); + +async function runFrameAttachedTest(client, expectedEventCount, callback) { + const { Page } = client; + + const ATTACHED = "Page.frameAttached"; + + const history = new RecordEvents(expectedEventCount); + history.addRecorder({ + event: Page.frameAttached, + eventName: ATTACHED, + messageFn: payload => { + return `Received ${ATTACHED} for frame id ${payload.frameId}`; + }, + }); + + const framesBefore = await getFlattendFrameList(); + await callback(); + const framesAfter = await getFlattendFrameList(); + + const frameAttachedEvents = await history.record(); + + if (expectedEventCount == 0) { + is(frameAttachedEvents.length, 0, "Got no frame attached event"); + return; + } + + // check how many frames were attached or detached + const count = Math.abs(framesBefore.size - framesAfter.size); + + is(count, expectedEventCount, "Expected amount of frames attached"); + is( + frameAttachedEvents.length, + count, + "Received the expected amount of frameAttached events" + ); + + // extract the new or removed frames + const framesAll = new Map([...framesBefore, ...framesAfter]); + const expectedFrames = new Map( + [...framesAll].filter(([key, _value]) => { + return !framesBefore.has(key) && framesAfter.has(key); + }) + ); + + frameAttachedEvents.forEach(({ payload }) => { + const { frameId, parentFrameId } = payload; + + console.log(`Check frame id ${frameId}`); + const expectedFrame = expectedFrames.get(frameId); + + ok(expectedFrame, `Found expected frame with id ${frameId}`); + is( + frameId, + expectedFrame.id, + "Got expected frame id for frameAttached event" + ); + is( + parentFrameId, + expectedFrame.parentId, + "Got expected parent frame id for frameAttached event" + ); + }); +}