Bug 1275287 - Use the same browser_style styling for options that we use for popups r?mixedpuppy draft
authorMatthew Wein <mwein@mozilla.com>
Thu, 02 Feb 2017 15:32:02 -0800
changeset 498041 a7b6756b58ae01bcf7fe5d9a8eabfa0f0ee33b86
parent 497875 6d38ad302429c98115c354d643e81987ecec5d3c
child 549063 42ec1d9cb54cac1027bf05a0ca8b8522df8c29d9
push id49089
push userbmo:mwein@mozilla.com
push dateTue, 14 Mar 2017 06:28:44 +0000
reviewersmixedpuppy
bugs1275287
milestone55.0a1
Bug 1275287 - Use the same browser_style styling for options that we use for popups r?mixedpuppy MozReview-Commit-ID: HtbkC9Nf6S8
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_optionsPage_browser_style.js
toolkit/components/extensions/ext-browser-content.js
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -60,16 +60,17 @@ support-files =
 [browser_ext_devtools_network.js]
 [browser_ext_devtools_page.js]
 [browser_ext_devtools_panel.js]
 [browser_ext_getViews.js]
 [browser_ext_incognito_views.js]
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
 [browser_ext_omnibox.js]
+[browser_ext_optionsPage_browser_style.js]
 [browser_ext_optionsPage_privileges.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_pageAction_popup_resize.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_pageAction_title.js]
 [browser_ext_popup_api_injection.js]
 [browser_ext_popup_background.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_browser_style.js
@@ -0,0 +1,78 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function* testOptionsBrowserStyle(optionsUI, assertMessage) {
+  function optionsScript() {
+    browser.test.onMessage.addListener((msgName, optionsUI, assertMessage) => {
+      if (msgName !== "check-style") {
+        browser.test.notifyFail("options-ui-browser_style");
+      }
+
+      let style = window.getComputedStyle(document.getElementById("button"));
+      let buttonBackgroundColor = style.backgroundColor;
+      let browserStyleBackgroundColor = "rgb(9, 150, 248)";
+      if (!("browser_style" in optionsUI) || optionsUI.browser_style) {
+        browser.test.assertEq(browserStyleBackgroundColor, buttonBackgroundColor, assertMessage);
+      } else {
+        browser.test.assertTrue(browserStyleBackgroundColor !== buttonBackgroundColor, assertMessage);
+      }
+
+      browser.test.notifyPass("options-ui-browser_style");
+    });
+    browser.test.sendMessage("options-ui-ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary",
+
+    manifest: {
+      "permissions": ["tabs"],
+      "options_ui": optionsUI,
+    },
+    files: {
+      "options.html": `
+        <!DOCTYPE html>
+        <html>
+          <button id="button" name="button" class="default">Default</button>
+          <script src="options.js" type="text/javascript"></script>
+        </html>`,
+      "options.js": optionsScript,
+    },
+    background() {
+      browser.runtime.openOptionsPage();
+    },
+  });
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+  yield extension.startup();
+  yield extension.awaitMessage("options-ui-ready");
+
+  extension.sendMessage("check-style", optionsUI, assertMessage);
+  yield extension.awaitFinish("options-ui-browser_style");
+
+  yield extension.unload();
+
+  yield BrowserTestUtils.removeTab(tab);
+}
+
+add_task(function* test_options_without_setting_browser_style() {
+  yield testOptionsBrowserStyle({
+    "page": "options.html",
+  }, "Expected correct style when browser_style is excluded");
+});
+
+add_task(function* test_options_with_browser_style_set_to_true() {
+  yield testOptionsBrowserStyle({
+    "page": "options.html",
+    "browser_style": true,
+  }, "Expected correct style when browser_style is set to `true`");
+});
+
+add_task(function* test_options_with_browser_style_set_to_false() {
+  yield testOptionsBrowserStyle({
+    "page": "options.html",
+    "browser_style": false,
+  }, "Expected no style when browser_style is set to `false`");
+});
--- a/toolkit/components/extensions/ext-browser-content.js
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -27,20 +27,21 @@ const {
 } = ExtensionUtils;
 
 /* globals addMessageListener, addEventListener, content, docShell, removeEventListener, sendAsyncMessage */
 
 // Minimum time between two resizes.
 const RESIZE_TIMEOUT = 100;
 
 const BrowserListener = {
-  init({allowScriptsToClose, fixedWidth, maxHeight, maxWidth, stylesheets}) {
+  init({allowScriptsToClose, fixedWidth, maxHeight, maxWidth, stylesheets, isInline}) {
     this.fixedWidth = fixedWidth;
     this.stylesheets = stylesheets || [];
 
+    this.isInline = isInline;
     this.maxWidth = maxWidth;
     this.maxHeight = maxHeight;
 
     this.oldBackground = null;
 
     if (allowScriptsToClose) {
       content.QueryInterface(Ci.nsIInterfaceRequestor)
              .getInterface(Ci.nsIDOMWindowUtils)
@@ -63,26 +64,30 @@ const BrowserListener = {
   },
 
   receiveMessage({name, data}) {
     if (name === "Extension:InitBrowser") {
       this.init(data);
     }
   },
 
+  loadStylesheets() {
+    let winUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIDOMWindowUtils);
+
+    for (let url of this.stylesheets) {
+      winUtils.addSheet(stylesheetMap.get(url), winUtils.AGENT_SHEET);
+    }
+  },
+
   handleEvent(event) {
     switch (event.type) {
       case "DOMWindowCreated":
         if (event.target === content.document) {
-          let winUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIDOMWindowUtils);
-
-          for (let url of this.stylesheets) {
-            winUtils.addSheet(stylesheetMap.get(url), winUtils.AGENT_SHEET);
-          }
+          this.loadStylesheets();
         }
         break;
 
       case "DOMWindowClose":
         if (event.target === content) {
           event.preventDefault();
 
           sendAsyncMessage("Extension:DOMWindowClose");
@@ -96,16 +101,22 @@ const BrowserListener = {
         }
         break;
 
       case "load":
         if (event.target.contentWindow === content) {
           // For about:addons inline <browser>s, we currently receive a load
           // event on the <browser> element, but no load or DOMContentLoaded
           // events from the content window.
+
+          // Inline browsers don't receive the "DOMWindowCreated" event, so this
+          // is a workaround to load the stylesheets.
+          if (this.isInline) {
+            this.loadStylesheets();
+          }
           sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href});
         } else if (event.target !== content.document) {
           break;
         }
 
         // We use a capturing listener, so we get this event earlier than any
         // load listeners in the content page. Resizing after a timeout ensures
         // that we calculate the size after the entire event cycle has completed
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -13,16 +13,17 @@ var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DownloadUtils.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
                                   "resource://gre/modules/ExtensionParent.jsm");
 
@@ -86,16 +87,24 @@ XPCOMUtils.defineLazyGetter(gStrings, "d
 
 XPCOMUtils.defineLazyGetter(gStrings, "brandShortName", function() {
   return this.brand.GetStringFromName("brandShortName");
 });
 XPCOMUtils.defineLazyGetter(gStrings, "appVersion", function() {
   return Services.appinfo.version;
 });
 
+XPCOMUtils.defineLazyGetter(this, "gInlineOptionsStylesheets", () => {
+  let stylesheets = ["chrome://browser/content/extension.css"];
+  if (AppConstants.platform === "macosx") {
+    stylesheets.push("chrome://browser/content/extension-mac.css");
+  }
+  return stylesheets;
+});
+
 document.addEventListener("load", initialize, true);
 window.addEventListener("unload", shutdown);
 
 class MessageDispatcher {
   constructor(target) {
     this.listeners = new Map();
     this.target = target;
   }
@@ -3644,17 +3653,27 @@ var gDetailView = {
         },
       };
 
       let mm = browser.messageManager || new FakeFrameMessageManager(browser);
       mm.loadFrameScript("chrome://extensions/content/ext-browser-content.js",
                          false);
       mm.addMessageListener("Extension:BrowserContentLoaded", messageListener);
       mm.addMessageListener("Extension:BrowserResized", messageListener);
-      mm.sendAsyncMessage("Extension:InitBrowser", {fixedWidth: true});
+
+      let browserOptions = {
+        fixedWidth: true,
+        isInline: true,
+      };
+
+      if (this._addon.optionsBrowserStyle) {
+        browserOptions.stylesheets = gInlineOptionsStylesheets;
+      }
+
+      mm.sendAsyncMessage("Extension:InitBrowser", browserOptions);
 
       browser.loadURI(optionsURL);
     });
   }),
 
   getSelectedAddon() {
     return this._addon;
   },
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -992,29 +992,36 @@ var loadManifestFromWebManifest = Task.a
   addon.unpack = false;
   addon.strictCompatibility = true;
   addon.bootstrap = true;
   addon.hasBinaryComponents = false;
   addon.multiprocessCompatible = true;
   addon.internalName = null;
   addon.updateURL = bss.update_url;
   addon.updateKey = null;
