Bug 1409697 - Fix multiple addon options rendered on addon reload. r=aswan
authorLuca Greco <lgreco@mozilla.com>
Fri, 17 Nov 2017 16:27:06 +0100
changeset 438295 44e3a8933093ac65ffd23cfdd2311725873f3f1b
parent 438294 3ef8715cb8d7488ff301b9e0eb71f6e5da54962e
child 438296 10eb9d8523fa3da33678bc20c9f759d408c8b0d4
push id117
push userfmarier@mozilla.com
push dateTue, 28 Nov 2017 20:17:16 +0000
reviewersaswan
bugs1409697
milestone59.0a1
Bug 1409697 - Fix multiple addon options rendered on addon reload. r=aswan MozReview-Commit-ID: 5IRvDqdW1ZO
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/test/browser/browser-common.ini
toolkit/mozapps/extensions/test/browser/browser_webext_options_addon_reload.js
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -3602,21 +3602,23 @@ var gDetailView = {
     var rows = document.getElementById("detail-downloads").parentNode;
 
     if (this._addon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER) {
       whenViewLoaded(async () => {
         await this._addon.startupPromise;
 
         const browserContainer = await this.createOptionsBrowser(rows);
 
-        // Make sure the browser is unloaded as soon as we change views,
-        // rather than waiting for the next detail view to load.
-        document.addEventListener("ViewChanged", function() {
-          browserContainer.remove();
-        }, {once: true});
+        if (browserContainer) {
+          // Make sure the browser is unloaded as soon as we change views,
+          // rather than waiting for the next detail view to load.
+          document.addEventListener("ViewChanged", function() {
+            browserContainer.remove();
+          }, {once: true});
+        }
 
         finish(browserContainer);
       });
     }
 
     if (aCallback)
       aCallback();
   },
