Bug 1394750 - Allow the webextension devtools_page to be disabled separately from the entire extension. draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 15 Jan 2018 20:56:17 +0100
changeset 777881 6908703230614a0a91a53ab971eeac746463a8ce
parent 777442 539daaa87ccdb462c8f890aa4b4298b96e13c84c
push id105317
push userluca.greco@alcacoop.it
push dateThu, 05 Apr 2018 12:58:40 +0000
bugs1394750
milestone61.0a1
Bug 1394750 - Allow the webextension devtools_page to be disabled separately from the entire extension. MozReview-Commit-ID: 6rnBYXlJPTz
browser/components/extensions/ext-browser.json
browser/components/extensions/parent/ext-devtools-panels.js
browser/components/extensions/parent/ext-devtools.js
browser/components/extensions/test/browser/browser_ext_devtools_panel.js
--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -40,16 +40,17 @@
     "paths": [
       ["commands"]
     ]
   },
   "devtools": {
     "url": "chrome://browser/content/parent/ext-devtools.js",
     "schema": "chrome://browser/content/schemas/devtools.json",
     "scopes": ["devtools_parent"],
+    "events": ["uninstall"],
     "manifest": ["devtools_page"],
     "paths": [
       ["devtools"]
     ]
   },
   "devtools_inspectedWindow": {
     "url": "chrome://browser/content/parent/ext-devtools-inspectedWindow.js",
     "schema": "chrome://browser/content/schemas/devtools_inspected_window.json",
--- a/browser/components/extensions/parent/ext-devtools-panels.js
+++ b/browser/components/extensions/parent/ext-devtools-panels.js
@@ -79,17 +79,16 @@ class ParentDevToolsPanel {
     const extensionName = this.context.extension.name;
 
     this.toolbox.addAdditionalTool({
       id: this.id,
       url: "chrome://browser/content/webext-panels.xul",
       icon: icon,
       label: title,
       tooltip: `DevTools Panel added by "${extensionName}" add-on.`,
-      visibilityswitch:  `devtools.webext-${this.id}.enabled`,
       isTargetSupported: target => target.isLocalTab,
       build: (window, toolbox) => {
         if (toolbox !== this.toolbox) {
           throw new Error("Unexpected toolbox received on addAdditionalTool build property");
         }
 
         const destroy = this.buildPanel(window);
 
@@ -592,17 +591,17 @@ this.devtools_panels = class extends Ext
               const iconInfo = IconDetails.getPreferredIcon(context.extension.manifest.icons,
                                                             context.extension, 128);
               icon = iconInfo ? iconInfo.icon : "";
             }
 
             icon = context.extension.baseURI.resolve(icon);
             url = context.extension.baseURI.resolve(url);
 
-            const id = `${makeWidgetId(newBasePanelId())}-devtools-panel`;
+            const id = `webext-devtools-panel-${makeWidgetId(newBasePanelId())}`;
 
             new ParentDevToolsPanel(context, {title, icon, url, id});
 
             // Resolved to the devtools panel id into the child addon process,
             // where it will be used to identify the messages related
             // to the panel API onShown/onHidden events.
             return Promise.resolve(id);
           },
--- a/browser/components/extensions/parent/ext-devtools.js
+++ b/browser/components/extensions/parent/ext-devtools.js
@@ -283,16 +283,20 @@ class DevToolsPageDefinition {
     if (this.devtoolsPageForTarget.size > 0) {
       throw new Error(
         `Leaked ${this.devtoolsPageForTarget.size} DevToolsPage instances in devtoolsPageForTarget Map`
       );
     }
   }
 }
 
+// Get the devtools preference given the extension id.
+function getDevToolsPrefBranchName(extensionId) {
+  return `devtools.webextensions.${extensionId}`;
+}
 
 let devToolsInitialized = false;
 initDevTools = function() {
   if (devToolsInitialized) {
     return;
   }
 
   /* eslint-disable mozilla/balanced-listeners */
@@ -307,17 +311,29 @@ initDevTools = function() {
                 "Only local tab are currently supported by the WebExtensions DevTools API.";
       let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
       scriptError.init(msg, null, null, null, null, Ci.nsIScriptError.warningFlag, "content javascript");
       Services.console.logMessage(scriptError);
 
       return;
     }
 
-    for (let devtoolsPage of devtoolsPageDefinitionMap.values()) {
+    for (let [extension, devtoolsPage] of devtoolsPageDefinitionMap) {
+      // Ensure that the WebExtension is listed in the toolbox options.
+      toolbox.registerWebExtension(extension.uuid, {
+        name: extension.name,
+        pref: `${getDevToolsPrefBranchName(extension.id)}.enabled`,
+      });
+
+      // Do not build the devtools page if the extension has been disabled
+      // (e.g. based on the devtools preference).
+      if (!toolbox.isWebExtensionEnabled(extension.uuid)) {
+        continue;
+      }
+
       devtoolsPage.buildForToolbox(toolbox);
     }
   });
 
   // Destroy a devtools page context for a destroyed toolbox,
   // based on the registered devtools_page definitions.
   DevToolsShim.on("toolbox-destroy", target => {
     if (!target.isLocalTab) {
@@ -332,33 +348,169 @@ initDevTools = function() {
   });
   /* eslint-enable mozilla/balanced-listeners */
 
   devToolsInitialized = true;
 };
 
 this.devtools = class extends ExtensionAPI {
   onManifestEntry(entryName) {
-    let {extension} = this;
-    let {manifest} = extension;
-
-    // Create and register a new devtools_page definition as specified in the
-    // "devtools_page" property in the extension manifest.
-    let devtoolsPageDefinition = new DevToolsPageDefinition(extension, manifest.devtools_page);
-    devtoolsPageDefinitionMap.set(extension, devtoolsPageDefinition);
+    this.initDevToolsPref();
+    this.createDevToolsPageDefinition();
   }
 
   onShutdown(reason) {
-    let {extension} = this;
+    this.destroyDevToolsPageDefinition();
+    this.uninitDevToolsPref();
+  }
 
-    // Destroy the registered devtools_page definition on extension shutdown.
-    if (devtoolsPageDefinitionMap.has(extension)) {
-      devtoolsPageDefinitionMap.get(extension).shutdown();
-      devtoolsPageDefinitionMap.delete(extension);
-    }
+  static onUninstall(extensionId) {
+    // Remove the preference branch on uninstall.
+    const prefBranch = Services.prefs.getBranch(
+      `${getDevToolsPrefBranchName(extensionId)}.`);
+
+    prefBranch.deleteBranch("");
   }
 
   getAPI(context) {
     return {
       devtools: {},
     };
   }
+
+  /**
+   * Initialize the DevTools preferences branch for the extension and
+   * start to observe it for changes on the "enabled" preference.
+   */
+  initDevToolsPref() {
+    const prefBranch = Services.prefs.getBranch(
+      `${getDevToolsPrefBranchName(this.extension.id)}.`);
+
+    // Initialize the devtools extension preference if it doesn't exist yet.
+    if (prefBranch.getPrefType("enabled") === prefBranch.PREF_INVALID) {
+      prefBranch.setBoolPref("enabled", true);
+    }
+
+    this.devtoolsPrefBranch = prefBranch;
+    this.devtoolsPrefBranch.addObserver("enabled", this);
+  }
+
+  /**
+   * Stop from observing the DevTools preferences branch for the extension.
+   */
+  uninitDevToolsPref() {
+    this.devtoolsPrefBranch.removeObserver("enabled", this);
+    this.devtoolsPrefBranch = null;
+  }
+
+  /**
+   * Test if the extension's devtools_page has been disabled using the
+   * DevTools preference.
+   *
+   * @returns {boolean}
+   *          true if the devtools_page for this extension is disabled.
+   */
+  isDevToolsPageDisabled() {
+    return !this.devtoolsPrefBranch.getBoolPref("enabled", false);
+  }
+
+  /**
+   * Observes the changed preferences on the DevTools preferences branch
+   * related to the extension.
+   *
+   * @param {nsIPrefBranch} subject  The observed preferences branch.
+   * @param {string}        topic    The notified topic.
+   * @param {string}        prefName The changed preference name.
+   */
+  observe(subject, topic, prefName) {
+    // We are currently interested only in the "enabled" preference from the
+    // WebExtension devtools preferences branch.
+    if (subject !== this.devtoolsPrefBranch || prefName !== "enabled") {
+      return;
+    }
+
+    if (this.isDevToolsPageDisabled()) {
+      this.shutdownDevToolsPages();
+    } else {
+      this.buildDevToolsPages();
+    }
+  }
+
+  /**
+   * Create the devtools_page definition for the extension and build the devtools_page
+   * for any existing toolbox that is supported as a target (currentl only toolbox with
+   * a local tab as a target).
+   */
+  createDevToolsPageDefinition() {
+    let {extension} = this;
+    let {manifest} = extension;
+
+    if (devtoolsPageDefinitionMap.has(extension)) {
+      throw new Error("Cannot create an extension devtools page multiple times");
+    }
+
+    // Create and register a new devtools_page definition as specified in the
+    // "devtools_page" property in the extension manifest.
+    let devtoolsPageDefinition = new DevToolsPageDefinition(extension, manifest.devtools_page);
+    devtoolsPageDefinitionMap.set(extension, devtoolsPageDefinition);
+
+    this.buildDevToolsPages();
+  }
+
+  /**
+   * Destroy the devtools_page definition for the extension, shutdown any built
+   * devtools_page from all the existing toolbox and ensure that the extension is unlisted
+   * from the toolbox options panel if the extension is being disabled or uninstalled.
+   *
+   */
+  destroyDevToolsPageDefinition() {
+    this.shutdownDevToolsPages();
+
+    // Destroy the registered devtools_page definition on extension shutdown.
+    devtoolsPageDefinitionMap.delete(this.extension);
+
+    // Iterate over the existing toolboxes and unlist the devtools webextension from them.
+    for (let toolbox of DevToolsShim.getToolboxes()) {
+      toolbox.unregisterWebExtension(this.extension.uuid);
+    }
+  }
+
+  /**
+   * Build the devtools_page instances for the existing toolboxes (if its definition has been
+   * created and the toolbox target is supported)/
+   */
+  buildDevToolsPages() {
+    const devtoolsPageDefinition = devtoolsPageDefinitionMap.get(this.extension);
+    if (!devtoolsPageDefinition) {
+      return;
+    }
+
+    // Iterate over the existing toolboxes and create the devtools page for them
+    // (if the toolbox target is supported).
+    for (let toolbox of DevToolsShim.getToolboxes()) {
+      if (!toolbox.target.isLocalTab) {
+        // Skip any non-local tab.
+        continue;
+      }
+
+      // Ensure that the WebExtension is listed in the toolbox options.
+      toolbox.registerWebExtension(this.extension.uuid, {
+        name: this.extension.name,
+        pref: `${getDevToolsPrefBranchName(this.extension.id)}.enabled`,
+      });
+
+      devtoolsPageDefinition.buildForToolbox(toolbox);
+    }
+  }
+
+  /**
+   * Shutdown any existing devtools_page instances from the existing toolboxes
+   * (without destroying its definition, so that the devtools_page can be rebuilt
+   * when it is re-enabled by toggling the related DevTools preference).
+   */
+  shutdownDevToolsPages() {
+    const devtoolsPageDefinition = devtoolsPageDefinitionMap.get(this.extension);
+
+    if (devtoolsPageDefinition) {
+      devtoolsPageDefinition.shutdown();
+    }
+  }
 };
--- a/browser/components/extensions/test/browser/browser_ext_devtools_panel.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_panel.js
@@ -15,34 +15,41 @@ const DEVTOOLS_THEME_PREF = "devtools.th
  *
  * - devtools.panels.themeName returns the correct value,
  *   both from a page and a panel.
  * - devtools.panels.onThemeChanged fires for theme changes,
  *   both from a page and a panel.
  * - devtools.panels.create is able to create a devtools panel.
  */
 
-function switchTheme(theme) {
-  const waitforThemeChanged = gDevTools.once("theme-changed");
-  Preferences.set(DEVTOOLS_THEME_PREF, theme);
-  return waitforThemeChanged;
+async function openToolboxForTab(tab) {
+  const target = gDevTools.getTargetForTab(tab);
+  const toolbox = await gDevTools.showToolbox(target, "testBlankPanel");
+  info("Developer toolbox opened");
+  return {toolbox, target};
 }
 
-async function testThemeSwitching(extension, locations = ["page"]) {
-  for (let newTheme of ["dark", "light"]) {
-    await switchTheme(newTheme);
-    for (let location of locations) {
-      is(await extension.awaitMessage(`devtools_theme_changed_${location}`),
-         newTheme,
-         `The onThemeChanged event listener fired for the ${location}.`);
-      is(await extension.awaitMessage(`current_theme_${location}`),
-         newTheme,
-         `The current theme is reported as expected for the ${location}.`);
-    }
-  }
+async function closeToolboxForTab(tab) {
+  const target = gDevTools.getTargetForTab(tab);
+  await gDevTools.closeToolbox(target);
+  await target.destroy();
+  info("Developer toolbox closed");
+}
+
+function createPage(jsScript, bodyText = "") {
+  return `<!DOCTYPE html>
+    <html>
+       <head>
+         <meta charset="utf-8">
+       </head>
+       <body>
+         ${bodyText}
+         <script src="${jsScript}"></script>
+       </body>
+    </html>`;
 }
 
 add_task(async function setup_blank_panel() {
   // Create a blank custom tool so that we don't need to wait the webconsole
   // to be fully loaded/unloaded to prevent intermittent failures (related
   // to a webconsole that is still loading when the test has been completed).
   const testBlankPanel = {
     id: "testBlankPanel",
@@ -64,83 +71,141 @@ add_task(async function setup_blank_pane
 
   registerCleanupFunction(() => {
     gDevTools.unregisterTool(testBlankPanel.id);
   });
 
   gDevTools.registerTool(testBlankPanel);
 });
 
-add_task(async function test_theme_name_no_panel() {
+async function test_theme_name(testWithPanel = false) {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
 
-  async function devtools_page() {
+  function switchTheme(theme) {
+    const waitforThemeChanged = gDevTools.once("theme-changed");
+    Preferences.set(DEVTOOLS_THEME_PREF, theme);
+    return waitforThemeChanged;
+  }
+
+  async function testThemeSwitching(extension, locations = ["page"]) {
+    for (let newTheme of ["dark", "light"]) {
+      await switchTheme(newTheme);
+      for (let location of locations) {
+        is(await extension.awaitMessage(`devtools_theme_changed_${location}`),
+           newTheme,
+           `The onThemeChanged event listener fired for the ${location}.`);
+        is(await extension.awaitMessage(`current_theme_${location}`),
+           newTheme,
+           `The current theme is reported as expected for the ${location}.`);
+      }
+    }
+  }
+
+  async function devtools_page(createPanel) {
+    if (createPanel) {
+      browser.devtools.panels.create(
+        "Test Panel Theme", "fake-icon.png", "devtools_panel.html"
+      );
+    }
+
     browser.devtools.panels.onThemeChanged.addListener(themeName => {
       browser.test.sendMessage("devtools_theme_changed_page", themeName);
       browser.test.sendMessage("current_theme_page", browser.devtools.panels.themeName);
     });
 
-    browser.test.sendMessage("initial_theme", browser.devtools.panels.themeName);
+    browser.test.sendMessage("initial_theme_page", browser.devtools.panels.themeName);
+  }
+
+  async function devtools_panel() {
+    browser.devtools.panels.onThemeChanged.addListener(themeName => {
+      browser.test.sendMessage("devtools_theme_changed_panel", themeName);
+      browser.test.sendMessage("current_theme_panel", browser.devtools.panels.themeName);
+    });
+
+    browser.test.sendMessage("initial_theme_panel", browser.devtools.panels.themeName);
+  }
+
+  let files = {
+    "devtools_page.html": createPage("devtools_page.js"),
+    "devtools_page.js": `(${devtools_page})(${testWithPanel})`,
+  };
+
+  if (testWithPanel) {
+    files["devtools_panel.js"] = devtools_panel;
+    files["devtools_panel.html"] = createPage("devtools_panel.js", "Test Panel Theme");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       devtools_page: "devtools_page.html",
     },
-    files: {
-      "devtools_page.html": `<!DOCTYPE html>
-      <html>
-       <head>
-         <meta charset="utf-8">
-       </head>
-       <body>
-         <script src="devtools_page.js"></script>
-       </body>
-      </html>`,
-      "devtools_page.js": devtools_page,
-    },
+    files,
   });
 
   // Ensure that the initial value of the devtools theme is "light".
   await SpecialPowers.pushPrefEnv({set: [[DEVTOOLS_THEME_PREF, "light"]]});
+  registerCleanupFunction(async function() {
+    await SpecialPowers.popPrefEnv();
+  });
 
   await extension.startup();
 
-  let target = gDevTools.getTargetForTab(tab);
-  await gDevTools.showToolbox(target, "testBlankPanel");
-  info("developer toolbox opened");
+  const {toolbox, target} = await openToolboxForTab(tab);
 
-  is(await extension.awaitMessage("initial_theme"),
+  info("Waiting initial theme from devtools_page");
+  is(await extension.awaitMessage("initial_theme_page"),
      "light",
      "The initial theme is reported as expected.");
 
-  await testThemeSwitching(extension);
+  if (testWithPanel) {
+    let toolboxAdditionalTools = toolbox.getAdditionalTools();
+    is(toolboxAdditionalTools.length, 1,
+       "Got the expected number of toolbox specific panel registered.");
+
+    let panelId = toolboxAdditionalTools[0].id;
 
-  await gDevTools.closeToolbox(target);
-  await target.destroy();
+    await gDevTools.showToolbox(target, panelId);
+    is(await extension.awaitMessage("initial_theme_panel"),
+       "light",
+       "The initial theme is reported as expected from a devtools panel.");
+
+    await testThemeSwitching(extension, ["page", "panel"]);
+  } else {
+    await testThemeSwitching(extension);
+  }
+
+  await closeToolboxForTab(tab);
 
   await extension.unload();
 
   BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_devtools_page_theme() {
+  await test_theme_name(false);
+});
+
+add_task(async function test_devtools_panel_theme() {
+  await test_theme_name(true);
 });
 
 add_task(async function test_devtools_page_panels_create() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
 
   async function devtools_page() {
     const result = {
       devtoolsPageTabId: browser.devtools.inspectedWindow.tabId,
       panelCreated: 0,
       panelShown: 0,
       panelHidden: 0,
     };
 
     try {
       const panel = await browser.devtools.panels.create(
-        "Test Panel", "fake-icon.png", "devtools_panel.html"
+        "Test Panel Create", "fake-icon.png", "devtools_panel.html"
       );
 
       result.panelCreated++;
 
       panel.onShown.addListener(contentWindow => {
         result.panelShown++;
         browser.test.assertEq("complete", contentWindow.document.readyState,
                               "Got the expected 'complete' panel document readyState");
@@ -150,190 +215,225 @@ add_task(async function test_devtools_pa
       });
 
       panel.onHidden.addListener(() => {
         result.panelHidden++;
 
         browser.test.sendMessage("devtools_panel_hidden", result);
       });
 
-      browser.devtools.panels.onThemeChanged.addListener(themeName => {
-        browser.test.sendMessage("devtools_theme_changed_page", themeName);
-        browser.test.sendMessage("current_theme_page", browser.devtools.panels.themeName);
-      });
-
       browser.test.sendMessage("devtools_panel_created");
-      browser.test.sendMessage("initial_theme_page", browser.devtools.panels.themeName);
     } catch (err) {
       // Make the test able to fail fast when it is going to be a failure.
       browser.test.sendMessage("devtools_panel_created");
       throw err;
     }
   }
 
   function devtools_panel() {
     // Set a property in the global and check that it is defined
     // and accessible from the devtools_page when the panel.onShown
     // event has been received.
     window.TEST_PANEL_GLOBAL = "test_panel_global";
 
-    browser.devtools.panels.onThemeChanged.addListener(themeName => {
-      browser.test.sendMessage("devtools_theme_changed_panel", themeName);
-      browser.test.sendMessage("current_theme_panel", browser.devtools.panels.themeName);
-    });
-
     browser.test.sendMessage("devtools_panel_inspectedWindow_tabId",
                              browser.devtools.inspectedWindow.tabId);
-    browser.test.sendMessage("initial_theme_panel", browser.devtools.panels.themeName);
   }
 
+  const EXTENSION_ID = "@create-devtools-panel.test";
+
   let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary",
     manifest: {
       devtools_page: "devtools_page.html",
+      applications: {
+        gecko: {id: EXTENSION_ID},
+      },
     },
     files: {
-      "devtools_page.html": `<!DOCTYPE html>
-      <html>
-       <head>
-         <meta charset="utf-8">
-       </head>
-       <body>
-         <script src="devtools_page.js"></script>
-       </body>
-      </html>`,
+      "devtools_page.html": createPage("devtools_page.js"),
       "devtools_page.js": devtools_page,
-      "devtools_panel.html":  `<!DOCTYPE html>
-      <html>
-       <head>
-         <meta charset="utf-8">
-       </head>
-       <body>
-         DEVTOOLS PANEL
-         <script src="devtools_panel.js"></script>
-       </body>
-      </html>`,
+      "devtools_panel.html": createPage("devtools_panel.js", "Test Panel Create"),
       "devtools_panel.js": devtools_panel,
     },
   });
 
-  registerCleanupFunction(function() {
-    Preferences.reset(DEVTOOLS_THEME_PREF);
-  });
-
-  // Ensure that the initial value of the devtools theme is "light".
-  Preferences.set(DEVTOOLS_THEME_PREF, "light");
-
   await extension.startup();
 
-  let target = gDevTools.getTargetForTab(tab);
+  const extensionPrefBranch = `devtools.webextensions.${EXTENSION_ID}.`;
+  const extensionPrefName = `${extensionPrefBranch}enabled`;
 
-  const toolbox = await gDevTools.showToolbox(target, "testBlankPanel");
-  info("developer toolbox opened");
+  let prefBranch = Services.prefs.getBranch(extensionPrefBranch);
+  ok(prefBranch, "The preference branch for the extension should have been created");
+  is(prefBranch.getBoolPref("enabled", false), true,
+     "The 'enabled' bool preference for the extension should be initially true");
+
 
-  await extension.awaitMessage("devtools_panel_created");
-  is(await extension.awaitMessage("initial_theme_page"),
-     "light",
-     "The initial theme is reported as expected from a devtools page.");
+  // Get the devtools panel id from the first item in the toolbox additional tools array.
+  const getPanelId = (toolbox) => {
+    let toolboxAdditionalTools = toolbox.getAdditionalTools();
+    is(toolboxAdditionalTools.length, 1,
+       "Got the expected number of toolbox specific panel registered.");
+    return toolboxAdditionalTools[0].id;
+  };
+
+  // Test the devtools panel shown and hide events.
+  const testPanelShowAndHide = async ({
+    target, panelId, isFirstPanelLoad, expectedResults,
+  }) => {
+    info("Wait Addon Devtools Panel to be shown");
 
-  const toolboxAdditionalTools = toolbox.getAdditionalTools();
+    await gDevTools.showToolbox(target, panelId);
+    const {devtoolsPageTabId} = await extension.awaitMessage("devtools_panel_shown");
 
-  is(toolboxAdditionalTools.length, 1,
-     "Got the expected number of toolbox specific panel registered.");
+    // If the panel is loaded for the first time, we expect to also
+    // receive the test messages and assert that both the page and the panel
+    // have the same devtools.inspectedWindow.tabId value.
+    if (isFirstPanelLoad) {
+      const devtoolsPanelTabId = await extension.awaitMessage("devtools_panel_inspectedWindow_tabId");
+      is(devtoolsPanelTabId, devtoolsPageTabId,
+         "Got the same devtools.inspectedWindow.tabId from devtools page and panel");
+    }
 
-  await testThemeSwitching(extension);
+    info("Wait Addon Devtools Panel to be shown");
+
+    await gDevTools.showToolbox(target, "testBlankPanel");
+    const results = await extension.awaitMessage("devtools_panel_hidden");
 
-  const panelDef = toolboxAdditionalTools[0];
-  const panelId = panelDef.id;
+    // We already checked the tabId, remove it from the results, so that we can check
+    // the remaining properties using a single Assert.deepEqual.
+    delete results.devtoolsPageTabId;
+
+    Assert.deepEqual(
+      results, expectedResults,
+      "Got the expected number of created panels and shown/hidden events"
+    );
+  };
 
-  await gDevTools.showToolbox(target, panelId);
-  const {devtoolsPageTabId} = await extension.awaitMessage("devtools_panel_shown");
-  const devtoolsPanelTabId = await extension.awaitMessage("devtools_panel_inspectedWindow_tabId");
-  is(devtoolsPanelTabId, devtoolsPageTabId,
-     "Got the same devtools.inspectedWindow.tabId from devtools page and panel");
-  is(await extension.awaitMessage("initial_theme_panel"),
-     "light",
-     "The initial theme is reported as expected from a devtools panel.");
-  info("Addon Devtools Panel shown");
+  // Test the extension devtools_page enabling/disabling through the related
+  // about:config preference.
+  const testExtensionDevToolsPref = async ({prefValue, toolbox, oldPanelId}) => {
+    if (!prefValue) {
+      // Test that the extension devtools_page is shutting down when the related
+      // about:config preference has been set to false, and the panel on its left
+      // is being selected.
+      info("Turning off the extension devtools page from its about:config preference");
+      let waitToolSelected = toolbox.once("select");
+      Services.prefs.setBoolPref(extensionPrefName, false);
+      const selectedTool = await waitToolSelected;
+      isnot(selectedTool, oldPanelId, "Expect a different panel to be selected");
+
+      let toolboxAdditionalTools = toolbox.getAdditionalTools();
+      is(toolboxAdditionalTools.length, 0, "Extension devtools panel unregistered");
+      is(toolbox.visibleAdditionalTools.filter(toolId => toolId == oldPanelId).length, 0,
+         "Removed panel should not be listed in the visible additional tools");
+    } else {
+      // Test that the extension devtools_page and panel are being created again when
+      // the related about:config preference has been set to true.
+      info("Turning on the extension devtools page from its about:config preference");
+      Services.prefs.setBoolPref(extensionPrefName, true);
+      await extension.awaitMessage("devtools_panel_created");
+
+      let toolboxAdditionalTools = toolbox.getAdditionalTools();
+      is(toolboxAdditionalTools.length, 1, "Got one extension devtools panel registered");
 
-  await testThemeSwitching(extension, ["page", "panel"]);
+      let newPanelId = getPanelId(toolbox);
+      is(toolbox.visibleAdditionalTools.filter(toolId => toolId == newPanelId).length, 1,
+         "Extension panel is listed in the visible additional tools");
+    }
+  };
+
+  // Wait that the devtools_page has created its devtools panel and retrieve its
+  // panel id.
+  let {toolbox, target} = await openToolboxForTab(tab);
+  await extension.awaitMessage("devtools_panel_created");
+  let panelId = getPanelId(toolbox);
 
-  await gDevTools.showToolbox(target, "testBlankPanel");
-  const results = await extension.awaitMessage("devtools_panel_hidden");
-  info("Addon Devtools Panel hidden");
+  info("Test panel show and hide - first cycle");
+  await testPanelShowAndHide({
+    target, panelId,
+    isFirstPanelLoad: true,
+    expectedResults: {
+      panelCreated: 1,
+      panelShown: 1,
+      panelHidden: 1,
+    },
+  });
 
-  is(results.panelCreated, 1, "devtools.panel.create callback has been called once");
-  is(results.panelShown, 1, "panel.onShown listener has been called once");
-  is(results.panelHidden, 1, "panel.onHidden listener has been called once");
+  info("Test panel show and hide - second cycle");
+  await testPanelShowAndHide({
+    target, panelId,
+    isFirstPanelLoad: false,
+    expectedResults: {
+      panelCreated: 1,
+      panelShown: 2,
+      panelHidden: 2,
+    },
+  });
 
+  // Go back to the extension devtools panel.
   await gDevTools.showToolbox(target, panelId);
   await extension.awaitMessage("devtools_panel_shown");
-  info("Addon Devtools Panel shown - second cycle");
-
-  await gDevTools.showToolbox(target, "testBlankPanel");
-  const secondCycleResults = await extension.awaitMessage("devtools_panel_hidden");
-  info("Addon Devtools Panel hidden - second cycle");
 
-  is(secondCycleResults.panelCreated, 1, "devtools.panel.create callback has been called once");
-  is(secondCycleResults.panelShown, 2, "panel.onShown listener has been called twice");
-  is(secondCycleResults.panelHidden, 2, "panel.onHidden listener has been called twice");
+  // Turn off the extension devtools page using the preference that enable/disable the
+  // devtools page for a given installed WebExtension.
+  await testExtensionDevToolsPref({
+    toolbox,
+    prefValue: false,
+    oldPanelId: panelId,
+  });
 
-  // Turn off the addon devtools panel using the visibilityswitch.
-  const waitToolVisibilityOff = toolbox.once("tool-unregistered");
-
-  Services.prefs.setBoolPref(`devtools.webext-${panelId}.enabled`, false);
-  gDevTools.emit("tool-unregistered", panelId);
-
-  await waitToolVisibilityOff;
+  // Close and Re-open the toolbox to verify that the toolbox doesn't load the
+  // devtools_page and the devtools panel.
+  info("Re-open the toolbox and expect no extension devtools panel");
+  await closeToolboxForTab(tab);
+  ({toolbox, target} = await openToolboxForTab(tab));
 
-  ok(toolbox.hasAdditionalTool(panelId),
-     "The tool has not been removed on visibilityswitch set to false");
-
-  is(toolbox.visibleAdditionalTools.filter(toolId => toolId == panelId).length, 0,
-     "The tool is not visible on visibilityswitch set to false");
-
-  // Turn on the addon devtools panel using the visibilityswitch.
-  const waitToolVisibilityOn = toolbox.once("tool-registered");
+  let toolboxAdditionalTools = toolbox.getAdditionalTools();
+  is(toolboxAdditionalTools.length, 0,
+     "Got no extension devtools panel on the opened toolbox as expected.");
 
-  Services.prefs.setBoolPref(`devtools.webext-${panelId}.enabled`, true);
-  gDevTools.emit("tool-registered", panelId);
-
-  await waitToolVisibilityOn;
+  // Close and Re-open the toolbox to verify that the toolbox does load the
+  // devtools_page and the devtools panel again.
+  info("Restart the toolbox and enable the extension devtools panel");
+  await closeToolboxForTab(tab);
+  ({toolbox, target} = await openToolboxForTab(tab));
 
-  ok(toolbox.hasAdditionalTool(panelId),
-     "The tool has been added on visibilityswitch set to true");
-  is(toolbox.visibleAdditionalTools.filter(toolId => toolId == panelId).length, 1,
-     "The tool is visible on visibilityswitch set to true");
+  // Turn the addon devtools panel back on using the preference that enable/disable the
+  // devtools page for a given installed WebExtension.
+  await testExtensionDevToolsPref({
+    toolbox,
+    prefValue: true,
+  });
 
   // Test devtools panel is loaded correctly after being toggled and
   // devtools panel events has been fired as expected.
-  await gDevTools.showToolbox(target, panelId);
-  await extension.awaitMessage("devtools_panel_shown");
-  is(await extension.awaitMessage("initial_theme_panel"),
-     "light",
-     "The initial theme is reported as expected from a devtools panel.");
-  info("Addon Devtools Panel shown - after visibilityswitch toggled");
+  panelId = getPanelId(toolbox);
 
-  info("Wait until the Addon Devtools Panel has been loaded - after visibilityswitch toggled");
-  const panelTabIdAfterToggle = await extension.awaitMessage("devtools_panel_inspectedWindow_tabId");
-  is(panelTabIdAfterToggle, devtoolsPageTabId,
-     "Got the same devtools.inspectedWindow.tabId from devtools panel after visibility toggled");
+  info("Test panel show and hide - after disabling/enabling devtools_page");
+  await testPanelShowAndHide({
+    target, panelId,
+    isFirstPanelLoad: true,
+    expectedResults: {
+      panelCreated: 1,
+      panelShown: 1,
+      panelHidden: 1,
+    },
+  });
 
-  await gDevTools.showToolbox(target, "testBlankPanel");
-  const toolToggledResults = await extension.awaitMessage("devtools_panel_hidden");
-  info("Addon Devtools Panel hidden - after visibilityswitch toggled");
-
-  is(toolToggledResults.panelCreated, 1, "devtools.panel.create callback has been called once");
-  is(toolToggledResults.panelShown, 3, "panel.onShown listener has been called three times");
-  is(toolToggledResults.panelHidden, 3, "panel.onHidden listener has been called three times");
-
-  await gDevTools.closeToolbox(target);
-  await target.destroy();
+  await closeToolboxForTab(tab);
 
   await extension.unload();
 
+  // Verify that the extension preference branch has been removed once the extension
+  // has been uninstalled.
+  prefBranch = Services.prefs.getBranch(extensionPrefBranch);
+  is(prefBranch.getPrefType("enabled"), prefBranch.PREF_INVALID,
+     "The preference branch for the extension should have been removed");
+
   BrowserTestUtils.removeTab(tab);
 });
 
 add_task(async function test_devtools_page_panels_switch_toolbox_host() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
 
   function devtools_panel() {
     const hasDevToolsAPINamespace = "devtools" in browser;
@@ -341,17 +441,17 @@ add_task(async function test_devtools_pa
     browser.test.sendMessage("devtools_panel_loaded", {
       hasDevToolsAPINamespace,
       panelLoadedURL: window.location.href,
     });
   }
 
   async function devtools_page() {
     const panel = await browser.devtools.panels.create(
-      "Test Panel", "fake-icon.png", "devtools_panel.html"
+      "Test Panel Switch Host", "fake-icon.png", "devtools_panel.html"
     );
 
     panel.onShown.addListener(panelWindow => {
       browser.test.sendMessage("devtools_panel_shown", panelWindow.location.href);
     });
 
     panel.onHidden.addListener(() => {
       browser.test.sendMessage("devtools_panel_hidden");
@@ -360,48 +460,26 @@ add_task(async function test_devtools_pa
     browser.test.sendMessage("devtools_panel_created");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       devtools_page: "devtools_page.html",
     },
     files: {
-      "devtools_page.html": `<!DOCTYPE html>
-      <html>
-       <head>
-         <meta charset="utf-8">
-       </head>
-       <body>
-         <script src="devtools_page.js"></script>
-       </body>
-      </html>`,
+      "devtools_page.html": createPage("devtools_page.js"),
       "devtools_page.js": devtools_page,
-      "devtools_panel.html":  `<!DOCTYPE html>
-      <html>
-       <head>
-         <meta charset="utf-8">
-       </head>
-       <body>
-         DEVTOOLS PANEL
-         <script src="devtools_panel.js"></script>
-       </body>
-      </html>`,
+      "devtools_panel.html": createPage("devtools_panel.js", "DEVTOOLS PANEL"),
       "devtools_panel.js": devtools_panel,
     },
   });
 
   await extension.startup();
 
-
-  let target = gDevTools.getTargetForTab(tab);
-
-  const toolbox = await gDevTools.showToolbox(target, "testBlankPanel");
-  info("developer toolbox opened");
-
+  let {toolbox, target} = await openToolboxForTab(tab);
   await extension.awaitMessage("devtools_panel_created");
 
   const toolboxAdditionalTools = toolbox.getAdditionalTools();
 
   is(toolboxAdditionalTools.length, 1,
      "Got the expected number of toolbox specific panel registered.");
 
   const panelDef = toolboxAdditionalTools[0];
@@ -443,18 +521,17 @@ add_task(async function test_devtools_pa
   info("Switch the toolbox from docked on bottom to the original dock mode");
   toolbox.switchHost(originalToolboxHostType);
 
   info("Wait for the panel test messages once toolbox dock mode has been restored");
   await extension.awaitMessage("devtools_panel_hidden");
   await extension.awaitMessage("devtools_panel_shown");
   await extension.awaitMessage("devtools_panel_loaded");
 
-  await gDevTools.closeToolbox(target);
-  await target.destroy();
+  await closeToolboxForTab(tab);
 
   await extension.unload();
 
   BrowserTestUtils.removeTab(tab);
 });
 
 add_task(async function test_devtools_page_invalid_panel_urls() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
@@ -540,46 +617,28 @@ add_task(async function test_devtools_pa
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       devtools_page: "devtools_page.html",
       icons: {
         "32": "icon.png",
       },
     },
     files: {
-      "devtools_page.html": `<!DOCTYPE html>
-        <html>
-         <head>
-           <meta charset="utf-8">
-         </head>
-         <body>
-           <script src="devtools_page.js"></script>
-         </body>
-        </html>`,
+      "devtools_page.html": createPage("devtools_page.js"),
       "devtools_page.js": devtools_page,
-      "panel.html":  `<!DOCTYPE html>
-        <html>
-         <head>
-           <meta charset="utf-8">
-         </head>
-         <body>
-           DEVTOOLS PANEL
-         </body>
-        </html>`,
+      "panel.html": createPage("panel.js", "DEVTOOLS PANEL"),
+      "panel.js": "",
       "icon.png": imageBuffer,
       "default-icon.png": imageBuffer,
     },
   });
 
   await extension.startup();
 
-  let target = gDevTools.getTargetForTab(tab);
-
-  let toolbox = await gDevTools.showToolbox(target, "testBlankPanel");
-
+  let {toolbox, target} = await openToolboxForTab(tab);
   info("developer toolbox opened");
 
   await extension.awaitMessage("devtools_page_ready");
 
   extension.sendMessage("start_test_panel_create");
 
   let done = false;
 
@@ -591,15 +650,14 @@ add_task(async function test_devtools_pa
     const lastTool = toolboxAdditionalTools[toolboxAdditionalTools.length - 1];
 
     gDevTools.showToolbox(target, lastTool.id);
     info("Last created panel selected");
   }
 
   await extension.awaitMessage("test_invalid_devtools_panel_urls_done");
 
-  await gDevTools.closeToolbox(target);
-  await target.destroy();
+  await closeToolboxForTab(tab);
 
   await extension.unload();
 
   BrowserTestUtils.removeTab(tab);
 });