Bug 1394750 - Keep track of the active and enabled devtools webextensions. draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 22 Jan 2018 22:42:35 +0100
changeset 777442 539daaa87ccdb462c8f890aa4b4298b96e13c84c
parent 777441 0be70e2bae6b0822e3a393debf839fc2f79757ef
child 777443 9909d18ce77657e56d338c0e6b06adac9e5e6a6c
child 777856 51a35f9be1db50fb79e9f3adad47fdade653ab03
child 777870 9ce2da4e3505b8ca037205547b9003b9e08fdf5e
child 777881 6908703230614a0a91a53ab971eeac746463a8ce
push id105206
push userluca.greco@alcacoop.it
push dateWed, 04 Apr 2018 19:06:55 +0000
bugs1394750
milestone61.0a1
Bug 1394750 - Keep track of the active and enabled devtools webextensions. MozReview-Commit-ID: 4KUQCls8CPe
devtools/client/framework/devtools.js
devtools/client/framework/test/browser_toolbox_options.js
devtools/client/framework/toolbox-options.js
devtools/client/framework/toolbox.js
devtools/startup/DevToolsShim.jsm
--- a/devtools/client/framework/devtools.js
+++ b/devtools/client/framework/devtools.js
@@ -692,16 +692,26 @@ DevTools.prototype = {
     }
 
     // Cleaning down the toolboxes: i.e.
     //   for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
     // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
   },
 
   /**
+   * Returns the array of the existing toolboxes.
+   *
+   * @return {Array<Toolbox>}
+   *   An array of toolboxes.
+   */
+  getToolboxes() {
+    return Array.from(this._toolboxes.values());
+  },
+
+  /**
    * Iterator that yields each of the toolboxes.
    */
   * [Symbol.iterator ]() {
     for (let toolbox of this._toolboxes) {
       yield toolbox;
     }
   }
 };
--- a/devtools/client/framework/test/browser_toolbox_options.js
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -9,29 +9,36 @@
 // Tests that changing preferences in the options panel updates the prefs
 // and toggles appropriate things in the toolbox.
 
 var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
 const {PrefObserver} = require("devtools/client/shared/prefs");
 