@@ -3632,18 +3634,27 @@ var gDetailView = {
       let detailViewBoxObject = gDetailView.node.boxObject;
       top -= detailViewBoxObject.y;
 
       detailViewBoxObject.scrollTo(0, top);
     }
   },
 
   async createOptionsBrowser(parentNode) {
-    let stack = document.createElement("stack");
-    stack.setAttribute("id", "addon-options-prompts-stack");
+    const containerId = "addon-options-prompts-stack";
+
+    let stack = document.getElementById(containerId);
+
+    if (stack) {
+      // Remove the existent options container (if any).
+      stack.remove();
+    }
+
+    stack = document.createElement("stack");
+    stack.setAttribute("id", containerId);
 
     let browser = document.createElement("browser");
     browser.setAttribute("type", "content");
     browser.setAttribute("disableglobalhistory", "true");
     browser.setAttribute("id", "addon-options");
     browser.setAttribute("class", "inline-options-browser");
     browser.setAttribute("transparent", "true");
     browser.setAttribute("forcemessagemanager", "true");
@@ -3651,17 +3662,17 @@ var gDetailView = {
 
     // The outer about:addons document listens for key presses to focus
     // the search box when / is pressed.  But if we're focused inside an
     // options page, don't let those keypresses steal focus.
     browser.addEventListener("keypress", event => {
       event.stopPropagation();
     });
 
-    let {optionsURL} = this._addon;
+    let {optionsURL, optionsBrowserStyle} = this._addon;
     let remote = !E10SUtils.canLoadURIInProcess(optionsURL, Services.appinfo.PROCESS_TYPE_DEFAULT);
 
     let readyPromise;
     if (remote) {
       browser.setAttribute("remote", "true");
       browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
       readyPromise = promiseEvent("XULFrameLoaderCreated", browser);
     } else {
@@ -3670,40 +3681,59 @@ var gDetailView = {
 
     stack.appendChild(browser);
     parentNode.appendChild(stack);
 
     // Force bindings to apply synchronously.
     browser.clientTop;
 
     await readyPromise;
+
+    if (!browser.messageManager) {
+      // If the browser.messageManager is undefined, the browser element has been
+      // removed from the document in the meantime (e.g. due to a rapid sequence
+      // of addon reload), ensure that the stack is also removed and return null.
+      stack.remove();
+      return null;
+    }
+
     ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
 
     return new Promise(resolve => {
       let messageListener = {
         receiveMessage({name, data}) {
           if (name === "Extension:BrowserResized")
             browser.style.height = `${data.height}px`;
           else if (name === "Extension:BrowserContentLoaded")
             resolve(stack);
         },
       };
 
       let mm = browser.messageManager;
+
+      if (!mm) {
+        // If the browser.messageManager is undefined, the browser element has been
+        // removed from the document in the meantime (e.g. due to a rapid sequence
+        // of addon reload), ensure that the stack is also removed and return null.
+        stack.remove();
+        resolve(null);
+        return;
+      }
+
       mm.loadFrameScript("chrome://extensions/content/ext-browser-content.js",
                          false);
       mm.addMessageListener("Extension:BrowserContentLoaded", messageListener);
       mm.addMessageListener("Extension:BrowserResized", messageListener);
 
       let browserOptions = {
         fixedWidth: true,
         isInline: true,
       };
 
-      if (this._addon.optionsBrowserStyle) {
+      if (optionsBrowserStyle) {
         browserOptions.stylesheets = extensionStylesheets;
       }
 
       mm.sendAsyncMessage("Extension:InitBrowser", browserOptions);
 
       browser.loadURI(optionsURL);
     });
   },
--- a/toolkit/mozapps/extensions/test/browser/browser-common.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser-common.ini
@@ -52,8 +52,10 @@ skip-if = buildapp == 'mulet'
 [browser_pluginprefs.js]
 [browser_pluginprefs_is_not_disabled.js]
 skip-if = buildapp == 'mulet'
 [browser_CTP_plugins.js]
 tags = blocklist
 skip-if = buildapp == 'mulet'
 [browser_webext_options.js]
 tags = webextensions
+[browser_webext_options_addon_reload.js]
+tags = webextensions
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_options_addon_reload.js
@@ -0,0 +1,105 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {AddonTestUtils} = Cu.import("resource://testing-common/AddonTestUtils.jsm", {});
+const {ExtensionParent} = Cu.import("resource://gre/modules/ExtensionParent.jsm", {});
+
+// This test function helps to detect when an addon options browser have been inserted
+// in the about:addons page.
+function waitOptionsBrowserInserted() {
+  return new Promise(resolve => {
+    async function listener(eventName, browser) {
+      // wait for a webextension XUL browser element that is owned by the "about:addons" page.
+      if (browser.ownerDocument.location.href == "about:addons") {
+        ExtensionParent.apiManager.off("extension-browser-inserted", listener);
+
+        resolve(browser);
+      }
+    }
+    ExtensionParent.apiManager.on("extension-browser-inserted", listener);
+  });
+}
+
+add_task(async function test_options_on_addon_reload() {
+  const ID = "@test-options-on-addon-reload";
+
+  function backgroundScript() {
+    const {browser} = window;
+    browser.runtime.openOptionsPage();
+  }
+
+  let extensionDefinition = {
+    useAddonManager: "temporary",
+
+    manifest: {
+      "options_ui": {
+        "page": "options.html",
+      },
+      "applications": {
+        "gecko": {
+          "id": ID,
+        },
+      },
+    },
+    files: {
+      "options.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+          </head>
+          <body>
+            Extension Options UI
+          </body>
+        </html>`,
+    },
+    background: backgroundScript,
+  };
+
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:addons");
+
+  const extension = ExtensionTestUtils.loadExtension(extensionDefinition);
+
+  const onceOptionsBrowserInserted = waitOptionsBrowserInserted();
+
+  await extension.startup();
+
+  info("Wait the options_ui page XUL browser to be created");
+  await onceOptionsBrowserInserted;
+
+  const aboutAddonsDocument = gBrowser.selectedBrowser.contentDocument;
+
+  Assert.equal(aboutAddonsDocument.location.href, "about:addons",
+               "The about:addons page is the currently selected tab");
+
+  const optionsBrowsers = aboutAddonsDocument.querySelectorAll("#addon-options");
+  Assert.equal(optionsBrowsers.length, 1, "Got a single XUL browser for the addon options_ui page");
+
+  // Reload the addon five times in a row, and then check that there is still one addon options browser.
+
+  let addon = await AddonManager.getAddonByID(ID);
+
+  for (let i = 0; i < 5; i++) {
+    const onceOptionsReloaded = Promise.all([
+      // Reloading the addon currently prevents the extension.awaitMessage test helper to be able
+      // to receive test messages from the reloaded extension, this test function helps to wait
+      // the extension has been restarted on addon reload.
+      AddonTestUtils.promiseWebExtensionStartup(),
+      TestUtils.topicObserved(AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
+                              (subject, data) => data == extension.id),
+    ]);
+
+    await addon.reload();
+
+    info("Wait the new options_ui page XUL browser to be created");
+    await onceOptionsReloaded;
+
+    let optionsBrowsers = aboutAddonsDocument.querySelectorAll("#addon-options");
+
+    Assert.equal(optionsBrowsers.length, 1, "Got a single XUL browser for the addon options_ui page");
+  }
+
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+  await extension.unload();
+});