Bug 1599773 - [remote] Implement Page.frameDetached. r=remote-protocol-reviewers,maja_zf
☠☠ backed out by 0f440282ada8 ☠ ☠
authorHenrik Skupin <mail@hskupin.info>
Tue, 19 May 2020 07:34:24 +0000
changeset 530760 779bc06348ad134826ba47896e54abf9363924a8
parent 530759 6f223ae549d830b35b71f50a42450d35550a40c5
child 530761 0794b8100c6c7942cc0419e468c05d1140a58da0
push id116353
push userhskupin@mozilla.com
push dateTue, 19 May 2020 07:35:14 +0000
treeherderautoland@779bc06348ad [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,162 @@
+/* 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 frameDetachedEvents = [];
+  Page.frameDetached(frame => frameDetachedEvents.push(frame));
+
+  const framesBefore = await getFlattendFrameList();
+  await callback();
+  const framesAfter = await getFlattendFrameList();
+
+  // check how many frames were added or removed
+  const count = Math.abs(framesBefore.size - framesAfter.size);
+
+  if (expectedEventCount == 0) {
+    is(frameDetachedEvents.length, 0, "Got no frame detached event");
+    return;
+  }
+
+  is(count, expectedEventCount, "Expected amount of frames removed");
+  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(event => {
+    const expectedFrame = expectedFrames.get(event.frameId);
+
+    is(
+      event.frameId,
+      expectedFrame.id,
+      "Got expected frame id for frameDetached event"
+    );
+  });
+}