Bug 1382173 - devtools shim support initialized and installed states;r=ochameau draft
authorJulian Descottes <jdescottes@mozilla.com>
Wed, 19 Jul 2017 13:40:53 +0200
changeset 611357 372e1f32746e4cc6e2686b9b92848d18ffde576b
parent 610636 9d79413df3f4caa4e0b36b434097848709125962
child 638136 73360113101f4a95da4886aab9d1abdbeb2a3ddf
push id69194
push userjdescottes@mozilla.com
push dateWed, 19 Jul 2017 16:01:16 +0000
reviewersochameau
bugs1382173
milestone56.0a1
Bug 1382173 - devtools shim support initialized and installed states;r=ochameau MozReview-Commit-ID: 4CmfzyLVrM4
devtools/shim/DevToolsShim.jsm
devtools/shim/tests/unit/test_devtools_shim.js
--- a/devtools/shim/DevToolsShim.jsm
+++ b/devtools/shim/DevToolsShim.jsm
@@ -1,14 +1,18 @@
 /* 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/. */
 
 "use strict";
 
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
 this.EXPORTED_SYMBOLS = [
   "DevToolsShim",
 ];
 
 function removeItem(array, callback) {
   let index = array.findIndex(callback);
   if (index >= 0) {
     array.splice(index, 1);
@@ -32,21 +36,32 @@ function removeItem(array, callback) {
  */
 this.DevToolsShim = {
   gDevTools: null,
   listeners: [],
   tools: [],
   themes: [],
 
   /**
-   * Check if DevTools are currently installed and available.
+   * Check if DevTools are currently installed (but not necessarily initialized).
    *
    * @return {Boolean} true if DevTools are installed.
    */
   isInstalled: function () {
+    return Services.io.getProtocolHandler("resource")
+             .QueryInterface(Ci.nsIResProtocolHandler)
+             .hasSubstitution("devtools");
+  },
+
+  /**
+   * Check if DevTools have already been initialized.
+   *
+   * @return {Boolean} true if DevTools are initialized.
+   */
+  isInitialized: function () {
     return !!this.gDevTools;
   },
 
   /**
    * Register an instance of gDevTools. Should be called by DevTools during startup.
    *
    * @param {DevTools} a devtools instance (from client/framework/devtools)
    */
@@ -56,129 +71,139 @@ this.DevToolsShim = {
     this.gDevTools.emit("devtools-registered");
   },
 
   /**
    * Unregister the current instance of gDevTools. Should be called by DevTools during
    * shutdown.
    */
   unregister: function () {
-    if (this.isInstalled()) {
+    if (this.isInitialized()) {
       this.gDevTools.emit("devtools-unregistered");
       this.gDevTools = null;
     }
   },
 
   /**
-   * The following methods can be called before DevTools are installed:
+   * The following methods can be called before DevTools are initialized:
    * - on
    * - off
    * - registerTool
    * - unregisterTool
    * - registerTheme
    * - unregisterTheme
    *
-   * If DevTools are not installed when calling the method, DevToolsShim will call the
+   * If DevTools are not initialized when calling the method, DevToolsShim will call the
    * appropriate method as soon as a gDevTools instance is registered.
    */
 
   /**
    * This method is used by browser/components/extensions/ext-devtools.js for the events:
    * - toolbox-created
    * - toolbox-destroyed
    */
   on: function (event, listener) {
-    if (this.isInstalled()) {
+    if (this.isInitialized()) {
       this.gDevTools.on(event, listener);
     } else {
       this.listeners.push([event, listener]);
     }
   },
 
   /**
    * This method is currently only used by devtools code, but is kept here for consistency
    * with on().
    */
   off: function (event, listener) {
-    if (this.isInstalled()) {
+    if (this.isInitialized()) {
       this.gDevTools.off(event, listener);
     } else {
       removeItem(this.listeners, ([e, l]) => e === event && l === listener);
     }
   },
 
   /**
    * This method is only used by the addon-sdk and should be removed when Firefox 56 is
    * no longer supported.
    */
   registerTool: function (tool) {
-    if (this.isInstalled()) {
+    if (this.isInitialized()) {
       this.gDevTools.registerTool(tool);
     } else {
       this.tools.push(tool);
     }
   },
 
   /**
    * This method is only used by the addon-sdk and should be removed when Firefox 56 is
    * no longer supported.
    */
   unregisterTool: function (tool) {
-    if (this.isInstalled()) {
+    if (this.isInitialized()) {
       this.gDevTools.unregisterTool(tool);
     } else {
       removeItem(this.tools, t => t === tool);
     }
   },
 
   /**
    * This method is only used by the addon-sdk and should be removed when Firefox 56 is
    * no longer supported.
    */
   registerTheme: function (theme) {
-    if (this.isInstalled()) {
+    if (this.isInitialized()) {
       this.gDevTools.registerTheme(theme);
     } else {
       this.themes.push(theme);
     }
   },
 
   /**
    * This method is only used by the addon-sdk and should be removed when Firefox 56 is
    * no longer supported.
    */
   unregisterTheme: function (theme) {
-    if (this.isInstalled()) {
+    if (this.isInitialized()) {
       this.gDevTools.unregisterTheme(theme);
     } else {
       removeItem(this.themes, t => t === theme);
     }
   },
 
   /**
    * Called from SessionStore.jsm in mozilla-central when saving the current state.
    *
    * @return {Array} array of currently opened scratchpad windows. Empty array if devtools
    *         are not installed
    */
   getOpenedScratchpads: function () {
     if (!this.isInstalled()) {
       return [];
     }
+
+    if (!this.isInitialized()) {
+      this._initDevTools();
+    }
+
     return this.gDevTools.getOpenedScratchpads();
   },
 
   /**
    * Called from SessionStore.jsm in mozilla-central when restoring a state that contained
    * opened scratchpad windows.
    */
   restoreScratchpadSession: function (scratchpads) {
     if (!this.isInstalled()) {
       return;
     }
+
+    if (!this.isInitialized()) {
+      this._initDevTools();
+    }
+
     this.gDevTools.restoreScratchpadSession(scratchpads);
   },
 
   /**
    * Called from nsContextMenu.js in mozilla-central when using the Inspect Element
    * context menu item.
    *
    * @param {XULTab} tab
@@ -189,19 +214,29 @@ this.DevToolsShim = {
    *        document.
    * @return {Promise} a promise that resolves when the node is selected in the inspector
    *         markup view or that resolves immediately if DevTools are not installed.
    */
   inspectNode: function (tab, selectors) {
     if (!this.isInstalled()) {
       return Promise.resolve();
     }
+
+    if (!this.isInitialized()) {
+      this._initDevTools();
+    }
+
     return this.gDevTools.inspectNode(tab, selectors);
   },
 
+  _initDevTools: function () {
+    let { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+    loader.require("devtools/client/framework/devtools-browser");
+  },
+
   _onDevToolsRegistered: function () {
     // Register all pending event listeners on the real gDevTools object.
     for (let [event, listener] of this.listeners) {
       this.gDevTools.on(event, listener);
     }
 
     for (let tool of this.tools) {
       this.gDevTools.registerTool(tool);
@@ -245,11 +280,15 @@ let webExtensionsMethods = [
 ];
 
 for (let method of [...addonSdkMethods, ...webExtensionsMethods]) {
   this.DevToolsShim[method] = function () {
     if (!this.isInstalled()) {
       throw new Error(`Method ${method} unavailable if DevTools are not installed`);
     }
 
+    if (!this.isInitialized()) {
+      this._initDevTools();
+    }
+
     return this.gDevTools[method].apply(this.gDevTools, arguments);
   };
 }
--- a/devtools/shim/tests/unit/test_devtools_shim.js
+++ b/devtools/shim/tests/unit/test_devtools_shim.js
@@ -1,17 +1,20 @@
 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const { DevToolsShim } =
+const { DevToolsShim: realDevToolsShim } =
     Components.utils.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
 
+// Create a copy of the DevToolsShim for the test.
+const DevToolsShim = Object.assign({}, realDevToolsShim);
+
 // Test the DevToolsShim
 
 /**
  * Create a mocked version of DevTools that records all calls made to methods expected
  * to be called by DevToolsShim.
  */
 function createMockDevTools() {
   let methods = [
@@ -36,16 +39,24 @@ function createMockDevTools() {
       mock.callLog[method].push(args);
     };
     mock.callLog[method] = [];
   }
 
   return mock;
 }
 
+function mockDevToolsInstalled() {
+  DevToolsShim.isInstalled = () => true;
+}
+
+function mockDevToolsUninstalled() {
+  DevToolsShim.isInstalled = () => false;
+}
+
 /**
  * Check if a given method was called an expected number of times, and finally check the
  * arguments provided to the last call, if appropriate.
  */
 function checkCalls(mock, method, length, lastArgs) {
   ok(mock.callLog[method].length === length,
       "Devtools.on was called the expected number of times");
 
@@ -57,72 +68,72 @@ function checkCalls(mock, method, length
   for (let i = 0; i < lastArgs.length; i++) {
     let expectedArg = lastArgs[i];
     ok(mock.callLog[method][length - 1][i] === expectedArg,
         `Devtools.${method} was called with the expected argument (index ${i})`);
   }
 }
 
 function test_register_unregister() {
-  ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
   DevToolsShim.register(createMockDevTools());
-  ok(DevToolsShim.isInstalled(), "DevTools are installed");
+  ok(DevToolsShim.isInitialized(), "DevTools are installed");
 
   DevToolsShim.unregister();
-  ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 }
 
 function test_on_is_forwarded_to_devtools() {
-  ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
   function cb1() {}
   function cb2() {}
   let mock = createMockDevTools();
 
   DevToolsShim.on("test_event", cb1);
   DevToolsShim.register(mock);
   checkCalls(mock, "on", 1, ["test_event", cb1]);
 
   DevToolsShim.on("other_event", cb2);
   checkCalls(mock, "on", 2, ["other_event", cb2]);
 }
 
 function test_off_called_before_registering_devtools() {
-  ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
   function cb1() {}
   let mock = createMockDevTools();
 
   DevToolsShim.on("test_event", cb1);
   DevToolsShim.off("test_event", cb1);
 
   DevToolsShim.register(mock);
   checkCalls(mock, "on", 0);
 }
 
 function test_off_called_before_with_bad_callback() {
-  ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
   function cb1() {}
   function cb2() {}
   let mock = createMockDevTools();
 
   DevToolsShim.on("test_event", cb1);
   DevToolsShim.off("test_event", cb2);
 
   DevToolsShim.register(mock);
   // on should still be called
   checkCalls(mock, "on", 1, ["test_event", cb1]);
   // Calls to off should not be held and forwarded.
   checkCalls(mock, "off", 0);
 }
 
 function test_registering_tool() {
-  ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
   let tool1 = {};
   let tool2 = {};
   let tool3 = {};
   let mock = createMockDevTools();
 
   // Pre-register tool1
   DevToolsShim.registerTool(tool1);
@@ -141,17 +152,17 @@ function test_registering_tool() {
 
   // Create a new mock and check the tools are not added once again.
   mock = createMockDevTools();
   DevToolsShim.register(mock);
   checkCalls(mock, "registerTool", 0);
 }
 
 function test_registering_theme() {
-  ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
   let theme1 = {};
   let theme2 = {};
   let theme3 = {};
   let mock = createMockDevTools();
 
   // Pre-register theme1
   DevToolsShim.registerTheme(theme1);
@@ -170,46 +181,51 @@ function test_registering_theme() {
 
   // Create a new mock and check the themes are not added once again.
   mock = createMockDevTools();
   DevToolsShim.register(mock);
   checkCalls(mock, "registerTheme", 0);
 }
 
 function test_events() {
-  ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
 
   let mock = createMockDevTools();
   // Check emit was not called.
   checkCalls(mock, "emit", 0);
 
   // Check emit is called once with the devtools-registered event.
   DevToolsShim.register(mock);
   checkCalls(mock, "emit", 1, ["devtools-registered"]);
 
   // Check emit is called once with the devtools-unregistered event.
   DevToolsShim.unregister();
   checkCalls(mock, "emit", 2, ["devtools-unregistered"]);
 }
 
 function test_scratchpad_apis() {
+  mockDevToolsUninstalled();
+
   ok(!DevToolsShim.isInstalled(), "DevTools are not installed");
 
   // Check that restoreScratchpadSession doesn't crash.
   DevToolsShim.restoreScratchpadSession([{}]);
 
   let scratchpads = DevToolsShim.getOpenedScratchpads();
   equal(scratchpads.length, 0,
       "getOpenedScratchpads returns [] when DevTools are not installed");
 
   let mock = createMockDevTools();
-  DevToolsShim.register(mock);
 
-  // Check that calls to restoreScratchpadSession are not held.
-  checkCalls(mock, "restoreScratchpadSession", 0);
+  mockDevToolsInstalled();
+  DevToolsShim._initDevTools = () => {
+    // Next call to getOpenedScratchpags is expected to initialize DevTools, which we
+    // simulate here by registering our mock.
+    DevToolsShim.register(mock);
+  };
 
   DevToolsShim.getOpenedScratchpads();
   checkCalls(mock, "getOpenedScratchpads", 1, []);
 
   let scratchpadSessions = [{}];
   DevToolsShim.restoreScratchpadSession(scratchpadSessions);
   checkCalls(mock, "restoreScratchpadSession", 1, [scratchpadSessions]);
 }