Bug 1233127 - Use createProcessingIntruction instead of addon-sdk to load stylesheets in tools;r=pbrosset
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 17 Dec 2015 17:00:28 -0800
changeset 301421 e1ce5b4fa814367b46379641a77f06eabd9348ed
parent 301420 060a16fa37d75fb4badaa9fa1c7a48454531dedc
child 301463 b32c4163ff05bd0293aa4e1cebc6636aaef60e1c
child 301496 8e54ec9a12f2af42dca48b957a88d2ce13e11d09
push idunknown
push userunknown
push dateunknown
reviewerspbrosset
bugs1233127
milestone46.0a1
Bug 1233127 - Use createProcessingIntruction instead of addon-sdk to load stylesheets in tools;r=pbrosset
devtools/client/framework/test/browser_toolbox_theme_registration.js
devtools/client/shared/test/browser_theme_switching.js
devtools/client/shared/theme-switching.js
--- a/devtools/client/framework/test/browser_toolbox_theme_registration.js
+++ b/devtools/client/framework/test/browser_toolbox_theme_registration.js
@@ -1,113 +1,89 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Test for dynamically registering and unregistering themes
 const CHROME_URL = "chrome://mochitests/content/browser/devtools/client/framework/test/";
 
 var toolbox;
 
-function test()
-{
-  gBrowser.selectedTab = gBrowser.addTab();
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
-    gDevTools.showToolbox(target).then(testRegister);
-  }, true);
-
-  content.location = "data:text/html,test for dynamically registering and unregistering themes";
-}
+add_task(function* themeRegistration() {
+  let tab = yield addTab("data:text/html,test");
+  let target = TargetFactory.forTab(tab);
+  toolbox = yield gDevTools.showToolbox(target);
 
-function testRegister(aToolbox)
-{
-  toolbox = aToolbox
-  gDevTools.once("theme-registered", themeRegistered);
+  let themeId = yield new Promise(resolve => {
+    gDevTools.once("theme-registered", (e, themeId) => {
+      resolve(themeId);
+    });
 
-  gDevTools.registerTheme({
-    id: "test-theme",
-    label: "Test theme",
-    stylesheets: [CHROME_URL + "doc_theme.css"],
-    classList: ["theme-test"],
+    gDevTools.registerTheme({
+      id: "test-theme",
+      label: "Test theme",
+      stylesheets: [CHROME_URL + "doc_theme.css"],
+      classList: ["theme-test"],
+    });
   });
-}
 
-function themeRegistered(event, themeId)
-{
   is(themeId, "test-theme", "theme-registered event handler sent theme id");
 
   ok(gDevTools.getThemeDefinitionMap().has(themeId), "theme added to map");
+});
 
-  // Test that new theme appears in the Options panel
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-  gDevTools.showToolbox(target, "options").then(() => {
-    let panel = toolbox.getCurrentPanel();
-    let doc = panel.panelWin.frameElement.contentDocument;
-    let themeOption = doc.querySelector("#devtools-theme-box > radio[value=test-theme]");
+add_task(function* themeInOptionsPanel() {
 
-    ok(themeOption, "new theme exists in the Options panel");
+  yield toolbox.selectTool("options");
 
-    // Apply the new theme.
-    applyTheme();
-  });
-}
-
-function applyTheme()
-{
+  let panel = toolbox.getCurrentPanel();
   let panelWin = toolbox.getCurrentPanel().panelWin;
   let doc = panelWin.frameElement.contentDocument;
+  let themeOption = doc.querySelector("#devtools-theme-box > radio[value=test-theme]");
+
+  ok(themeOption, "new theme exists in the Options panel");
+
   let testThemeOption = doc.querySelector("#devtools-theme-box > radio[value=test-theme]");
   let lightThemeOption = doc.querySelector("#devtools-theme-box > radio[value=light]");
 
   let color = panelWin.getComputedStyle(testThemeOption).color;
   isnot(color, "rgb(255, 0, 0)", "style unapplied");
 
   // Select test theme.
   testThemeOption.click();
 
+  info("Waiting for theme to finish loading");
+  yield once(panelWin, "theme-switch-complete");
+
   color = panelWin.getComputedStyle(testThemeOption).color;
   is(color, "rgb(255, 0, 0)", "style applied");
 
   // Select light theme
   lightThemeOption.click();
 
+  info("Waiting for theme to finish loading");
+  yield once(panelWin, "theme-switch-complete");
+
   color = panelWin.getComputedStyle(testThemeOption).color;
   isnot(color, "rgb(255, 0, 0)", "style unapplied");
 
   // Select test theme again.
   testThemeOption.click();
+});
 
-  // Then unregister the test theme.
-  testUnregister();
-}
-
-function testUnregister()
-{
+add_task(function* themeUnregistration() {
   gDevTools.unregisterTheme("test-theme");
 
   ok(!gDevTools.getThemeDefinitionMap().has("test-theme"), "theme removed from map");
 
   let panelWin = toolbox.getCurrentPanel().panelWin;
   let doc = panelWin.frameElement.contentDocument;
   let themeBox = doc.querySelector("#devtools-theme-box");
 
   // The default light theme must be selected now.
   is(themeBox.selectedItem, themeBox.querySelector("[value=light]"),
     "theme light must be selected");
-
-  // Make sure the tab-attaching process is done before we destroy the toolbox.
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-  let actor = target.activeTab.actor;
-  target.client.attachTab(actor, (response) => {
-    cleanup();
-  });
-}
+});
 
-function cleanup()
-{
-  toolbox.destroy().then(function() {
-    toolbox = null;
-    gBrowser.removeCurrentTab();
-    finish();
-  });
-}
+add_task(function* cleanup() {
+  yield toolbox.destroy();
+  toolbox = null;
+});
--- a/devtools/client/shared/test/browser_theme_switching.js
+++ b/devtools/client/shared/test/browser_theme_switching.js
@@ -2,25 +2,43 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var toolbox;
 
 add_task(function*() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   let toolbox = yield gDevTools.showToolbox(target);
-  let root = toolbox.frame.contentDocument.documentElement;
+  let doc = toolbox.frame.contentDocument;
+  let root = doc.documentElement;
 
   let platform = root.getAttribute("platform");
   let expectedPlatform = getPlatform();
   is(platform, expectedPlatform, ":root[platform] is correct");
 
   let theme = Services.prefs.getCharPref("devtools.theme");
   let className = "theme-" + theme;
-  ok(root.classList.contains(className), ":root has " + className + " class (current theme)");
+  ok(root.classList.contains(className),
+     ":root has " + className + " class (current theme)");
+
+  // Convert the xpath result into an array of strings
+  // like `href="{URL}" type="text/css"`
+  let sheetsIterator = doc.evaluate("processing-instruction('xml-stylesheet')",
+                       doc, null, XPathResult.ANY_TYPE, null);
+  let sheetsInDOM = [];
+  let sheet;
+  while (sheet = sheetsIterator.iterateNext()) {
+    sheetsInDOM.push(sheet.data);
+  }
+
+  let sheetsFromTheme = gDevTools.getThemeDefinition(theme).stylesheets;
+  info ("Checking for existence of " + sheetsInDOM.length + " sheets");
+  for (let sheet of sheetsFromTheme) {
+    ok(sheetsInDOM.some(s=>s.includes(sheet)), "There is a stylesheet for " + sheet);
+  }
 
   yield toolbox.destroy();
 });
 
 function getPlatform() {
   let {OS} = Services.appinfo;
   if (OS == "WINNT") {
     return "win";
--- a/devtools/client/shared/theme-switching.js
+++ b/devtools/client/shared/theme-switching.js
@@ -1,78 +1,116 @@
 /* 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/. */
 
 (function() {
-  const DEVTOOLS_SKIN_URL = "chrome://devtools/skin/";
+  const SCROLLBARS_URL = "chrome://devtools/skin/floating-scrollbars-light.css";
   let documentElement = document.documentElement;
+  let devtoolsStyleSheets = new WeakMap();
 
   function forceStyle() {
     let computedStyle = window.getComputedStyle(documentElement);
     if (!computedStyle) {
       // Null when documentElement is not ready. This method is anyways not
       // required then as scrollbars would be in their state without flushing.
       return;
     }
     let display = computedStyle.display; // Save display value
     documentElement.style.display = "none";
     window.getComputedStyle(documentElement).display; // Flush
     documentElement.style.display = display; // Restore
   }
 
+  /*
+   * Append a new processing instruction and return an object with
+   *  - styleSheet: DOMNode
+   *  - loadPromise: Promise that resolves once the sheets loads or errors
+   */
+  function appendStyleSheet(url) {
+    let styleSheetAttr = `href="${url}" type="text/css"`;
+    let styleSheet = document.createProcessingInstruction(
+      "xml-stylesheet", styleSheetAttr);
+    let loadPromise = new Promise((resolve, reject) => {
+      function onload() {
+        styleSheet.removeEventListener("load", onload);
+        styleSheet.removeEventListener("error", onerror);
+        resolve();
+      }
+      function onerror() {
+        styleSheet.removeEventListener("load", onload);
+        styleSheet.removeEventListener("error", onerror);
+        reject("Failed to load theme file " + url);
+      }
+
+      styleSheet.addEventListener("load", onload);
+      styleSheet.addEventListener("error", onerror);
+    });
+    document.insertBefore(styleSheet, documentElement);
+    return {styleSheet, loadPromise};
+  }
+
+  /*
+   * Notify the window that a theme switch finished so tests can check the DOM
+   */
+  function notifyWindow() {
+    window.dispatchEvent(new CustomEvent("theme-switch-complete", {}));
+  }
+
+  /*
+   * Apply all the sheets from `newTheme` and remove all of the sheets
+   * from `oldTheme`
+   */
   function switchTheme(newTheme, oldTheme) {
     if (newTheme === oldTheme) {
       return;
     }
 
     let oldThemeDef = gDevTools.getThemeDefinition(oldTheme);
 
     // Unload all theme stylesheets related to the old theme.
     if (oldThemeDef) {
-      for (let url of oldThemeDef.stylesheets) {
-        StylesheetUtils.removeSheet(window, url, "author");
+      for (let sheet of devtoolsStyleSheets.get(oldThemeDef) || []) {
+        sheet.remove();
       }
     }
 
     // Load all stylesheets associated with the new theme.
     let newThemeDef = gDevTools.getThemeDefinition(newTheme);
 
     // The theme might not be available anymore (e.g. uninstalled)
     // Use the default one.
     if (!newThemeDef) {
       newThemeDef = gDevTools.getThemeDefinition("light");
     }
 
+    // Store the sheets in a WeakMap for access later when the theme gets
+    // unapplied.  It's hard to query for processing instructions so this
+    // is an easy way to access them later without storing a property on
+    // the window
+    devtoolsStyleSheets.set(newThemeDef, []);
+
+    let loadEvents = [];
     for (let url of newThemeDef.stylesheets) {
-      StylesheetUtils.loadSheet(window, url, "author");
+      let {styleSheet,loadPromise} = appendStyleSheet(url);
+      devtoolsStyleSheets.get(newThemeDef).push(styleSheet);
+      loadEvents.push(loadPromise);
     }
 
     // Floating scroll-bars like in OSX
     let hiddenDOMWindow = Cc["@mozilla.org/appshell/appShellService;1"]
                  .getService(Ci.nsIAppShellService)
                  .hiddenDOMWindow;
 
     // TODO: extensions might want to customize scrollbar styles too.
     if (!hiddenDOMWindow.matchMedia("(-moz-overlay-scrollbars)").matches) {
-      let scrollbarsUrl = Services.io.newURI(
-        DEVTOOLS_SKIN_URL + "floating-scrollbars-light.css", null, null);
-
       if (newTheme == "dark") {
-        StylesheetUtils.loadSheet(
-          window,
-          scrollbarsUrl,
-          "agent"
-        );
+        StylesheetUtils.loadSheet(window, SCROLLBARS_URL, "agent");
       } else if (oldTheme == "dark") {
-        StylesheetUtils.removeSheet(
-          window,
-          scrollbarsUrl,
-          "agent"
-        );
+        StylesheetUtils.removeSheet(window, SCROLLBARS_URL, "agent");
       }
       forceStyle();
     }
 
     if (oldThemeDef) {
       for (let name of oldThemeDef.classList) {
         documentElement.classList.remove(name);
       }
@@ -87,26 +125,27 @@
     }
 
     if (newThemeDef.onApply) {
       newThemeDef.onApply(window, oldTheme);
     }
 
     // Final notification for further theme-switching related logic.
     gDevTools.emit("theme-switched", window, newTheme, oldTheme);
+
+    Promise.all(loadEvents).then(notifyWindow, console.error.bind(console));
   }
 
   function handlePrefChange(event, data) {
     if (data.pref == "devtools.theme") {
       switchTheme(data.newValue, data.oldValue);
     }
   }
 
   const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
   Cu.import("resource://gre/modules/Services.jsm");
   Cu.import("resource://devtools/client/framework/gDevTools.jsm");
   const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
   const StylesheetUtils = require("sdk/stylesheet/utils");
 
   let os;
   let platform = navigator.platform;
   if (platform.startsWith("Win")) {