Bug 1409697 - Fix multiple addon options rendered on addon reload.
MozReview-Commit-ID: 5IRvDqdW1ZO
--- 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();
+});