diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 7ddefd0a4f06..ca31998c2057 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -622,6 +622,10 @@ @RESPATH@/browser/chrome/webide.manifest @RESPATH@/browser/@PREF_DIR@/webide-prefs.js +; [DevTools Shim Files] +@RESPATH@/browser/chrome/devtools-shim@JAREXT@ +@RESPATH@/browser/chrome/devtools-shim.manifest + ; DevTools @RESPATH@/browser/chrome/devtools@JAREXT@ @RESPATH@/browser/chrome/devtools.manifest diff --git a/devtools/client/framework/devtools.js b/devtools/client/framework/devtools.js index 2c0c513de0ae..f880b241ec6c 100644 --- a/devtools/client/framework/devtools.js +++ b/devtools/client/framework/devtools.js @@ -8,6 +8,7 @@ const {Cu} = require("chrome"); const Services = require("Services"); // Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized +loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true); loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true); loader.lazyRequireGetter(this, "ToolboxHostManager", "devtools/client/framework/toolbox-host-manager", true); loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true); @@ -468,6 +469,18 @@ DevTools.prototype = { return true; }), + /** + * Wrapper on TargetFactory.forTab, constructs a Target for the provided tab. + * + * @param {XULTab} tab + * The tab to use in creating a new target. + * + * @return {TabTarget} A target object + */ + getTargetForTab: function (tab) { + return TargetFactory.forTab(tab); + }, + /** * Either the SDK Loader has been destroyed by the add-on contribution * workflow, or firefox is shutting down. diff --git a/devtools/moz.build b/devtools/moz.build index 3cfad8fae3a3..f988fd50dea9 100644 --- a/devtools/moz.build +++ b/devtools/moz.build @@ -15,6 +15,7 @@ if CONFIG['MOZ_DEVTOOLS'] == 'all': DIRS += [ 'server', 'shared', + 'shim', ] # /browser uses DIST_SUBDIR. We opt-in to this treatment when building diff --git a/devtools/shim/DevToolsShim.jsm b/devtools/shim/DevToolsShim.jsm new file mode 100644 index 000000000000..581554d478c8 --- /dev/null +++ b/devtools/shim/DevToolsShim.jsm @@ -0,0 +1,170 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = [ + "DevToolsShim", +]; + +function removeItem(array, callback) { + let index = array.findIndex(callback); + if (index >= 0) { + array.splice(index, 1); + } +} + +/** + * The DevToolsShim is a part of the DevTools go faster project, which moves the Firefox + * DevTools outside of mozilla-central to an add-on. It aims to bridge the gap for + * existing mozilla-central code that still needs to interact with DevTools (such as + * web-extensions). + * + * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools, + * that work whether the DevTools addon is installed or not. It can be used to start + * listening to events, register tools, themes. As soon as a DevTools addon is installed + * the DevToolsShim will forward all the requests received until then to the real DevTools + * instance. + * + * DevToolsShim.isInstalled() can also be used to know if DevTools are currently + * installed. + */ +this.DevToolsShim = { + gDevTools: null, + listeners: [], + tools: [], + themes: [], + + /** + * Check if DevTools are currently installed and available. + * + * @return {Boolean} true if DevTools are installed. + */ + isInstalled: 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) + */ + register: function (gDevTools) { + this.gDevTools = gDevTools; + this._onDevToolsRegistered(); + }, + + /** + * Unregister the current instance of gDevTools. Should be called by DevTools during + * shutdown. + */ + unregister: function () { + this.gDevTools = null; + }, + + /** + * The following methods can be called before DevTools are installed: + * - on + * - off + * - registerTool + * - unregisterTool + * - registerTheme + * - unregisterTheme + * + * If DevTools are not installed 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()) { + 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()) { + 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()) { + 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()) { + 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()) { + 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()) { + this.gDevTools.unregisterTheme(theme); + } else { + removeItem(this.themes, t => t === theme); + } + }, + + _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); + } + + for (let theme of this.themes) { + this.gDevTools.registerTheme(theme); + } + + this.listeners = []; + this.tools = []; + this.themes = []; + }, +}; diff --git a/devtools/shim/jar.mn b/devtools/shim/jar.mn new file mode 100644 index 000000000000..5536034fecc7 --- /dev/null +++ b/devtools/shim/jar.mn @@ -0,0 +1,3 @@ +devtools-shim.jar: +% content devtools-shim %content/ + content/DevToolsShim.jsm (DevToolsShim.jsm) diff --git a/devtools/shim/moz.build b/devtools/shim/moz.build new file mode 100644 index 000000000000..552ad174f67d --- /dev/null +++ b/devtools/shim/moz.build @@ -0,0 +1,3 @@ +JAR_MANIFESTS += ['jar.mn'] + +XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini'] diff --git a/devtools/shim/tests/unit/.eslintrc.js b/devtools/shim/tests/unit/.eslintrc.js new file mode 100644 index 000000000000..c25bec1e2c4c --- /dev/null +++ b/devtools/shim/tests/unit/.eslintrc.js @@ -0,0 +1,21 @@ +// This file was copied from the .eslintrc.xpcshell.js +// This new xpcshell test folder should stay in mozilla-central while devtools move to a +// GitHub repository, hence the duplication. +module.exports = { + "extends": [ + "plugin:mozilla/xpcshell-test" + ], + "rules": { + // Allow non-camelcase so that run_test doesn't produce a warning. + "camelcase": "off", + // Allow using undefined variables so that tests can refer to functions + // and variables defined in head.js files, without having to maintain a + // list of globals in each .eslintrc file. + // Note that bug 1168340 will eventually help auto-registering globals + // from head.js files. + "no-undef": "off", + "block-scoped-var": "off", + // Tests can always import anything. + "mozilla/reject-some-requires": "off", + } +}; diff --git a/devtools/shim/tests/unit/test_devtools_shim.js b/devtools/shim/tests/unit/test_devtools_shim.js new file mode 100644 index 000000000000..8b4f48cb8fbe --- /dev/null +++ b/devtools/shim/tests/unit/test_devtools_shim.js @@ -0,0 +1,192 @@ +/* -*- 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 } = + Components.utils.import("chrome://devtools-shim/content/DevToolsShim.jsm", {}); + +// 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 = [ + "on", + "off", + "registerTool", + "registerTheme", + "unregisterTool", + "unregisterTheme", + ]; + + let mock = { + callLog: {} + }; + + for (let method of methods) { + // Create a stub for method, that only pushes its arguments in the inner callLog + mock[method] = function (...args) { + mock.callLog[method].push(args); + }; + mock.callLog[method] = []; + } + + return mock; +} + +/** + * 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"); + + // If we don't want to check the last call or if the method was never called, bail out. + if (!lastArgs || length === 0) { + return; + } + + 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"); + + DevToolsShim.register(createMockDevTools()); + ok(DevToolsShim.isInstalled(), "DevTools are installed"); + + DevToolsShim.unregister(); + ok(!DevToolsShim.isInstalled(), "DevTools are not installed"); +} + +function test_on_is_forwarded_to_devtools() { + ok(!DevToolsShim.isInstalled(), "DevTools are not installed"); + + 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"); + + 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"); + + 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"); + + let tool1 = {}; + let tool2 = {}; + let tool3 = {}; + let mock = createMockDevTools(); + + // Pre-register tool1 + DevToolsShim.registerTool(tool1); + + // Pre-register tool3, but unregister right after + DevToolsShim.registerTool(tool3); + DevToolsShim.unregisterTool(tool3); + + DevToolsShim.register(mock); + checkCalls(mock, "registerTool", 1, [tool1]); + + DevToolsShim.registerTool(tool2); + checkCalls(mock, "registerTool", 2, [tool2]); + + DevToolsShim.unregister(); + + // 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"); + + let theme1 = {}; + let theme2 = {}; + let theme3 = {}; + let mock = createMockDevTools(); + + // Pre-register theme1 + DevToolsShim.registerTheme(theme1); + + // Pre-register theme3, but unregister right after + DevToolsShim.registerTheme(theme3); + DevToolsShim.unregisterTheme(theme3); + + DevToolsShim.register(mock); + checkCalls(mock, "registerTheme", 1, [theme1]); + + DevToolsShim.registerTheme(theme2); + checkCalls(mock, "registerTheme", 2, [theme2]); + + DevToolsShim.unregister(); + + // Create a new mock and check the themes are not added once again. + mock = createMockDevTools(); + DevToolsShim.register(mock); + checkCalls(mock, "registerTheme", 0); +} + +function run_test() { + test_register_unregister(); + DevToolsShim.unregister(); + + test_on_is_forwarded_to_devtools(); + DevToolsShim.unregister(); + + test_off_called_before_registering_devtools(); + DevToolsShim.unregister(); + + test_off_called_before_with_bad_callback(); + DevToolsShim.unregister(); + + test_registering_tool(); + DevToolsShim.unregister(); + + test_registering_theme(); + DevToolsShim.unregister(); +} diff --git a/devtools/shim/tests/unit/xpcshell.ini b/devtools/shim/tests/unit/xpcshell.ini new file mode 100644 index 000000000000..c329a440d988 --- /dev/null +++ b/devtools/shim/tests/unit/xpcshell.ini @@ -0,0 +1,6 @@ +[DEFAULT] +tags = devtools +firefox-appdir = browser +skip-if = toolkit == 'android' + +[test_devtools_shim.js]