Bug 1418602 - Allow theming sidebars. r=mconley
authorTim Nguyen <ntim.bugs@gmail.com>
Mon, 06 Aug 2018 17:46:43 +0100
changeset 487696 e751eeb6e0328bb16858c7cc090d5bc81369fd54
parent 487695 3486c61e21091dfdb5f58b92711b16e4f610af27
child 487697 6f2e61c51d4b96589f105bdc4c2de2921f67d183
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1418602
milestone63.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 1418602 - Allow theming sidebars. r=mconley MozReview-Commit-ID: 97zkU7raehV
browser/actors/LightweightThemeChild.jsm
browser/base/content/contentTheme.js
browser/components/places/content/bookmarksSidebar.js
browser/components/places/content/bookmarksSidebar.xul
browser/components/places/content/historySidebar.js
browser/components/places/content/historySidebar.xul
browser/modules/ThemeVariableMap.jsm
browser/themes/linux/places/sidebar.css
browser/themes/osx/places/sidebar.css
browser/themes/shared/places/sidebar.inc.css
browser/themes/windows/places/sidebar.css
toolkit/components/extensions/parent/ext-theme.js
toolkit/components/extensions/schemas/theme.json
toolkit/components/extensions/test/browser/browser.ini
toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
toolkit/components/extensions/test/browser/head.js
--- a/browser/actors/LightweightThemeChild.jsm
+++ b/browser/actors/LightweightThemeChild.jsm
@@ -6,16 +6,18 @@
 
 var EXPORTED_SYMBOLS = ["LightweightThemeChild"];
 
 ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 /**
  * LightweightThemeChild forwards theme data to in-content pages.
+ * It is both instantiated by the traditional Actor mechanism,
+ * and also manually within the sidebar JS global (which has no message manager)
  */
 class LightweightThemeChild extends ActorChild {
   constructor(mm) {
     super(mm);
 
     this.init();
   }
 
--- a/browser/base/content/contentTheme.js
+++ b/browser/base/content/contentTheme.js
@@ -27,16 +27,53 @@ const inContentVariableMap = [
       const {r, g, b, a} = rgbaChannels;
       if (!_isTextColorDark(r, g, b)) {
         element.setAttribute("lwt-newtab-brighttext", "true");
       }
 
       return `rgba(${r}, ${g}, ${b}, ${a})`;
     },
   }],
