Bug 1257550 - Awesomebar result providers should be able to add document fragments or iframes. r?mak draft
authorDrew Willcoxon <adw@mozilla.com>
Wed, 21 Dec 2016 19:14:30 -0800
changeset 452732 1a5ae13a3be287ff9e94c637204ffb89badb908a
parent 447633 ad993783599ab2ede0cf931fdec02f4df40a7a6d
child 540280 64224b59629765819a9180caa995405de16cf244
push id39463
push userdwillcoxon@mozilla.com
push dateThu, 22 Dec 2016 03:14:49 +0000
reviewersmak
bugs1257550
milestone53.0a1
Bug 1257550 - Awesomebar result providers should be able to add document fragments or iframes. r?mak MozReview-Commit-ID: J4aJtETNLoO
browser/base/content/test/urlbar/Panel.jsm
browser/base/content/test/urlbar/browser.ini
browser/base/content/test/urlbar/browser_urlbarAddonIframe.js
browser/base/content/test/urlbar/urlbarAddonIframe.html
browser/base/content/test/urlbar/urlbarAddonIframe.js
browser/base/content/test/urlbar/urlbarAddonIframeContentScript.js
browser/base/content/urlbarBindings.xml
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/Panel.jsm
@@ -0,0 +1,252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = [
+  "Panel",
+];
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+this.Panel = function (panelElt, iframeURL) {
+  this.p = panelElt;
+  this.iframeURL = iframeURL;
+  this._initPanel();
+  this.urlbar.addEventListener("keydown", this);
+  this.urlbar.addEventListener("input", this);
+  this._emitQueue = [];
+};
+
+this.Panel.prototype = {
+
+  get document() {
+    return this.p.ownerDocument;
+  },
+
+  get window() {
+    return this.document.defaultView;
+  },
+
+  get urlbar() {
+    return this.window.gURLBar;
+  },
+
+  iframe: null,
+
+  get iframeDocument() {
+    return this.iframe.contentDocument;
+  },
+
+  get iframeWindow() {
+    return this.iframe.contentWindow;
+  },
+
+  destroy() {
+    this.p.destroyAddonIframe(this);
+    this.urlbar.removeEventListener("keydown", this);
+    this.urlbar.removeEventListener("input", this);
+  },
+
+  _initPanel() {
+    this.iframe = this.p.initAddonIframe(this, {
+      _invalidate: this._invalidate.bind(this),
+    });
+    if (!this.iframe) {
+      // This will be the case when somebody else already owns the iframe.
+      // First consumer wins right now.
+      return;
+    }
+    let onLoad = event => {
+      this.iframe.removeEventListener("load", onLoad, true);
+      this._initIframeContent(event.target.defaultView);
+    };
+    this.iframe.addEventListener("load", onLoad, true);
+    this.iframe.setAttribute("src", this.iframeURL);
+  },
+
+  _initIframeContent(win) {
+    // Clone the urlbar API functions into the iframe window.
+    win = XPCNativeWrapper.unwrap(win);
+    let apiInstance = Cu.cloneInto(iframeAPIPrototype, win, {
+      cloneFunctions: true,
+    });
+    apiInstance._panel = this;
+    Object.defineProperty(win, "urlbar", {
+      get() {
+        return apiInstance;
+      },
+    });
+  },
+
+  // This is called by the popup directly.  It overrides the popup's own
+  // _invalidate method.
+  _invalidate() {
+    this._emit("reset");
+    this._currentIndex = 0;
+    if (this._appendResultTimeout) {
+      this.window.clearTimeout(this._appendResultTimeout);
+    }
+    this._appendCurrentResult();
+  },
+
+  // This emulates the popup's own _appendCurrentResult method, except instead
+  // of appending results to the popup, it emits "result" events to the iframe.
+  _appendCurrentResult() {
+    let controller = this.p.mInput.controller;
+    for (let i = 0; i < this.p.maxResults; i++) {
+      let idx = this._currentIndex;
+      if (idx >= this.p._matchCount) {
+        break;
+      }
+      let url = controller.getValueAt(idx);
+      let action = this.urlbar._parseActionUrl(url);
+      this._emit("result", {
+        url: url,
+        action: action,
+        image: controller.getImageAt(idx),
+        title: controller.getCommentAt(idx),
+        type: controller.getStyleAt(idx),
+        text: controller.searchString.replace(/^\s+/, "").replace(/\s+$/, ""),
+      });
+      this._currentIndex++;
+    }
+    if (this._currentIndex < this.p.matchCount) {
+      this._appendResultTimeout = this.window.setTimeout(() => {
+        this._appendCurrentResult();
+      });
+    }
+  },
+
+  get height() {
+    return this.iframe.getBoundingClientRect().height;
+  },
+
+  set height(val) {
+    this.p.removeAttribute("height");
+    this.iframe.style.height = val + "px";
+  },
+
+  handleEvent(event) {
+    let methName = "_on" + event.type[0].toUpperCase() + event.type.substr(1);
+    this[methName](event);
+  },
+
+  _onKeydown(event) {
+    let emittedEvent = this._emitUrlbarEvent(event);
+    if (emittedEvent && emittedEvent.defaultPrevented) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+  },
+
+  _onInput(event) {
+    this._emitUrlbarEvent(event);
+  },
+
+  _emitUrlbarEvent(event) {
+    let properties = [
+      "altKey",
+      "code",
+      "ctrlKey",
+      "key",
+      "metaKey",
+      "shiftKey",
+    ];
+    let detail = properties.reduce((memo, prop) => {
+      memo[prop] = event[prop];
+      return memo;
+    }, {});
+    return this._emit(event.type, detail);
+  },
+
+  _emit(eventName, detailObj=null) {
+    this._emitQueue.push({
+      name: eventName,
+      detail: detailObj,
+    });
+    return this._processEmitQueue();
+  },
+
+  _processEmitQueue() {
+    if (!this._emitQueue.length) {
+      return null;
+    }
+
+    // iframe.contentWindow can be undefined right after the iframe is created,
+    // even after a number of seconds have elapsed.  Don't know why.  But that's
+    // entirely the reason for having a queue instead of simply dispatching
+    // events as they're created, unfortunately.
+    if (!this.iframeWindow) {
+      if (!this._processEmitQueueTimer) {
+        this._processEmitQueueTimer = setInterval(() => {
+          this._processEmitQueue();
+        }, 100);
+      }
+      return null;
+    }
+
+    if (this._processEmitQueueTimer) {
+      clearInterval(this._processEmitQueueTimer);
+      delete this._processEmitQueueTimer;
+    }
+
+    let { name, detail } = this._emitQueue.shift();
+    let win = XPCNativeWrapper.unwrap(this.iframeWindow);
+    let event = new this.iframeWindow.CustomEvent(name, {
+      detail: Cu.cloneInto(detail, win),
+      cancelable: true,
+    });
+    this.iframeWindow.dispatchEvent(event);
+
+    // More events may be queued up, so recurse.  Do it after a turn of the
+    // event loop to avoid growing the stack as big as the queue, and to let the
+    // caller handle the returned event first.
+    let recurseTimer = setTimeout(() => {
+      this._processEmitQueue();
+    }, 100);
+
+    return event;
+  },
+};
+
+
+// This is the consumer API that's cloned into the iframe window.  Be careful of
+// defining static values on this, or even getters and setters (that aren't real
+// functions).  The cloning process means that such values are copied by value,
+// at the time of cloning, which is probably not what you want.  That's why some
+// of these are functions even though it'd be nicer if they were getters and
+// setters.
+let iframeAPIPrototype = {
+
+  getPanelHeight() {
+    return this._panel.height;
+  },
+
+  setPanelHeight(val) {
+    this._panel.height = val;
+  },
+
+  getValue() {
+    return this._panel.urlbar.value;
+  },
+
+  setValue(val) {
+    this._panel.urlbar.value = val;
+  },
+
+  getMaxResults() {
+    return this._panel.p.maxResults;
+  },
+
+  setMaxResults(val) {
+    this._panel.p.maxResults = val;
+  },
+
+  enter() {
+    this._panel.urlbar.handleCommand();
+  },
+};
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -43,16 +43,22 @@ subsuite = clipboard
 [browser_removeUnsafeProtocolsFromURLBarPaste.js]
 subsuite = clipboard
 [browser_search_favicon.js]
 [browser_tabMatchesInAwesomebar.js]
 support-files =
   moz.png
 [browser_tabMatchesInAwesomebar_perwindowpb.js]
 skip-if = os == 'linux' # Bug 1104755
