Bug 1599773 - [remote] Implement Page.frameDetached. r=remote-protocol-reviewers,maja_zf
authorHenrik Skupin <mail@hskupin.info>
Tue, 19 May 2020 19:45:27 +0000
changeset 530949 8b594c96ab8e8e10e224169cd95ebb57d18ccf0e
parent 530948 62b8adbc0e67be4570ff98f33406729667e71eee
child 530950 5cf6e62ea26befe090114c51306f69a7fbb56a14
push id37434
push userabutkovits@mozilla.com
push dateWed, 20 May 2020 10:05:10 +0000
treeherdermozilla-central@005ef1c25992 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersremote-protocol-reviewers, maja_zf
bugs1599773
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1599773 - [remote] Implement Page.frameDetached. r=remote-protocol-reviewers,maja_zf Differential Revision: https://phabricator.services.mozilla.com/D71293
remote/domains/content/Page.jsm
remote/observers/ContextObserver.jsm
remote/test/browser/page/browser.ini
remote/test/browser/page/browser_frameDetached.js
--- a/remote/domains/content/Page.jsm
+++ b/remote/domains/content/Page.jsm
@@ -33,16 +33,17 @@ class Page extends ContentProcessDomain 
 
     this.enabled = false;
     this.lifecycleEnabled = false;
     // script id => { source, worldName }
     this.scriptsToEvaluateOnLoad = new Map();
     this.worldsToEvaluateOnLoad = new Set();
 
     this._onFrameAttached = this._onFrameAttached.bind(this);