+  ["--lwt-sidebar-background-color", {
+    lwtProperty: "sidebar",
+  }],
+  ["--lwt-sidebar-text-color", {
+    lwtProperty: "sidebar_text",
+    processColor(rgbaChannels, element) {
+      if (!rgbaChannels) {
+        element.removeAttribute("lwt-sidebar");
+        element.removeAttribute("lwt-sidebar-brighttext");
+        return null;
+      }
+
+      element.setAttribute("lwt-sidebar", "true");
+      const {r, g, b, a} = rgbaChannels;
+      if (!_isTextColorDark(r, g, b)) {
+        element.setAttribute("lwt-sidebar-brighttext", "true");
+      }
+
+      return `rgba(${r}, ${g}, ${b}, ${a})`;
+    },
+  }],
+  ["--lwt-sidebar-highlight-background-color", {
+    lwtProperty: "sidebar_highlight",
+  }],
+  ["--lwt-sidebar-highlight-text-color", {
+    lwtProperty: "sidebar_highlight_text",
+    processColor(rgbaChannels, element) {
+      if (!rgbaChannels) {
+        element.removeAttribute("lwt-sidebar-highlight");
+        return null;
+      }
+      element.setAttribute("lwt-sidebar-highlight", "true");
+
+      const {r, g, b, a} = rgbaChannels;
+      return `rgba(${r}, ${g}, ${b}, ${a})`;
+    },
+  }],
 ];
 
 /**
  * ContentThemeController handles theme updates sent by the frame script.
  * To be able to use ContentThemeController, you must add your page to the whitelist
  * in LightweightThemeChildListener.jsm
  */
 const ContentThemeController = {
@@ -53,17 +90,19 @@ const ContentThemeController = {
    * @param {Object} event object containing the theme update.
    */
   handleEvent({ type, detail }) {
     if (type == "LightweightTheme:Set") {
       let {data} = detail;
       if (!data) {
         data = {};
       }
-      this._setProperties(document.body, data);
+      // XUL documents don't have a body
+      const element = document.body ? document.body : document.documentElement;
+      this._setProperties(element, data);
     }
   },
 
   /**
    * Set a CSS variable to a given value
    * @param {Element} elem The element where the CSS variable should be added.
    * @param {string} variableName The CSS variable to set.
    * @param {string} value The new value of the CSS variable.
@@ -85,17 +124,17 @@ const ContentThemeController = {
     for (let [cssVarName, definition] of inContentVariableMap) {
       const {
         lwtProperty,
         processColor,
       } = definition;
       let value = themeData[lwtProperty];
 
       if (processColor) {
-        value = processColor(value, document.body);
+        value = processColor(value, elem);
       } else if (value) {
         const {r, g, b, a} = value;
         value = `rgba(${r}, ${g}, ${b}, ${a})`;
       }
 
       this._setProperty(elem, cssVarName, value);
     }
   },
--- a/browser/components/places/content/bookmarksSidebar.js
+++ b/browser/components/places/content/bookmarksSidebar.js
@@ -2,16 +2,17 @@
 /* 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/. */
 
 /* Shared Places Import - change other consumers if you change this: */
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
+  LightweightThemeChild: "resource:///actors/LightweightThemeChild.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
   PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
 });
 XPCOMUtils.defineLazyScriptGetter(this, "PlacesTreeView",
                                   "chrome://browser/content/places/treeView.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["PlacesInsertionPoint", "PlacesController",
@@ -20,16 +21,27 @@ XPCOMUtils.defineLazyScriptGetter(this, 
 /* End Shared Places Import */
 
 function init() {
   let uidensity = window.top.document.documentElement.getAttribute("uidensity");
   if (uidensity) {
     document.documentElement.setAttribute("uidensity", uidensity);
   }
 
+  /* Listen for sidebar theme changes */
+  let themeListener = new LightweightThemeChild({
+    content: window,
+    chromeOuterWindowID: window.top.windowUtils.outerWindowID,
+    docShell: window.docShell,
+  });
+
+  window.addEventListener("unload", () => {
+    themeListener.cleanup();
+  });
+
   document.getElementById("bookmarks-view").place =
     "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY;
 }
 
 function searchBookmarks(aSearchString) {
   var tree = document.getElementById("bookmarks-view");
   if (!aSearchString) {
     // eslint-disable-next-line no-self-assign
--- a/browser/components/places/content/bookmarksSidebar.xul
+++ b/browser/components/places/content/bookmarksSidebar.xul
@@ -26,16 +26,18 @@
       aria-label="&bookmarksButton.label;">
 
   <script type="application/javascript"
           src="chrome://browser/content/places/bookmarksSidebar.js"/>
   <script type="application/javascript"
           src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
+  <script type="application/javascript"
+          src="chrome://browser/content/contentTheme.js"/>
 
 #include placesCommands.inc.xul
 #include ../../../../toolkit/content/editMenuCommands.inc.xul
 #include placesContextMenu.inc.xul
 #include bookmarksHistoryTooltip.inc.xul
 
   <hbox id="sidebar-search-container" align="center">
     <textbox id="search-box" flex="1" type="search"
--- a/browser/components/places/content/historySidebar.js
+++ b/browser/components/places/content/historySidebar.js
@@ -2,16 +2,17 @@
 /* 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/. */
 
 /* Shared Places Import - change other consumers if you change this: */
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
+  LightweightThemeChild: "resource:///actors/LightweightThemeChild.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
   PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
 });
 XPCOMUtils.defineLazyScriptGetter(this, "PlacesTreeView",
                                   "chrome://browser/content/places/treeView.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["PlacesInsertionPoint", "PlacesController",
@@ -27,16 +28,27 @@ var gHistoryGrouping = "";
 var gSearching = false;
 
 function HistorySidebarInit() {
   let uidensity = window.top.document.documentElement.getAttribute("uidensity");
   if (uidensity) {
     document.documentElement.setAttribute("uidensity", uidensity);
   }
 
+  /* Listen for sidebar theme changes */
+  let themeListener = new LightweightThemeChild({
+    content: window,
+    chromeOuterWindowID: window.top.windowUtils.outerWindowID,
+    docShell: window.docShell,
+  });
+
+  window.addEventListener("unload", () => {
+    themeListener.cleanup();
+  });
+
   gHistoryTree = document.getElementById("historyTree");
   gSearchBox = document.getElementById("search-box");
 
   gHistoryGrouping = document.getElementById("viewButton").
                               getAttribute("selectedsort");
 
   if (gHistoryGrouping == "site")
     document.getElementById("bysite").setAttribute("checked", "true");
--- a/browser/components/places/content/historySidebar.xul
+++ b/browser/components/places/content/historySidebar.xul
@@ -26,16 +26,18 @@
       aria-label="&historyButton.label;">
 
   <script type="application/javascript"
           src="chrome://browser/content/places/historySidebar.js"/>
   <script type="application/javascript"
           src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
+  <script type="application/javascript"
+          src="chrome://browser/content/contentTheme.js"/>
 
 #include ../../../../toolkit/content/editMenuCommands.inc.xul
 
 #include placesCommands.inc.xul
 
 #include ../../../../toolkit/content/editMenuKeys.inc.xul
 #ifdef XP_MACOSX
   <keyset id="editMenuKeysExtra">
--- a/browser/modules/ThemeVariableMap.jsm
+++ b/browser/modules/ThemeVariableMap.jsm
@@ -79,9 +79,13 @@ const ThemeVariableMap = [
   ["--autocomplete-popup-highlight-color", {
     lwtProperty: "popup_highlight_text"
   }],
 ];
 
 const ThemeContentPropertyList = [
   "ntp_background",
   "ntp_text",
+  "sidebar",
+  "sidebar_highlight",
+  "sidebar_highlight_text",
+  "sidebar_text",
 ];
--- a/browser/themes/linux/places/sidebar.css
+++ b/browser/themes/linux/places/sidebar.css
@@ -18,23 +18,16 @@
   margin: 1px 0;
   margin-inline-start: 4px;
 }
 
 #viewButton:-moz-focusring:not(:hover):not([open]) {
   outline: 1px dotted -moz-DialogText;
 }
 
-.sidebar-placesTree {
-  margin: 0;
-  color: inherit;
-  -moz-appearance: none;
-  background: transparent;
-}
-
 :root[uidensity=touch] #search-box,
 :root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
   min-height: 32px;
 }
 
 .sidebar-placesTreechildren::-moz-tree-cell(leaf) ,
 .sidebar-placesTreechildren::-moz-tree-image(leaf) {
   cursor: pointer;
--- a/browser/themes/osx/places/sidebar.css
+++ b/browser/themes/osx/places/sidebar.css
@@ -5,16 +5,19 @@
 /* Sidebars */
 
 %include ../../shared/places/sidebar.inc.css
 
 .sidebar-placesTree {
   margin: 0;
   /* Default font size is 11px on mac, so this is 12px */
   font-size: 1.0909rem;
+}
+
+.sidebar-panel:not([lwt-sidebar]) .sidebar-placesTree {
   -moz-appearance: -moz-mac-source-list;
   -moz-font-smoothing-background-color: -moz-mac-source-list;
 }
 
 :root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
   min-height: 32px;
 }
 
--- a/browser/themes/shared/places/sidebar.inc.css
+++ b/browser/themes/shared/places/sidebar.inc.css
@@ -2,16 +2,61 @@
  * 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/. */
 
 .sidebar-panel {
   -moz-appearance: none;
   background-color: transparent;
 }
 
+/* Themed sidebars */
+
+.sidebar-panel[lwt-sidebar] {
+  background-color: var(--lwt-sidebar-background-color);
+  color: var(--lwt-sidebar-text-color);
+}
+
+.sidebar-panel[lwt-sidebar] .sidebar-placesTreechildren::-moz-tree-row(selected) {
+  background-color: hsla(0,0%,80%,.3);
+}
+
+.sidebar-panel[lwt-sidebar-brighttext] .sidebar-placesTreechildren::-moz-tree-row(selected) {
+  -moz-appearance: none;
+  background-color: rgba(249,249,250,.1);
+}
+
+.sidebar-panel[lwt-sidebar-brighttext] .sidebar-placesTreechildren::-moz-tree-image(selected),
+.sidebar-panel[lwt-sidebar-brighttext] .sidebar-placesTreechildren::-moz-tree-twisty(selected),
+.sidebar-panel[lwt-sidebar-brighttext] .sidebar-placesTreechildren::-moz-tree-cell-text(selected) {
+  color: var(--lwt-sidebar-text-color);
+}
+
+.sidebar-panel[lwt-sidebar-highlight] .sidebar-placesTreechildren::-moz-tree-row(selected,focus) {
+  -moz-appearance: none;
+  background-color: var(--lwt-sidebar-highlight-background-color);
+}
+
+.sidebar-panel[lwt-sidebar-highlight] .sidebar-placesTreechildren::-moz-tree-image(selected, focus),
+.sidebar-panel[lwt-sidebar-highlight] .sidebar-placesTreechildren::-moz-tree-twisty(selected, focus),
+.sidebar-panel[lwt-sidebar-highlight] .sidebar-placesTreechildren::-moz-tree-cell-text(selected, focus) {
+  color: var(--lwt-sidebar-highlight-text-color);
+}
+
+/* Sidebar tree */
+
+.sidebar-placesTree {
+  -moz-appearance: none;
+  background-color: transparent;
+  color: inherit;
+  border: 0;
+  margin: 0;
+}
+
+/* View button */
+
 #viewButton {
   -moz-appearance: none;
   border-radius: 4px;
   padding: 2px 4px;
   color: inherit;
 }
 
 #viewButton:hover {