+  addon.optionsBrowserStyle = true;
   addon.optionsURL = null;
   addon.optionsType = null;
   addon.aboutURL = null;
   addon.dependencies = Object.freeze(Array.from(extension.dependencies));
 
   if (manifest.options_ui) {
     // Store just the relative path here, the AddonWrapper getURL
     // wrapper maps this to a full URL.
     addon.optionsURL = manifest.options_ui.page;
     if (manifest.options_ui.open_in_tab)
       addon.optionsType = AddonManager.OPTIONS_TYPE_TAB;
     else
       addon.optionsType = AddonManager.OPTIONS_TYPE_INLINE_BROWSER;
+
+    if (manifest.options_ui.browser_style === null)
+      logger.warn("Please specify whether you want browser_style " +
+          "or not in your options_ui options.");
+    else
+      addon.optionsBrowserStyle = manifest.options_ui.browser_style;
   }
 
   // WebExtensions don't use iconURLs
   addon.iconURL = null;
   addon.icon64URL = null;
   addon.icons = manifest.icons || {};
   addon.userPermissions = extension.userPermissions();
 
@@ -1242,21 +1249,22 @@ let loadManifestFromRDF = Task.async(fun
       }
     }
   } else {
     // Some add-on types are always restartless.
     if (RESTARTLESS_TYPES.has(addon.type)) {
       addon.bootstrap = true;
     }
 
-    // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
-    // all other types they are silently ignored
+    // Only extensions are allowed to provide an optionsURL, optionsType,
+    // optionsBrowserStyle, or aboutURL. For all other types they are silently ignored
+    addon.aboutURL = null;
+    addon.optionsBrowserStyle = null;
+    addon.optionsType = null;
     addon.optionsURL = null;
-    addon.optionsType = null;
-    addon.aboutURL = null;
 
     if (addon.type == "theme") {
       if (!addon.internalName)
         throw new Error("Themes must include an internalName property");
       addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true";
     }
   }
 