+    this._onFrameDetached = this._onFrameDetached.bind(this);
     this._onFrameNavigated = this._onFrameNavigated.bind(this);
     this._onScriptLoaded = this._onScriptLoaded.bind(this);
 
     this.session.contextObserver.on("script-loaded", this._onScriptLoaded);
   }
 
   destructor() {
     this.setLifecycleEventsEnabled({ enabled: false });
@@ -52,16 +53,17 @@ class Page extends ContentProcessDomain 
     super.destructor();
   }
 
   // commands
 
   async enable() {
     if (!this.enabled) {
       this.session.contextObserver.on("frame-attached", this._onFrameAttached);
+      this.session.contextObserver.on("frame-detached", this._onFrameDetached);
       this.session.contextObserver.on(
         "frame-navigated",
         this._onFrameNavigated
       );
 
       this.chromeEventHandler.addEventListener("readystatechange", this, {
         mozSystemGroup: true,
         capture: true,
@@ -86,16 +88,17 @@ class Page extends ContentProcessDomain 
 
       this.enabled = true;
     }
   }
 
   disable() {
     if (this.enabled) {
       this.session.contextObserver.off("frame-attached", this._onFrameAttached);
+      this.session.contextObserver.off("frame-detached", this._onFrameDetached);
       this.session.contextObserver.off(
         "frame-navigated",
         this._onFrameNavigated
       );
 
       this.chromeEventHandler.removeEventListener("readystatechange", this, {
         mozSystemGroup: true,
         capture: true,
@@ -249,16 +252,20 @@ class Page extends ContentProcessDomain 
   _onFrameAttached(name, { frameId, parentFrameId }) {
     this.emit("Page.frameAttached", {
       frameId,
       parentFrameId,
       stack: null,
     });
   }
 
+  _onFrameDetached(name, { frameId }) {
+    this.emit("Page.frameDetached", { frameId });
+  }
+
   _onFrameNavigated(name, { frameId, window }) {
     const url = window.location.href;
     this.emit("Page.frameNavigated", {
       frame: {
         id: frameId,
         // frameNavigated is only emitted for the top level document
         // so that it never has a parent.
         parentId: null,
--- a/remote/observers/ContextObserver.jsm
+++ b/remote/observers/ContextObserver.jsm
@@ -55,16 +55,17 @@ class ContextObserver {
     this.chromeEventHandler.addEventListener("pagehide", this, {
       mozSystemGroup: true,
     });
 
     Services.obs.addObserver(this, "inner-window-destroyed");
 
     if (FRAMES_ENABLED) {
       Services.obs.addObserver(this, "webnavigation-create");
+      Services.obs.addObserver(this, "webnavigation-destroy");
     }
   }
 
   destructor() {
     this.chromeEventHandler.removeEventListener("DOMWindowCreated", this, {
       mozSystemGroup: true,
     });
     this.chromeEventHandler.removeEventListener("pageshow", this, {
@@ -73,16 +74,17 @@ 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");
+      Services.obs.removeObserver(this, "webnavigation-destroy");
     }
   }
 
   handleEvent({ type, target, persisted }) {
     const window = target.defaultView;
     const frameId = window.docShell.browsingContext.id.toString();
     const id = window.windowUtils.currentInnerWindowID;
 
@@ -130,21 +132,32 @@ class ContextObserver {
       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;
+      case "webnavigation-destroy":
+        subject.QueryInterface(Ci.nsIDocShell);
+        this.onDocShellDestroyed(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,
     });
   }
+
+  onDocShellDestroyed(docShell) {
+    // TODO: Use a unique identifier for frames (bug 1605359)
+    this.emit("frame-detached", {
+      frameId: docShell.browsingContext.id.toString(),
+    });
+  }
 }
--- a/remote/test/browser/page/browser.ini
+++ b/remote/test/browser/page/browser.ini
@@ -10,16 +10,17 @@ support-files =
   head.js
   doc_empty.html
   sjs_redirect.sjs
 
 [browser_bringToFront.js]
 [browser_captureScreenshot.js]
 [browser_createIsolatedWorld.js]
 [browser_frameAttached.js]
+[browser_frameDetached.js]
 [browser_frameNavigated.js]
 [browser_getFrameTree.js]
 [browser_getLayoutMetrics.js]
 [browser_getNavigationHistory.js]
 [browser_javascriptDialog_alert.js]
 [browser_javascriptDialog_beforeunload.js]
 [browser_javascriptDialog_confirm.js]
 [browser_javascriptDialog_otherTarget.js]
new file mode 100644
--- /dev/null
+++ b/remote/test/browser/page/browser_frameDetached.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const DOC = toDataURL("<div>foo</div>");
+const DOC_IFRAME_MULTI = toDataURL(`
+  <iframe src='data:text/html,foo'></iframe>
+  <iframe src='data:text/html,bar'></iframe>
+`);
+const DOC_IFRAME_NESTED = toDataURL(`
+  <iframe src="data:text/html,<iframe src='data:text/html,foo'></iframe>">
+  </iframe>
+`);
+
+// Disable bfcache to force documents to be destroyed on navigation
+Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("browser.sessionhistory.max_total_viewers");
+});
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+  await loadURL(DOC_IFRAME_MULTI);
+
+  await runFrameDetachedTest(client, 0, async () => {
+    info("Navigate away from a page with an iframe");
+    await loadURL(DOC);
+  });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+  const { Page } = client;
+
+  await Page.enable();
+
+  await loadURL(DOC_IFRAME_MULTI);
+
+  await Page.disable();
+
+  await runFrameDetachedTest(client, 0, async () => {
+    info("Navigate away from a page with an iframe");
+    await loadURL(DOC);
+  });
+});
+
+add_task(async function noEventWhenNavigatingWithNoFrames({ client }) {
+  const { Page } = client;
+
+  await Page.enable();
+
+  await loadURL(DOC);
+
+  await runFrameDetachedTest(client, 0, async () => {
+    info("Navigate away from a page with no iframes");
+    await loadURL(DOC);
+  });
+});
+
+add_task(async function eventWhenNavigatingWithFrames({ client }) {
+  const { Page } = client;
+
+  await Page.enable();
+
+  await loadURL(DOC_IFRAME_MULTI);
+
+  await runFrameDetachedTest(client, 2, async () => {
+    info("Navigate away from a page with an iframe");
+    await loadURL(DOC);
+  });
+});
+
+add_task(async function eventWhenNavigatingWithNestedFrames({ client }) {
+  const { Page } = client;
+
+  await Page.enable();
+
+  await loadURL(DOC_IFRAME_NESTED);
+
+  await runFrameDetachedTest(client, 2, async () => {
+    info("Navigate away from a page with nested iframes");
+    await loadURL(DOC);
+  });
+});
+
+add_task(async function eventWhenDetachingFrame({ client }) {
+  const { Page } = client;
+
+  await Page.enable();
+
+  await loadURL(DOC_IFRAME_MULTI);
+
+  await runFrameDetachedTest(client, 1, async () => {
+    // Remove the single frame from the page
+    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+      const frame = content.document.getElementsByTagName("iframe")[0];
+      frame.remove();
+    });
+  });
+});
+
+add_task(async function eventWhenDetachingNestedFrames({ client }) {
+  const { Page, Runtime } = client;
+
+  await Page.enable();
+
+  await loadURL(DOC_IFRAME_NESTED);
+
+  await Runtime.enable();
+  const { context } = await Runtime.executionContextCreated();
+
+  await runFrameDetachedTest(client, 2, async () => {
+    // Remove top-frame, which also removes any nested frames
+    await evaluate(client, context.id, async () => {
+      const frame = document.getElementsByTagName("iframe")[0];
+      frame.remove();
+    });
+  });
+});
+
+async function runFrameDetachedTest(client, expectedEventCount, callback) {
+  const { Page } = client;
+
+  const DETACHED = "Page.frameDetached";
+
+  const history = new RecordEvents(expectedEventCount);
+  history.addRecorder({
+    event: Page.frameDetached,
+    eventName: DETACHED,
+    messageFn: payload => {
+      return `Received ${DETACHED} for frame id ${payload.frameId}`;
+    },
+  });
+
+  const framesBefore = await getFlattendFrameList();
+  await callback();
+  const framesAfter = await getFlattendFrameList();
+
+  const frameDetachedEvents = await history.record();
+
+  if (expectedEventCount == 0) {
+    is(frameDetachedEvents.length, 0, "Got no frame detached 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 detached");
+  is(
+    frameDetachedEvents.length,
+    count,
+    "Received the expected amount of frameDetached 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);
+    })
+  );
+
+  frameDetachedEvents.forEach(({ payload }) => {
+    const { frameId } = payload;
+
+    console.log(`Check frame id ${frameId}`);
+    const expectedFrame = expectedFrames.get(frameId);
+
+    is(
+      frameId,
+      expectedFrame.id,
+      "Got expected frame id for frameDetached event"
+    );
+  });
+}