Bug 1394750 - Keep track of the active and enabled devtools webextensions.
MozReview-Commit-ID: 4KUQCls8CPe
--- 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.");
}