--- a/browser/themes/windows/places/sidebar.css
+++ b/browser/themes/windows/places/sidebar.css
@@ -1,24 +1,16 @@
 /* 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/. */
 
 /* Sidebars */
 
 %include ../../shared/places/sidebar.inc.css
 
-.sidebar-placesTree {
-  -moz-appearance: none;
-  background-color: transparent;
-  color: inherit;
-  border: 0;
-  margin: 0;
-}
-
 :root[uidensity=touch] #search-box,
 :root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
   min-height: 32px;
 }
 
 .sidebar-placesTreechildren::-moz-tree-cell,
 .sidebar-placesTreechildren::-moz-tree-twisty {
   padding: 0 4px;
--- a/toolkit/components/extensions/parent/ext-theme.js
+++ b/toolkit/components/extensions/parent/ext-theme.js
@@ -1,12 +1,14 @@
 "use strict";
 
 /* global windowTracker, EventManager, EventEmitter */
 
+/* eslint-disable complexity */
+
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "LightweightThemeManager",
                                "resource://gre/modules/LightweightThemeManager.jsm");
 
 var {
   getWinUtils,
 } = ExtensionUtils;
@@ -179,16 +181,20 @@ class Theme {
         case "button_background_active":
         case "popup":
         case "popup_text":
         case "popup_border":
         case "popup_highlight":
         case "popup_highlight_text":
         case "ntp_background":
         case "ntp_text":
+        case "sidebar":
+        case "sidebar_text":
+        case "sidebar_highlight":
+        case "sidebar_highlight_text":
           this.lwtStyles[color] = cssColor;
           break;
         default:
           if (this.experiment && this.experiment.colors && color in this.experiment.colors) {
             this.lwtStyles.experimental.colors[color] = cssColor;
           } else {
             const {logger} = this.extension;
             logger.warn(`Unrecognized theme property found: colors.${color}`);
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -231,16 +231,32 @@
               },
               "ntp_background": {
                 "$ref": "ThemeColor",
                 "optional": true
               },
               "ntp_text": {
                 "$ref": "ThemeColor",
                 "optional": true
+              },
+              "sidebar": {
+                "$ref": "ThemeColor",
+                "optional": true
+              },
+              "sidebar_text": {
+                "$ref": "ThemeColor",
+                "optional": true
+              },
+              "sidebar_highlight": {
+                "$ref": "ThemeColor",
+                "optional": true
+              },
+              "sidebar_highlight_text": {
+                "$ref": "ThemeColor",
+                "optional": true
               }
             },
             "additionalProperties": { "$ref": "ThemeColor" }
           },
           "icons": {
             "type": "object",
             "optional": true,
             "properties": {
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -27,10 +27,11 @@ skip-if = verify
 [browser_ext_themes_toolbars.js]
 [browser_ext_themes_toolbarbutton_icons.js]
 [browser_ext_themes_toolbarbutton_colors.js]
 [browser_ext_themes_theme_transition.js]
 [browser_ext_themes_arrowpanels.js]
 [browser_ext_themes_tab_selected.js]
 [browser_ext_themes_autocomplete_popup.js]
 [browser_ext_themes_sanitization.js]
+[browser_ext_themes_sidebars.js]
 [browser_ext_themes_findbar.js]
 [browser_ext_themes_warnings.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
@@ -0,0 +1,136 @@
+"use strict";
+
+// This test checks whether the sidebar color properties work.
+
+/**
+ * Test whether the selected browser has the sidebar theme applied
+ * @param {Object} theme that is applied
+ * @param {boolean} isBrightText whether the brighttext attribute should be set
+ */
+async function test_sidebar_theme(theme, isBrightText) {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      theme,
+    },
+  });
+
+  const content = SidebarUI.browser.contentWindow;
+  const root = content.document.documentElement;
+
+  ok(!root.hasAttribute("lwt-sidebar"),
+     "Sidebar should not have lwt-sidebar attribute");
+  ok(!root.hasAttribute("lwt-sidebar-brighttext"),
+     "Sidebar should not have lwt-sidebar-brighttext attribute");
+  ok(!root.hasAttribute("lwt-sidebar-highlight"),
+     "Sidebar should not have lwt-sidebar-highlight attribute");
+
+  const rootCS = content.getComputedStyle(root);
+  const originalBackground = rootCS.backgroundColor;
+  const originalColor = rootCS.color;
+
+  // ::-moz-tree-row(selected, focus) computed style can't be accessed, so we create a fake one.
+  const highlightCS = {
+    get backgroundColor() {
+      // Standardize to rgb like other computed style.
+      let color = rootCS.getPropertyValue("--lwt-sidebar-highlight-background-color");
+      let [r, g, b] = color.replace("rgba(", "")
+        .split(",")
+        .map(channel => parseInt(channel, 10));
+      return `rgb(${r}, ${g}, ${b})`;
+    },
+
+    get color() {
+      let color = rootCS.getPropertyValue("--lwt-sidebar-highlight-text-color");
+      let [r, g, b] = color.replace("rgba(", "")
+        .split(",")
+        .map(channel => parseInt(channel, 10));
+      return `rgb(${r}, ${g}, ${b})`;
+    },
+  };
+  const originalHighlightBackground = highlightCS.backgroundColor;
+  const originalHighlightColor = highlightCS.color;
+
+  await extension.startup();
+
+  Services.ppmm.sharedData.flush();
+
+  const actualBackground = hexToCSS(theme.colors.sidebar) || originalBackground;
+  const actualColor = hexToCSS(theme.colors.sidebar_text) || originalColor;
+  const actualHighlightBackground = hexToCSS(theme.colors.sidebar_highlight) || originalHighlightBackground;
+  const actualHighlightColor = hexToCSS(theme.colors.sidebar_highlight_text) || originalHighlightColor;
+  const isCustomHighlight = !!theme.colors.sidebar_highlight_text;
+  const isCustomSidebar = !!theme.colors.sidebar_text;
+
+  is(root.hasAttribute("lwt-sidebar"), isCustomSidebar,
+     `Sidebar should${!isCustomSidebar ? " not" : ""} have lwt-sidebar attribute`);
+  is(root.hasAttribute("lwt-sidebar-brighttext"), isBrightText,
+     `Sidebar should${!isBrightText ? " not" : ""} have lwt-sidebar-brighttext attribute`);
+  is(root.hasAttribute("lwt-sidebar-highlight"), isCustomHighlight,
+     `Sidebar should${!isCustomHighlight ? " not" : ""} have lwt-sidebar-highlight attribute`);
+
+  is(rootCS.backgroundColor, actualBackground, "Sidebar background should be set.");
+  is(rootCS.color, actualColor, "Sidebar text color should be set.");
+
+  is(highlightCS.backgroundColor, actualHighlightBackground,
+     "Sidebar highlight background color should be set.");
+  is(highlightCS.color, actualHighlightColor,
+     "Sidebar highlight text color should be set.");
+
+  await extension.unload();
+
+  Services.ppmm.sharedData.flush();
+
+  ok(!root.hasAttribute("lwt-sidebar"),
+     "Sidebar should not have lwt-sidebar attribute");
+  ok(!root.hasAttribute("lwt-sidebar-brighttext"),
+     "Sidebar should not have lwt-sidebar-brighttext attribute");
+  ok(!root.hasAttribute("lwt-sidebar-highlight"),
+     "Sidebar should not have lwt-sidebar-highlight attribute");
+
+  is(rootCS.backgroundColor, originalBackground,
+     "Sidebar background should be reset.");
+  is(rootCS.color, originalColor,
+     "Sidebar text color should be reset.");
+  is(highlightCS.backgroundColor, originalHighlightBackground,
+     "Sidebar highlight background color should be reset.");
+  is(highlightCS.color, originalHighlightColor,
+     "Sidebar highlight text color should be reset.");
+}
+
+add_task(async function test_support_sidebar_colors() {
+  for (let command of ["viewBookmarksSidebar", "viewHistorySidebar"]) {
+    info("Executing command: " + command);
+
+    await SidebarUI.show(command);
+
+    await test_sidebar_theme({
+      colors: {
+        sidebar: "#fafad2", // lightgoldenrodyellow
+        sidebar_text: "#2f4f4f", // darkslategrey
+      },
+    }, false);
+
+    await test_sidebar_theme({
+      colors: {
+        sidebar: "#8b4513", // saddlebrown
+        sidebar_text: "#ffa07a", // lightsalmon
+      },
+    }, true);
+
+    await test_sidebar_theme({
+      colors: {
+        sidebar: "#fffafa", // snow
+        sidebar_text: "#663399", // rebeccapurple
+        sidebar_highlight: "#7cfc00", // lawngreen
+        sidebar_highlight_text: "#ffefd5", // papayawhip
+      },
+    }, false);
+
+    await test_sidebar_theme({
+      colors: {
+        sidebar_highlight: "#a0522d", // sienna
+        sidebar_highlight_text: "#fff5ee", // seashell
+      },
+    }, false);
+  }
+});
--- a/toolkit/components/extensions/test/browser/head.js
+++ b/toolkit/components/extensions/test/browser/head.js
@@ -33,25 +33,31 @@ const ENCODED_IMAGE_DATA = "iVBORw0KGgoA
   "eG+mgfrcLHh3C79bx6wttGEqERiH/AjPohWMouv2ZAAAAAElFTkSuQmCC";
 const ACCENT_COLOR = "#a14040";
 const TEXT_COLOR = "#fac96e";
 // For testing aliases of the colors above:
 const FRAME_COLOR = [71, 105, 91];
 const TAB_BACKGROUND_TEXT_COLOR = [207, 221, 192, .9];
 
 function hexToRGB(hex) {
+  if (!hex) {
+    return null;
+  }
   hex = parseInt((hex.indexOf("#") > -1 ? hex.substring(1) : hex), 16);
   return [hex >> 16, (hex & 0x00FF00) >> 8, (hex & 0x0000FF)];
 }
 
 function rgbToCSS(rgb) {
   return `rgb(${rgb.join(", ")})`;
 }
 
 function hexToCSS(hex) {
+  if (!hex) {
+    return null;
+  }
   return rgbToCSS(hexToRGB(hex));
 }
 
 function imageBufferFromDataURI(encodedImageData) {
   let decodedImageData = atob(encodedImageData);
   return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
 }