+[browser_urlbarAddonIframe.js]
+support-files =
+  Panel.jsm
+  urlbarAddonIframe.html
+  urlbarAddonIframe.js
+  urlbarAddonIframeContentScript.js
 [browser_urlbarAboutHomeLoading.js]
 [browser_urlbarAutoFillTrimURLs.js]
 [browser_urlbarCopying.js]
 subsuite = clipboard
 support-files =
   authenticate.sjs
 [browser_urlbarDecode.js]
 [browser_urlbarDelete.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarAddonIframe.js
@@ -0,0 +1,220 @@
+"use strict";
+
+// The purpose of this test is to test the urlbar popup's add-on iframe.  It has
+// a few parts:
+//
+// (1) This file, a normal browser mochitest.
+// (2) html/js files that are loaded in the urlbar popup's add-on iframe:
+//     urlbarAddonIframe.{html,js}
+// (3) A content script that mediates between the first two parts:
+//     urlbarAddonIframeContentScript.js
+//
+// The main test file (this file) sends messages to the content script, which
+// forwards them as events to the iframe.  These messages tell the iframe js to
+// do various things like call functions on the urlbar API and expect events.
+// In response, the iframe js dispatches ack events to the content script, which
+// forwards them as messages to the main test file.
+//
+// The content script may not be necessary right now since the iframe is not
+// remote.  But this structure ensures that if the iframe is made remote in the
+// future, then the test won't have to change very much, and ideally not at all.
+//
+// Actually there's one other part:
+//
+// (4) The Panel.jsm that's bundled with add-ons that use the iframe.
+//
+// Panel.jsm defines the API that's made available to add-on scripts running in
+// the iframe.  This API is orthogonal to the add-on iframe itself.  You could
+// load any html/js in the iframe, technically.  But the purpose of the iframe
+// is to support this Panel.jsm API, so that's what this test tests.
+
+const PANEL_JSM_BASENAME = "Panel.jsm";
+const IFRAME_BASENAME = "urlbarAddonIframe.html";
+const CONTENT_SCRIPT_BASENAME = "urlbarAddonIframeContentScript.js";
+
+// The iframe's message manager.
+let gMsgMan;
+
+add_task(function* () {
+  let rootDirURL = getRootDirectory(gTestPath);
+  let jsmURL = rootDirURL + PANEL_JSM_BASENAME;
+  let iframeURL = rootDirURL + IFRAME_BASENAME;
+  let contentScriptURL = rootDirURL + CONTENT_SCRIPT_BASENAME;
+
+  let { Panel } = Cu.import(jsmURL, {});
+  let panel = new Panel(gURLBar.popup, iframeURL);
+  registerCleanupFunction(() => {
+    panel.destroy();
+    Assert.ok(gURLBar.popup._addonIframe === null, "iframe should be gone");
+  });
+
+  let iframe = gURLBar.popup._addonIframe;
+  Assert.ok(!!iframe, "iframe should not be null");
+
+  gMsgMan =
+    iframe.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
+  gMsgMan.loadFrameScript(contentScriptURL, false);
+
+  yield promiseIframeLoad();
+
+  // urlbar.getValue
+  let value = "this value set by the test";
+  gURLBar.value = value;
+  let readValue = yield promiseUrlbarFunctionCall("getValue");
+  Assert.equal(readValue, value, "value");
+
+  // urlbar.setValue
+  value = "this value set by the iframe";
+  yield promiseUrlbarFunctionCall("setValue", value);
+  Assert.equal(gURLBar.value, value, "setValue");
+
+  // urlbar.getMaxResults
+  let maxResults = gURLBar.popup.maxResults;
+  Assert.equal(typeof(maxResults), "number", "Sanity check");
+  let readMaxResults = yield promiseUrlbarFunctionCall("getMaxResults");
+  Assert.equal(readMaxResults, maxResults, "getMaxResults");
+
+  // urlbar.setMaxResults
+  let newMaxResults = maxResults + 10;
+  yield promiseUrlbarFunctionCall("setMaxResults", newMaxResults);
+  Assert.equal(gURLBar.popup.maxResults, newMaxResults, "setMaxResults");
+  gURLBar.popup.maxResults = maxResults;
+
+  // urlbar.enter
+  value = "http://mochi.test:8888/";
+  yield promiseUrlbarFunctionCall("setValue", value);
+  Assert.equal(gURLBar.value, value, "setValue");
+  yield promiseUrlbarFunctionCall("enter");
+  let browser = gBrowser.selectedBrowser;
+  yield BrowserTestUtils.browserLoaded(browser);
+  Assert.equal(browser.currentURI.spec, value,
+               "enter should have loaded the URL");
+
+  // input, reset, and result events.  There should always be at least one
+  // result, the heuristic result.
+  value = "test";
+  let promiseValues = yield Promise.all([
+    promiseEvent("input")[1],
+    promiseEvent("reset")[1],
+    promiseEvent("result")[1],
+    promiseAutocompleteResultPopup(value, window, true),
+  ]);
+
+  // Check the heuristic result.
+  let result = promiseValues[2];
+  let engineName = Services.search.currentEngine.name;
+  Assert.equal(result.url,
+               `moz-action:searchengine,{"engineName":"${engineName}","input":"test","searchQuery":"test"}`,
+               "result.url");
+  Assert.ok("action" in result, "result.action");
+  Assert.equal(result.action.type, "searchengine", "result.action.type");
+  Assert.ok("params" in result.action, "result.action.params");
+  Assert.equal(result.action.params.engineName, engineName,
+               "result.action.params.engineName");
+  Assert.equal(typeof(result.image), "string", "result.image");
+  Assert.equal(result.title, engineName, "result.title");
+  Assert.equal(result.type, "action searchengine heuristic", "result.type");
+  Assert.equal(result.text, value, "result.text");
+
+  // keydown event.  promiseEvent sends an async message to the iframe, but
+  // synthesizeKey is sync, so we need to wait until the content JS receives
+  // the message and adds its event listener before synthesizing the key.
+  let keydownPromises = promiseEvent("keydown");
+  yield keydownPromises[0];
+  EventUtils.synthesizeKey("KEY_ArrowDown", {
+    type: "keydown",
+    code: "ArrowDown",
+  });
+  yield keydownPromises[1];
+
+  // urlbar.getPanelHeight
+  let height = iframe.getBoundingClientRect().height;
+  let readHeight = yield promiseUrlbarFunctionCall("getPanelHeight");
+  Assert.equal(readHeight, height, "getPanelHeight");
+
+  // urlbar.setPanelHeight
+  let newHeight = height + 100;
+  yield promiseUrlbarFunctionCall("setPanelHeight", newHeight);
+  yield new Promise(resolve => {
+    // The height change is animated, so give it time to complete.  Again, wait
+    // a sec to be safe.
+    setTimeout(resolve, 1000);
+  });
+  Assert.equal(iframe.getBoundingClientRect().height, newHeight,
+               "setPanelHeight");
+});
+
+function promiseIframeLoad() {
+  let msgName = "TestIframeLoadAck";
+  return new Promise(resolve => {
+    info("Waiting for iframe load ack");
+    gMsgMan.addMessageListener(msgName, function onMsg(msg) {
+      info("Received iframe load ack");
+      gMsgMan.removeMessageListener(msgName, onMsg);
+      resolve();
+    });
+  });
+}
+
+/**
+ * Returns a single promise that's resolved when the content JS has called the
+ * function.
+ */
+function promiseUrlbarFunctionCall(...args) {
+  return promiseMessage("function", args)[0];
+}
+
+/**
+ * Returns two promises in an array.  The first is resolved when the content JS
+ * has added its event listener.  The second is resolved when the content JS
+ * has received the event.
+ */
+function promiseEvent(type) {
+  return promiseMessage("event", type, 2);
+}
+
+let gNextMessageID = 1;
+
+/**
+ * Returns an array of promises, one per ack.  Each is resolved when the content
+ * JS acks the message.  numExpectedAcks is the number of acks you expect.
+ */
+function promiseMessage(type, data, numExpectedAcks=1) {
+  let testMsgName = "TestMessage";
+  let ackMsgName = "TestMessageAck";
+  let msgID = gNextMessageID++;
+  gMsgMan.sendAsyncMessage(testMsgName, {
+    type: type,
+    messageID: msgID,
+    data: data,
+  });
+  let ackPromises = [];
+  for (let i = 0; i < numExpectedAcks; i++) {
+    let ackIndex = i;
+    ackPromises.push(new Promise(resolve => {
+      info("Waiting for message ack: " + JSON.stringify({
+        type: type,
+        msgID: msgID,
+        ackIndex: ackIndex,
+      }));
+      gMsgMan.addMessageListener(ackMsgName, function onMsg(msg) {
+        // Messages have IDs so that an ack can be correctly paired with the
+        // initial message it's replying to.  It's not an error if the ack's ID
+        // isn't equal to msgID here.  That will happen when multiple messages
+        // have been sent in a single turn of the event loop so that they're all
+        // waiting on acks.  Same goes for ackIndex.
+        if (msg.data.messageID != msgID || msg.data.ackIndex != ackIndex) {
+          return;
+        }
+        info("Received message ack: " + JSON.stringify({
+          type: type,
+          msgID: msg.data.messageID,
+          ackIndex: ackIndex,
+        }));
+        gMsgMan.removeMessageListener(ackMsgName, onMsg);
+        resolve(msg.data.data);
+      });
+    }));
+  }
+  return ackPromises;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/urlbarAddonIframe.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <script src="http://mochi.test:8888/browser/browser/base/content/test/urlbar/urlbarAddonIframe.js"></script>
+  </head>
+  <body>
+    Hello
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/urlbarAddonIframe.js
@@ -0,0 +1,52 @@
+// Listen for messages from the test.
+addEventListener("TestEvent", event => {
+  let type = event.detail.type;
+  dump("urlbarAddonIframe.js got TestEvent, type=" + type +
+       " messageID=" + event.detail.messageID + "\n");
+  switch (type) {
+  case "function":
+    callUrlbarFunction(event.detail);
+    break;
+  case "event":
+    expectEvent(event.detail);
+    break;
+  }
+});
+
+// Calls a urlbar API function.
+function callUrlbarFunction(detail) {
+  let args = detail.data;
+  let methodName = args.shift();
+  dump("urlbarAddonIframe.js calling urlbar." + methodName + "\n");
+  let rv = urlbar[methodName](...args);
+  ack(detail, rv);
+}
+
+// Waits for an event of a specified type to happen.
+function expectEvent(detail) {
+  let type = detail.data;
+  dump("urlbarAddonIframe.js expecting event of type " + type + "\n");
+  // Ack that the message was received and an event listener was added.
+  ack(detail, null, 0);
+  addEventListener(type, function onEvent(event) {
+    dump("urlbarAddonIframe.js got event of type " + type + "\n");
+    if (event.type != type) {
+      return;
+    }
+    dump("urlbarAddonIframe.js got expected event\n");
+    removeEventListener(type, onEvent);
+    // Ack that the event was received.
+    ack(detail, event.detail, 1);
+  });
+}
+
+// Sends an ack to the test.
+function ack(originalEventDetail, ackData=null, ackIndex=0) {
+  dispatchEvent(new CustomEvent("TestEventAck", {
+    detail: {
+      messageID: originalEventDetail.messageID,
+      ackIndex: ackIndex,
+      data: ackData,
+    },
+  }));
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/urlbarAddonIframeContentScript.js
@@ -0,0 +1,23 @@
+// Forward messages from the test to the iframe as events.
+addMessageListener("TestMessage", msg => {
+  content.dispatchEvent(new content.CustomEvent("TestEvent", {
+    detail: Components.utils.cloneInto(msg.data, content),
+  }));
+});
+
+// Forward events from the iframe to the test as messages.
+addEventListener("TestEventAck", event => {
+  // The waiveXrays call is copied from the contentSearch.js part of
+  // browser_ContentSearch.js test.  Not sure whether it's necessary here.
+  sendAsyncMessage("TestMessageAck", Components.utils.waiveXrays(event.detail));
+}, true, true);
+
+// Send a message to the test when the iframe is loaded.
+if (content.document.readyState == "complete") {
+  sendAsyncMessage("TestIframeLoadAck");
+} else {
+  addEventListener("load", function onLoad(event) {
+    removeEventListener("load", onLoad);
+    sendAsyncMessage("TestIframeLoadAck");
+  }, true, true);
+}
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1451,28 +1451,33 @@ file, You can obtain one at http://mozil
             return this.input.mController.matchCount > 0 &&
                    this.input.mController
                              .getStyleAt(0)
                              .split(/\s+/).indexOf("heuristic") > 0;
           ]]>
         </getter>
       </property>
 
-      <property name="maxResults" readonly="true">
+      <property name="maxResults">
         <getter>
           <![CDATA[
             if (!this._maxResults) {
               var prefService =
                 Components.classes["@mozilla.org/preferences-service;1"]
                           .getService(Components.interfaces.nsIPrefBranch);
               this._maxResults = prefService.getIntPref("browser.urlbar.maxRichResults");
             }
             return this._maxResults;
           ]]>
         </getter>
+        <setter>
+          <![CDATA[
+            return this._maxResults = parseInt(val);
+          ]]>
+        </setter>
       </property>
 
       <method name="openAutocompletePopup">
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body>
           <![CDATA[
           // initially the panel is hidden
@@ -1809,16 +1814,109 @@ file, You can obtain one at http://mozil
           //    disabled), this won't be called, and the selectedIndex will be
           //    the default -1 value. Then handleEnter will know it should not
           //    delay the action, cause a result wont't ever arrive.
           this.input.controller.setInitiallySelectedIndex(0);
           this.overrideValue = null;
         ]]></body>
       </method>
 
+      <field name="_addonIframe">null</field>
+      <field name="_addonIframeOwner">null</field>
+      <field name="_addonIframeOverriddenFunctionsByName">{}</field>
+
+      <!-- These methods must be overridden and properly handled by the API
+           runtime so that it doesn't break the popup.  If any of these methods
+           is not overridden, then initAddonIframe should throw. -->
+      <field name="_addonIframeOverrideFunctionNames">[
+        "_invalidate",
+      ]</field>
+
+      <field name="_addonIframeHiddenAnonids">[
+        "search-suggestions-notification",
+        "richlistbox",
+        "one-off-search-buttons",
+      ]</field>
+      <field name="_addonIframeHiddenDisplaysByAnonid">{}</field>
+
+      <method name="initAddonIframe">
+        <parameter name="owner"/>
+        <parameter name="overrides"/>
+        <body><![CDATA[
+          if (this._addonIframeOwner) {
+            // Another add-on has already requested the iframe.  Return null to
+            // signal to the calling add-on that it should not take over the
+            // popup.  First add-on wins for now.
+            return null;
+          }
+          // Make sure all overrides are provided before doing anything.
+          for (let name of this._addonIframeOverrideFunctionNames) {
+            if (typeof(overrides[name]) != "function") {
+              throw new Error(
+                "Override for method '" + name + "' must be given"
+              );
+            }
+          }
+          // OK, insert the iframe.
+          this._addonIframeOwner = owner;
+          this._addonIframe = this._makeAddonIframe();
+          this._addonIframeOverriddenFunctionsByName = {};
+          for (let name of this._addonIframeOverrideFunctionNames) {
+            this._addonIframeOverriddenFunctionsByName[name] = this[name];
+            this[name] = overrides[name];
+          }
+          return this._addonIframe;
+        ]]></body>
+      </method>
+
+      <method name="destroyAddonIframe">
+        <parameter name="owner"/>
+        <body><![CDATA[
+          if (this._addonIframeOwner != owner) {
+            throw new Error("You're not the iframe owner");
+          }
+          this._addonIframeOwner = null;
+          this._addonIframe.remove();
+          this._addonIframe = null;
+          for (let anonid of this._addonIframeHiddenAnonids) {
+            let child = document.getAnonymousElementByAttribute(
+              this, "anonid", anonid
+            );
+            child.style.display =
+              this._addonIframeHiddenDisplaysByAnonid[anonid];
+          }
+          for (let name in this._addonIframeOverriddenFunctionsByName) {
+            this[name] = this._addonIframeOverriddenFunctionsByName[name];
+          }
+          this._addonIframeOverriddenFunctionsByName = {};
+        ]]></body>
+      </method>
+
+      <method name="_makeAddonIframe">
+        <body><![CDATA[
+          this._addonIframeHiddenDisplaysByAnonid = {};
+          for (let anonid of this._addonIframeHiddenAnonids) {
+            let child = document.getAnonymousElementByAttribute(
+              this, "anonid", anonid
+            );
+            this._addonIframeHiddenDisplaysByAnonid[anonid] =
+              child.style.display;
+            child.style.display = "none";
+          }
+          let XUL_NS =
+            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+          let iframe = document.createElementNS(XUL_NS, "iframe");
+          iframe.setAttribute("type", "content");
+          iframe.setAttribute("flex", "1");
+          iframe.style.transition = "height 100ms";
+          this.appendChild(iframe);
+          return iframe;
+        ]]></body>
+      </method>
+
     </implementation>
     <handlers>
 
       <handler event="SelectedOneOffButtonChanged"><![CDATA[
         this._selectedOneOffChanged();
       ]]></handler>
 
       <handler event="mousedown"><![CDATA[