-add_task(async function () {
+add_task(async function() {
   const URL = "data:text/html;charset=utf8,test for dynamically registering " +
               "and unregistering tools";
   registerNewTool();
   let tab = await addTab(URL);
   let target = TargetFactory.forTab(tab);
   toolbox = await gDevTools.showToolbox(target);
   doc = toolbox.doc;
   await registerNewPerToolboxTool();
   await testSelectTool();
   await testOptionsShortcut();
   await testOptions();
   await testToggleTools();
+
+  // Test that registered WebExtensions becomes entries in the
+  // options panel and toggling their checkbox toggle the related
+  // preference.
+  await registerNewWebExtensions();
+  await testToggleWebExtensions();
+
   await cleanup();
 });
 
 function registerNewTool() {
   let toolDefinition = {
     id: "test-tool",
     isTargetSupported: () => true,
     visibilityswitch: "devtools.test-tool.enabled",
@@ -43,16 +50,32 @@ function registerNewTool() {
   ok(!gDevTools.getToolDefinitionMap().has("test-tool"),
     "The tool is not registered");
 
   gDevTools.registerTool(toolDefinition);
   ok(gDevTools.getToolDefinitionMap().has("test-tool"),
     "The tool is registered");
 }
 
+// Register a fake WebExtension to check that it is
+// listed in the toolbox options.
+function registerNewWebExtensions() {
+  // Register some fake extensions and init the related preferences
+  // (similarly to ext-devtools.js).
+  for (let i = 0; i < 2; i++) {
+    const extPref = `devtools.webextensions.fakeExtId${i}.enabled`;
+    Services.prefs.setBoolPref(extPref, true);
+
+    toolbox.registerWebExtension(`fakeUUID${i}`, {
+      name: `Fake WebExtension ${i}`,
+      pref: extPref,
+    });
+  }
+}
+
 function registerNewPerToolboxTool() {
   let toolDefinition = {
     id: "test-pertoolbox-tool",
     isTargetSupported: () => true,
     visibilityswitch: "devtools.test-pertoolbox-tool.enabled",
     url: "about:blank",
     label: "perToolboxSomeLabel"
   };
@@ -100,18 +123,17 @@ async function testOptionsShortcut() {
   is(toolbox.currentToolId, "webconsole", "webconsole is reselected (2)");
   synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
   is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
 }
 
 async function testOptions() {
   let tool = toolbox.getPanel("options");
   panelWin = tool.panelWin;
-  let prefNodes = tool.panelDoc.querySelectorAll(
-    "input[type=checkbox][data-pref]");
+  let prefNodes = tool.panelDoc.querySelectorAll("input[type=checkbox][data-pref]");
 
   // Store modified pref names so that they can be cleared on error.
   for (let node of tool.panelDoc.querySelectorAll("[data-pref]")) {
     let pref = node.getAttribute("data-pref");
     modifiedPrefs.push(pref);
   }
 
   for (let node of prefNodes) {
@@ -176,57 +198,183 @@ async function testMouseClick(node, pref
     is(GetPref(pref), !prefValue, "New value is correct for " + pref);
     deferred.resolve();
   });
 
   node.scrollIntoView();
 
   // We use executeSoon here to ensure that the element is in view and
   // clickable.
-  executeSoon(function () {
+  executeSoon(function() {
     info("Click event synthesized for pref " + pref);
     EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
   });
 
   await deferred.promise;
 
   ok(changeSeen, "Correct pref was changed");
   observer.destroy();
 }
 
+async function testToggleWebExtensions() {
+  const disabledExtensions = new Set();
+  let toggleableWebExtensions = toolbox.listWebExtensions();
+
+  function toggleWebExtension(node) {
+    node.scrollIntoView();
+    EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
+  }
+
+  function assertExpectedDisabledExtensions() {
+    for (let ext of toggleableWebExtensions) {
+      if (disabledExtensions.has(ext)) {
+        ok(!toolbox.isWebExtensionEnabled(ext.uuid),
+           `The WebExtension "${ext.name}" should be disabled`);
+      } else {
+        ok(toolbox.isWebExtensionEnabled(ext.uuid),
+           `The WebExtension "${ext.name}" should  be enabled`);
+      }
+    }
+  }
+
+  function assertAllExtensionsDisabled() {
+    const enabledUUIDs = toggleableWebExtensions
+            .filter(ext => toolbox.isWebExtensionEnabled(ext.uuid))
+            .map(ext => ext.uuid);
+
+    Assert.deepEqual(enabledUUIDs, [],
+                     "All the registered WebExtensions should be disabled");
+  }
+
+  function assertAllExtensionsEnabled() {
+    const disabledUUIDs = toolbox.listWebExtensions()
+            .filter(ext => !toolbox.isWebExtensionEnabled(ext.uuid))
+            .map(ext => ext.uuid);
+
+    Assert.deepEqual(disabledUUIDs, [],
+                     "All the registered WebExtensions should be enabled");
+  }
+
+  function getWebExtensionNodes() {
+    let toolNodes = panelWin.document.querySelectorAll(
+      "#default-tools-box input[type=checkbox]:not([data-unsupported])," +
+        "#additional-tools-box input[type=checkbox]:not([data-unsupported])");
+
+    return [...toolNodes].filter(node => {
+      return toggleableWebExtensions.some(
+        ({uuid}) => node.getAttribute("id") === `webext-${uuid}`
+      );
+    });
+  }
+
+  let webExtensionNodes = getWebExtensionNodes();
+
+  is(webExtensionNodes.length, toggleableWebExtensions.length,
+     "There should be a toggle checkbox for every WebExtension registered");
+
+  for (let ext of toggleableWebExtensions) {
+    ok(toolbox.isWebExtensionEnabled(ext.uuid),
+       `The WebExtension "${ext.name}" is initially enabled`);
+  }
+
+  // Store modified pref names so that they can be cleared on error.
+  for (let ext of toggleableWebExtensions) {
+    modifiedPrefs.push(ext.pref);
+  }
+
+  // Turn each registered WebExtension to disabled.
+  for (let node of webExtensionNodes) {
+    toggleWebExtension(node);
+
+    const toggledExt = toggleableWebExtensions.find(ext => {
+      return node.id == `webext-${ext.uuid}`;
+    });
+    ok(toggledExt, "Found a WebExtension for the checkbox element");
+    disabledExtensions.add(toggledExt);
+
+    assertExpectedDisabledExtensions();
+  }
+
+  assertAllExtensionsDisabled();
+
+  // Turn each registered WebExtension to enabled.
+  for (let node of webExtensionNodes) {
+    toggleWebExtension(node);
+
+    const toggledExt = toggleableWebExtensions.find(ext => {
+      return node.id == `webext-${ext.uuid}`;
+    });
+    ok(toggledExt, "Found a WebExtension for the checkbox element");
+    disabledExtensions.delete(toggledExt);
+
+    assertExpectedDisabledExtensions();
+  }
+
+  assertAllExtensionsEnabled();
+
+  // Unregister the WebExtensions one by one, and check that only the expected
+  // ones have been unregistered, and the remaining onea are still listed.
+  for (let ext of toggleableWebExtensions) {
+    ok(toolbox.listWebExtensions().length > 0,
+       "There should still be extensions registered");
+    toolbox.unregisterWebExtension(ext.uuid);
+
+    const registeredUUIDs = toolbox.listWebExtensions().map(item => item.uuid);
+    ok(!registeredUUIDs.includes(ext.uuid),
+       `the WebExtension "${ext.name}" should have been unregistered`);
+
+    webExtensionNodes = getWebExtensionNodes();
+
+    const checkboxEl = webExtensionNodes.find(el => el.id === `webext-${ext.uuid}`);
+    is(checkboxEl, undefined,
+       "The unregistered WebExtension checkbox should have been removed");
+
+    is(registeredUUIDs.length, webExtensionNodes.length,
+       "There should be the expected number of WebExtensions checkboxes");
+  }
+
+  is(toolbox.listWebExtensions().length, 0,
+     "All WebExtensions have been unregistered");
+
+  webExtensionNodes = getWebExtensionNodes();
+
+  is(webExtensionNodes.length, 0,
+     "There should not be any checkbox for the unregistered WebExtensions");
+}
+
 async function testToggleTools() {
   let toolNodes = panelWin.document.querySelectorAll(
     "#default-tools-box input[type=checkbox]:not([data-unsupported])," +
     "#additional-tools-box input[type=checkbox]:not([data-unsupported])");
   let enabledTools = [...toolNodes].filter(node => node.checked);
 
   let toggleableTools = gDevTools.getDefaultTools()
                                  .filter(tool => {
                                    return tool.visibilityswitch;
                                  })
                                  .concat(gDevTools.getAdditionalTools())
                                  .concat(toolbox.getAdditionalTools());
 
-
   for (let node of toolNodes) {
     let id = node.getAttribute("id");
     ok(toggleableTools.some(tool => tool.id === id),
-      "There should be a toggle checkbox for: " + id);
+       "There should be a toggle checkbox for: " + id);
   }
 
   // Store modified pref names so that they can be cleared on error.
   for (let tool of toggleableTools) {
     let pref = tool.visibilityswitch;
     modifiedPrefs.push(pref);
   }
 
   // Toggle each tool
   for (let node of toolNodes) {
     await toggleTool(node);
   }
+
   // Toggle again to reset tool enablement state
   for (let node of toolNodes) {
     await toggleTool(node);
   }
 
   // Test that a tool can still be added when no tabs are present:
   // Disable all tools
   for (let node of enabledTools) {
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -59,16 +59,18 @@ function OptionsPanel(iframeWindow, tool
   this.panelWin = iframeWindow;
 
   this.toolbox = toolbox;
   this.isReady = false;
 
   this._prefChanged = this._prefChanged.bind(this);
   this._themeRegistered = this._themeRegistered.bind(this);
   this._themeUnregistered = this._themeUnregistered.bind(this);
+  this._webExtensionRegistered = this._webExtensionRegistered.bind(this);
+  this._webExtensionUnregistered = this._webExtensionUnregistered.bind(this);
   this._disableJSClicked = this._disableJSClicked.bind(this);
 
   this.disableJSNode = this.panelDoc.getElementById("devtools-disable-javascript");
 
   this._addListeners();
 
   const EventEmitter = require("devtools/shared/event-emitter");
   EventEmitter.decorate(this);
@@ -98,23 +100,30 @@ OptionsPanel.prototype = {
 
   _addListeners: function() {
     Services.prefs.addObserver("devtools.cache.disabled", this._prefChanged);
     Services.prefs.addObserver("devtools.theme", this._prefChanged);
     Services.prefs.addObserver("devtools.source-map.client-service.enabled",
                                this._prefChanged);
     gDevTools.on("theme-registered", this._themeRegistered);
     gDevTools.on("theme-unregistered", this._themeUnregistered);
+
+    this.toolbox.on("webextension-registered", this._webExtensionRegistered);
+    this.toolbox.on("webextension-unregistered", this._webExtensionUnregistered);
   },
 
   _removeListeners: function() {
     Services.prefs.removeObserver("devtools.cache.disabled", this._prefChanged);
     Services.prefs.removeObserver("devtools.theme", this._prefChanged);
     Services.prefs.removeObserver("devtools.source-map.client-service.enabled",
                                   this._prefChanged);
+
+    this.toolbox.off("webextension-registered", this._webExtensionRegistered);
+    this.toolbox.off("webextension-unregistered", this._webExtensionUnregistered);
+
     gDevTools.off("theme-registered", this._themeRegistered);
     gDevTools.off("theme-unregistered", this._themeUnregistered);
   },
 
   _prefChanged: function(subject, topic, prefName) {
     if (prefName === "devtools.cache.disabled") {
       let cacheDisabled = GetPref(prefName);
       let cbx = this.panelDoc.getElementById("devtools-disable-cache");
@@ -134,16 +143,28 @@ OptionsPanel.prototype = {
     let themeBox = this.panelDoc.getElementById("devtools-theme-box");
     let themeInput = themeBox.querySelector(`[value=${theme.id}]`);
 
     if (themeInput) {
       themeInput.parentNode.remove();
     }
   },
 
+  _webExtensionRegistered: function(extensionUUID) {
+    // Refresh the tools list when a new webextension has been registered
+    // to the toolbox.
+    this.setupToolsList();
+  },
+
+  _webExtensionUnregistered: function(extensionUUID) {
+    // Refresh the tools list when a new webextension has been unregistered
+    // from the toolbox.
+    this.setupToolsList();
+  },
+
   async setupToolbarButtonsList() {
     // Ensure the toolbox is open, and the buttons are all set up.
     await this.toolbox.isOpen;
 
     let enabledToolbarButtonsBox = this.panelDoc.getElementById(
       "enabled-toolbox-buttons-box");
 
     let toolbarButtons = this.toolbox.toolbarButtons;
@@ -195,24 +216,27 @@ OptionsPanel.prototype = {
     let toolsNotSupportedLabel = this.panelDoc.getElementById(
       "tools-not-supported-label");
     let atleastOneToolNotSupported = false;
 
     const toolbox = this.toolbox;
 
     // Signal tool registering/unregistering globally (for the tools registered
     // globally) and per toolbox (for the tools registered to a single toolbox).
-    let onCheckboxClick = function(id) {
-      let toolDefinition = gDevTools._tools.get(id) || toolbox.getToolDefinition(id);
+    // This event handler expect this to be binded to the related checkbox element.
+    let onCheckboxClick = function(tool) {
       // Set the kill switch pref boolean to true
-      Services.prefs.setBoolPref(toolDefinition.visibilityswitch, this.checked);
-      gDevTools.emit(this.checked ? "tool-registered" : "tool-unregistered", id);
+      Services.prefs.setBoolPref(tool.visibilityswitch, this.checked);
+
+      if (!tool.isWebExtension) {
+        gDevTools.emit(this.checked ? "tool-registered" : "tool-unregistered", tool.id);
+      }
     };
 
-    let createToolCheckbox = tool => {
+    let createToolCheckbox = (tool) => {
       let checkboxLabel = this.panelDoc.createElement("label");
       let checkboxInput = this.panelDoc.createElement("input");
       checkboxInput.setAttribute("type", "checkbox");
       checkboxInput.setAttribute("id", tool.id);
       checkboxInput.setAttribute("title", tool.tooltip || "");
 
       let checkboxSpanLabel = this.panelDoc.createElement("span");
       if (tool.isTargetSupported(this.target)) {
@@ -224,53 +248,83 @@ OptionsPanel.prototype = {
         checkboxInput.setAttribute("data-unsupported", "true");
         checkboxInput.setAttribute("disabled", "true");
       }
 
       if (InfallibleGetBoolPref(tool.visibilityswitch)) {
         checkboxInput.setAttribute("checked", "true");
       }
 
-      checkboxInput.addEventListener("change",
-        onCheckboxClick.bind(checkboxInput, tool.id));
+      checkboxInput.addEventListener("change", onCheckboxClick.bind(checkboxInput, tool));
 
       checkboxLabel.appendChild(checkboxInput);
       checkboxLabel.appendChild(checkboxSpanLabel);
       return checkboxLabel;
     };
 
+    // Clean up any existent default tools content.
+    for (let label of defaultToolsBox.querySelectorAll("label")) {
+      label.remove();
+    }
+
     // Populating the default tools lists
     let toggleableTools = gDevTools.getDefaultTools().filter(tool => {
       return tool.visibilityswitch && !tool.hiddenInOptions;
     });
 
     for (let tool of toggleableTools) {
       defaultToolsBox.appendChild(createToolCheckbox(tool));
     }
 
-    // Populating the additional tools list that came from add-ons.
+    // Clean up any existent additional tools content.
+    for (let label of additionalToolsBox.querySelectorAll("label")) {
+      label.remove();
+    }
+
+    // Populating the additional tools list.
     let atleastOneAddon = false;
     for (let tool of gDevTools.getAdditionalTools()) {
       atleastOneAddon = true;
       additionalToolsBox.appendChild(createToolCheckbox(tool));
     }
 
-    // Populating the additional toolbox-specific tools list that came
-    // from WebExtension add-ons.
-    for (let tool of this.toolbox.getAdditionalTools()) {
+    // Populating the additional tools that came from the installed WebExtension add-ons.
+    for (let {uuid, name, pref} of toolbox.listWebExtensions()) {
       atleastOneAddon = true;
-      additionalToolsBox.appendChild(createToolCheckbox(tool));
+
+      additionalToolsBox.appendChild(createToolCheckbox({
+        isWebExtension: true,
+
+        // Use the preference as the unified webextensions tool id.
+        id: `webext-${uuid}`,
+        tooltip: name,
+        label: name,
+        // Disable the devtools extension using the given pref name:
+        // the toolbox options for the WebExtensions are not related to a single
+        // tool (e.g. a devtools panel created from the extension devtools_page)
+        // but to the entire devtools part of a webextension which is enabled
+        // by the Addon Manager (but it may be disabled by its related
+        // devtools about:config preference), and so the following
+        visibilityswitch: pref,
+
+        // Only local tabs are currently supported as targets.
+        isTargetSupported: target => target.isLocalTab,
+      }));
     }
 
     if (!atleastOneAddon) {
       additionalToolsBox.style.display = "none";
+    } else {
+      additionalToolsBox.style.display = "";
     }
 
     if (!atleastOneToolNotSupported) {
       toolsNotSupportedLabel.style.display = "none";
+    } else {
+      toolsNotSupportedLabel.style.display = "";
     }
 
     this.panelWin.focus();
   },
 
   setupThemeList: function() {
     let themeBox = this.panelDoc.getElementById("devtools-theme-box");
     let themeLabels = themeBox.querySelectorAll("label");
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -99,16 +99,20 @@ loader.lazyGetter(this, "registerHarOver
  *        A unique identifier to differentiate toolbox documents from the
  *        chrome codebase when passing DOM messages
  */
 function Toolbox(target, selectedTool, hostType, contentWindow, frameId) {
   this._target = target;
   this._win = contentWindow;
   this.frameId = frameId;
 
+  // Map of the available DevTools WebExtensions:
+  //   Map<extensionUUID, extensionName>
+  this._webExtensions = new Map();
+
   this._toolPanels = new Map();
   this._inspectorExtensionSidebars = new Map();
   this._telemetry = new Telemetry();
 
   this._initInspector = null;
   this._inspector = null;
   this._styleSheets = null;
 
@@ -3073,10 +3077,67 @@ Toolbox.prototype = {
     // The panel doesn't have to exist (it must be selected
     // by the user at least once to be created).
     // Return undefined content in such case.
     if (!netPanel) {
       return Promise.resolve({content: {}});
     }
 
     return netPanel.panelWin.Netmonitor.fetchResponseContent(requestId);
-  }
+  },
+
+  // Support management of installed WebExtensions that provide a devtools_page.
+
+  /**
+   * List the subset of the active WebExtensions which have a devtools_page (used by
+   * toolbox-options.js to create the list of the tools provided by the enabled
+   * WebExtensions).
+   * @see devtools/client/framework/toolbox-options.js
+   */
+  listWebExtensions: function() {
+    // Return the array of the enabled webextensions (we can't use the prefs list here,
+    // because some of them may be disabled by the Addon Manager and still have a devtools
+    // preference).
+    return Array.from(this._webExtensions).map(([uuid, {name, pref}]) => {
+      return {uuid, name, pref};
+    });
+  },
+
+  /**
+   * Add a WebExtension to the list of the active extensions (given the extension UUID,
+   * a unique id assigned to an extension when it is installed, and its name),
+   * and emit a "webextension-registered" event to allow toolbox-options.js
+   * to refresh the listed tools accordingly.
+   * @see browser/components/extensions/ext-devtools.js
+   */
+  registerWebExtension: function(extensionUUID, {name, pref}) {
+    // Ensure that an installed extension (active in the AddonManager) which
+    // provides a devtools page is going to be listed in the toolbox options
+    // (and refresh its name if it was already listed).
+    this._webExtensions.set(extensionUUID, {name, pref});
+    this.emit("webextension-registered", extensionUUID);
+  },
+
+  /**
+   * Remove an active WebExtension from the list of the active extensions (given the
+   * extension UUID, a unique id assigned to an extension when it is installed, and its
+   * name), and emit a "webextension-unregistered" event to allow toolbox-options.js
+   * to refresh the listed tools accordingly.
+   * @see browser/components/extensions/ext-devtools.js
+   */
+  unregisterWebExtension: function(extensionUUID) {
+    // Ensure that an extension that has been disabled/uninstalled from the AddonManager
+    // is going to be removed from the toolbox options.
+    this._webExtensions.delete(extensionUUID);
+    this.emit("webextension-unregistered", extensionUUID);
+  },
+
+  /**
+   * A helper function which returns true if the extension with the given UUID is listed
+   * as active for the toolbox and has its related devtools about:config preference set
+   * to true.
+   * @see browser/components/extensions/ext-devtools.js
+   */
+  isWebExtensionEnabled: function(extensionUUID) {
+    let extInfo = this._webExtensions.get(extensionUUID);
+    return extInfo && Services.prefs.getBoolPref(extInfo.pref, false);
+  },
 };
--- a/devtools/startup/DevToolsShim.jsm
+++ b/devtools/startup/DevToolsShim.jsm
@@ -211,31 +211,32 @@ this.DevToolsShim = {
   initDevTools: function(reason) {
     if (!this.isEnabled()) {
       throw new Error("DevTools are not enabled and can not be initialized.");
     }
 
     if (!this.isInitialized()) {
       DevtoolsStartup.initDevTools(reason);
     }
-  }
+  },
 };
 
 /**
  * Compatibility layer for webextensions.
  *
  * Those methods are called only after a DevTools webextension was loaded in DevTools,
  * therefore DevTools should always be available when they are called.
  */
 let webExtensionsMethods = [
   "createTargetForTab",
   "createWebExtensionInspectedWindowFront",
   "getTargetForTab",
   "getTheme",
   "openBrowserConsole",
+  "getToolboxes",
 ];
 
 for (let method of webExtensionsMethods) {
   this.DevToolsShim[method] = function() {
     if (!this.isEnabled()) {
       throw new Error("Could not call a DevToolsShim webextension method ('" + method +
         "'): DevTools are not initialized.");
     }