@@ -7329,16 +7337,21 @@ AddonWrapper.prototype = {
       return AddonManager.OPTIONS_TYPE_INLINE;
 
     if (hasOptionsURL)
       return AddonManager.OPTIONS_TYPE_DIALOG;
 
     return null;
   },
 
+  get optionsBrowserStyle() {
+    let addon = addonFor(this);
+    return addon.optionsBrowserStyle;
+  },
+
   get iconURL() {
     return AddonManager.getPreferredIconURL(this, 48);
   },
 
   get icon64URL() {
     return AddonManager.getPreferredIconURL(this, 64);
   },
 
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -76,26 +76,26 @@ const DB_METADATA        = ["syncGUID",
 const DB_BOOL_METADATA   = ["visible", "active", "userDisabled", "appDisabled",
                             "pendingUninstall", "bootstrap", "skinnable",
                             "softDisabled", "isForeignInstall",
                             "hasBinaryComponents", "strictCompatibility"];
 
 // Properties to save in JSON file
 const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
                           "internalName", "updateURL", "updateKey", "optionsURL",
-                          "optionsType", "aboutURL", "icons", "iconURL", "icon64URL",
+                          "optionsType", "optionsBrowserStyle", "aboutURL",
                           "defaultLocale", "visible", "active", "userDisabled",
                           "appDisabled", "pendingUninstall", "descriptor", "installDate",
                           "updateDate", "applyBackgroundUpdates", "bootstrap",
                           "skinnable", "size", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall", "hasBinaryComponents",
                           "strictCompatibility", "locales", "targetApplications",
                           "targetPlatforms", "multiprocessCompatible", "signedState",
                           "seen", "dependencies", "hasEmbeddedWebExtension", "mpcOptedOut",
-                          "userPermissions"];
+                          "userPermissions", "icons", "iconURL", "icon64URL"];
 
 // Properties that should be migrated where possible from an old database. These
 // shouldn't include properties that can be read directly from install.rdf files
 // or calculated
 const DB_MIGRATE_METADATA = ["installDate", "userDisabled", "softDisabled",
                             "sourceURI", "applyBackgroundUpdates",
                             "releaseNotesURI", "foreignInstall", "syncGUID"];