summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework')
-rw-r--r--devtools/client/framework/ToolboxProcess.jsm291
-rw-r--r--devtools/client/framework/about-devtools-toolbox.js61
-rw-r--r--devtools/client/framework/attach-thread.js115
-rw-r--r--devtools/client/framework/browser-menus.js390
-rw-r--r--devtools/client/framework/connect/connect.css112
-rw-r--r--devtools/client/framework/connect/connect.js236
-rw-r--r--devtools/client/framework/connect/connect.xhtml52
-rw-r--r--devtools/client/framework/dev-edition-promo/dev-edition-logo.pngbin0 -> 6764 bytes
-rw-r--r--devtools/client/framework/dev-edition-promo/dev-edition-promo.css94
-rw-r--r--devtools/client/framework/dev-edition-promo/dev-edition-promo.xul36
-rw-r--r--devtools/client/framework/devtools-browser.js758
-rw-r--r--devtools/client/framework/devtools.js534
-rw-r--r--devtools/client/framework/gDevTools.jsm162
-rw-r--r--devtools/client/framework/location-store.js103
-rw-r--r--devtools/client/framework/menu-item.js65
-rw-r--r--devtools/client/framework/menu.js173
-rw-r--r--devtools/client/framework/moz.build33
-rw-r--r--devtools/client/framework/options-panel.css107
-rw-r--r--devtools/client/framework/selection.js247
-rw-r--r--devtools/client/framework/sidebar.js592
-rw-r--r--devtools/client/framework/source-map-service.js209
-rw-r--r--devtools/client/framework/source-map-util.js20
-rw-r--r--devtools/client/framework/source-map-worker.js220
-rw-r--r--devtools/client/framework/source-map.js84
-rw-r--r--devtools/client/framework/target-from-url.js120
-rw-r--r--devtools/client/framework/target.js825
-rw-r--r--devtools/client/framework/test/.eslintrc.js6
-rw-r--r--devtools/client/framework/test/browser.ini95
-rw-r--r--devtools/client/framework/test/browser_browser_toolbox.js65
-rw-r--r--devtools/client/framework/test/browser_browser_toolbox_debugger.js131
-rw-r--r--devtools/client/framework/test/browser_devtools_api.js264
-rw-r--r--devtools/client/framework/test/browser_devtools_api_destroy.js71
-rw-r--r--devtools/client/framework/test/browser_dynamic_tool_enabling.js41
-rw-r--r--devtools/client/framework/test/browser_ignore_toolbox_network_requests.js33
-rw-r--r--devtools/client/framework/test/browser_keybindings_01.js115
-rw-r--r--devtools/client/framework/test/browser_keybindings_02.js65
-rw-r--r--devtools/client/framework/test/browser_keybindings_03.js53
-rw-r--r--devtools/client/framework/test/browser_menu_api.js181
-rw-r--r--devtools/client/framework/test/browser_new_activation_workflow.js69
-rw-r--r--devtools/client/framework/test/browser_source_map-01.js115
-rw-r--r--devtools/client/framework/test/browser_source_map-02.js113
-rw-r--r--devtools/client/framework/test/browser_target_events.js56
-rw-r--r--devtools/client/framework/test/browser_target_from_url.js133
-rw-r--r--devtools/client/framework/test/browser_target_remote.js25
-rw-r--r--devtools/client/framework/test/browser_target_support.js74
-rw-r--r--devtools/client/framework/test/browser_toolbox_custom_host.js57
-rw-r--r--devtools/client/framework/test/browser_toolbox_dynamic_registration.js105
-rw-r--r--devtools/client/framework/test/browser_toolbox_getpanelwhenready.js36
-rw-r--r--devtools/client/framework/test/browser_toolbox_highlight.js81
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts.js139
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts_size.js69
-rw-r--r--devtools/client/framework/test/browser_toolbox_hosts_telemetry.js50
-rw-r--r--devtools/client/framework/test/browser_toolbox_keyboard_navigation.js81
-rw-r--r--devtools/client/framework/test/browser_toolbox_minimize.js106
-rw-r--r--devtools/client/framework/test/browser_toolbox_options.js297
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_buttons.js163
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js34
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js47
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs28
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js.html46
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js.js119
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html33
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html10
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js126
-rw-r--r--devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing_frame_script.js46
-rw-r--r--devtools/client/framework/test/browser_toolbox_races.js81
-rw-r--r--devtools/client/framework/test/browser_toolbox_raise.js78
-rw-r--r--devtools/client/framework/test/browser_toolbox_ready.js21
-rw-r--r--devtools/client/framework/test/browser_toolbox_remoteness_change.js43
-rw-r--r--devtools/client/framework/test/browser_toolbox_select_event.js101
-rw-r--r--devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js48
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar.js181
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar_events.js93
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar_existing_tabs.js78
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js80
-rw-r--r--devtools/client/framework/test/browser_toolbox_sidebar_tool.xul18
-rw-r--r--devtools/client/framework/test/browser_toolbox_split_console.js85
-rw-r--r--devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js68
-rw-r--r--devtools/client/framework/test/browser_toolbox_target.js60
-rw-r--r--devtools/client/framework/test/browser_toolbox_textbox_context_menu.js55
-rw-r--r--devtools/client/framework/test/browser_toolbox_theme_registration.js102
-rw-r--r--devtools/client/framework/test/browser_toolbox_toggle.js108
-rw-r--r--devtools/client/framework/test/browser_toolbox_tool_ready.js51
-rw-r--r--devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js135
-rw-r--r--devtools/client/framework/test/browser_toolbox_transport_events.js108
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_01.js46
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_02.js54
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_03.js40
-rw-r--r--devtools/client/framework/test/browser_toolbox_view_source_04.js39
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_reload_target.js100
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_shortcuts.js84
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_changes.js108
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_changes_page.html10
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_frame_select.js94
-rw-r--r--devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html11
-rw-r--r--devtools/client/framework/test/browser_toolbox_zoom.js67
-rw-r--r--devtools/client/framework/test/browser_two_tabs.js149
-rw-r--r--devtools/client/framework/test/code_binary_search.coffee18
-rw-r--r--devtools/client/framework/test/code_binary_search.js29
-rw-r--r--devtools/client/framework/test/code_binary_search.map10
-rw-r--r--devtools/client/framework/test/code_math.js9
-rw-r--r--devtools/client/framework/test/code_ugly.js3
-rw-r--r--devtools/client/framework/test/doc_empty-tab-01.html14
-rw-r--r--devtools/client/framework/test/doc_theme.css3
-rw-r--r--devtools/client/framework/test/doc_viewsource.html13
-rw-r--r--devtools/client/framework/test/head.js148
-rw-r--r--devtools/client/framework/test/helper_disable_cache.js128
-rw-r--r--devtools/client/framework/test/serviceworker.js6
-rw-r--r--devtools/client/framework/test/shared-head.js596
-rw-r--r--devtools/client/framework/test/shared-redux-head.js85
-rw-r--r--devtools/client/framework/toolbox-highlighter-utils.js324
-rw-r--r--devtools/client/framework/toolbox-host-manager.js244
-rw-r--r--devtools/client/framework/toolbox-hosts.js425
-rw-r--r--devtools/client/framework/toolbox-init.js73
-rw-r--r--devtools/client/framework/toolbox-options.js431
-rw-r--r--devtools/client/framework/toolbox-options.xhtml201
-rw-r--r--devtools/client/framework/toolbox-process-window.js230
-rw-r--r--devtools/client/framework/toolbox-process-window.xul47
-rw-r--r--devtools/client/framework/toolbox-window.xul47
-rw-r--r--devtools/client/framework/toolbox.js2417
-rw-r--r--devtools/client/framework/toolbox.xul83
121 files changed, 16997 insertions, 0 deletions
diff --git a/devtools/client/framework/ToolboxProcess.jsm b/devtools/client/framework/ToolboxProcess.jsm
new file mode 100644
index 000000000..cd12e92cd
--- /dev/null
+++ b/devtools/client/framework/ToolboxProcess.jsm
@@ -0,0 +1,291 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const DBG_XUL = "chrome://devtools/content/framework/toolbox-process-window.xul";
+const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";
+
+const { require, DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "Telemetry", function () {
+ return require("devtools/client/shared/telemetry");
+});
+XPCOMUtils.defineLazyGetter(this, "EventEmitter", function () {
+ return require("devtools/shared/event-emitter");
+});
+const promise = require("promise");
+const Services = require("Services");
+
+this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
+
+var processes = new Set();
+
+/**
+ * Constructor for creating a process that will hold a chrome toolbox.
+ *
+ * @param function aOnClose [optional]
+ * A function called when the process stops running.
+ * @param function aOnRun [optional]
+ * A function called when the process starts running.
+ * @param object aOptions [optional]
+ * An object with properties for configuring BrowserToolboxProcess.
+ */
+this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) {
+ let emitter = new EventEmitter();
+ this.on = emitter.on.bind(emitter);
+ this.off = emitter.off.bind(emitter);
+ this.once = emitter.once.bind(emitter);
+ // Forward any events to the shared emitter.
+ this.emit = function (...args) {
+ emitter.emit(...args);
+ BrowserToolboxProcess.emit(...args);
+ };
+
+ // If first argument is an object, use those properties instead of
+ // all three arguments
+ if (typeof aOnClose === "object") {
+ if (aOnClose.onClose) {
+ this.once("close", aOnClose.onClose);
+ }
+ if (aOnClose.onRun) {
+ this.once("run", aOnClose.onRun);
+ }
+ this._options = aOnClose;
+ } else {
+ if (aOnClose) {
+ this.once("close", aOnClose);
+ }
+ if (aOnRun) {
+ this.once("run", aOnRun);
+ }
+ this._options = aOptions || {};
+ }
+
+ this._telemetry = new Telemetry();
+
+ this.close = this.close.bind(this);
+ Services.obs.addObserver(this.close, "quit-application", false);
+ this._initServer();
+ this._initProfile();
+ this._create();
+
+ processes.add(this);
+};
+
+EventEmitter.decorate(BrowserToolboxProcess);
+
+/**
+ * Initializes and starts a chrome toolbox process.
+ * @return object
+ */
+BrowserToolboxProcess.init = function (aOnClose, aOnRun, aOptions) {
+ return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions);
+};
+
+/**
+ * Passes a set of options to the BrowserAddonActors for the given ID.
+ *
+ * @param aId string
+ * The ID of the add-on to pass the options to
+ * @param aOptions object
+ * The options.
+ * @return a promise that will be resolved when complete.
+ */
+BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) {
+ let promises = [];
+
+ for (let process of processes.values()) {
+ promises.push(process.debuggerServer.setAddonOptions(aId, aOptions));
+ }
+
+ return promise.all(promises);
+};
+
+BrowserToolboxProcess.prototype = {
+ /**
+ * Initializes the debugger server.
+ */
+ _initServer: function () {
+ if (this.debuggerServer) {
+ dumpn("The chrome toolbox server is already running.");
+ return;
+ }
+
+ dumpn("Initializing the chrome toolbox server.");
+
+ // Create a separate loader instance, so that we can be sure to receive a
+ // separate instance of the DebuggingServer from the rest of the devtools.
+ // This allows us to safely use the tools against even the actors and
+ // DebuggingServer itself, especially since we can mark this loader as
+ // invisible to the debugger (unlike the usual loader settings).
+ this.loader = new DevToolsLoader();
+ this.loader.invisibleToDebugger = true;
+ let { DebuggerServer } = this.loader.require("devtools/server/main");
+ this.debuggerServer = DebuggerServer;
+ dumpn("Created a separate loader instance for the DebuggerServer.");
+
+ // Forward interesting events.
+ this.debuggerServer.on("connectionchange", this.emit);
+
+ this.debuggerServer.init();
+ this.debuggerServer.addBrowserActors();
+ this.debuggerServer.allowChromeProcess = true;
+ dumpn("initialized and added the browser actors for the DebuggerServer.");
+
+ let chromeDebuggingPort =
+ Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
+ let chromeDebuggingWebSocket =
+ Services.prefs.getBoolPref("devtools.debugger.chrome-debugging-websocket");
+ let listener = this.debuggerServer.createListener();
+ listener.portOrPath = chromeDebuggingPort;
+ listener.webSocket = chromeDebuggingWebSocket;
+ listener.open();
+
+ dumpn("Finished initializing the chrome toolbox server.");
+ dumpn("Started listening on port: " + chromeDebuggingPort);
+ },
+
+ /**
+ * Initializes a profile for the remote debugger process.
+ */
+ _initProfile: function () {
+ dumpn("Initializing the chrome toolbox user profile.");
+
+ let debuggingProfileDir = Services.dirsvc.get("ProfLD", Ci.nsIFile);
+ debuggingProfileDir.append(CHROME_DEBUGGER_PROFILE_NAME);
+ try {
+ debuggingProfileDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ } catch (ex) {
+ // Don't re-copy over the prefs again if this profile already exists
+ if (ex.result === Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ this._dbgProfilePath = debuggingProfileDir.path;
+ } else {
+ dumpn("Error trying to create a profile directory, failing.");
+ dumpn("Error: " + (ex.message || ex));
+ }
+ return;
+ }
+
+ this._dbgProfilePath = debuggingProfileDir.path;
+
+ // We would like to copy prefs into this new profile...
+ let prefsFile = debuggingProfileDir.clone();
+ prefsFile.append("prefs.js");
+ // ... but unfortunately, when we run tests, it seems the starting profile
+ // clears out the prefs file before re-writing it, and in practice the
+ // file is empty when we get here. So just copying doesn't work in that
+ // case.
+ // We could force a sync pref flush and then copy it... but if we're doing
+ // that, we might as well just flush directly to the new profile, which
+ // always works:
+ Services.prefs.savePrefFile(prefsFile);
+
+ dumpn("Finished creating the chrome toolbox user profile at: " + this._dbgProfilePath);
+ },
+
+ /**
+ * Creates and initializes the profile & process for the remote debugger.
+ */
+ _create: function () {
+ dumpn("Initializing chrome debugging process.");
+ let process = this._dbgProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(Services.dirsvc.get("XREExeF", Ci.nsIFile));
+
+ let xulURI = DBG_XUL;
+
+ if (this._options.addonID) {
+ xulURI += "?addonID=" + this._options.addonID;
+ }
+
+ dumpn("Running chrome debugging process.");
+ let args = ["-no-remote", "-foreground", "-profile", this._dbgProfilePath, "-chrome", xulURI];
+
+ // During local development, incremental builds can trigger the main process
+ // to clear its startup cache with the "flag file" .purgecaches, but this
+ // file is removed during app startup time, so we aren't able to know if it
+ // was present in order to also clear the child profile's startup cache as
+ // well.
+ //
+ // As an approximation of "isLocalBuild", check for an unofficial build.
+ if (!Services.appinfo.isOfficial) {
+ args.push("-purgecaches");
+ }
+
+ // Disable safe mode for the new process in case this was opened via the
+ // keyboard shortcut.
+ let nsIEnvironment = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
+ let originalValue = nsIEnvironment.get("MOZ_DISABLE_SAFE_MODE_KEY");
+ nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", "1");
+
+ process.runwAsync(args, args.length, { observe: () => this.close() });
+
+ // Now that the process has started, it's safe to reset the env variable.
+ nsIEnvironment.set("MOZ_DISABLE_SAFE_MODE_KEY", originalValue);
+
+ this._telemetry.toolOpened("jsbrowserdebugger");
+
+ dumpn("Chrome toolbox is now running...");
+ this.emit("run", this);
+ },
+
+ /**
+ * Closes the remote debugging server and kills the toolbox process.
+ */
+ close: function () {
+ if (this.closed) {
+ return;
+ }
+
+ dumpn("Cleaning up the chrome debugging process.");
+ Services.obs.removeObserver(this.close, "quit-application");
+
+ if (this._dbgProcess.isRunning) {
+ this._dbgProcess.kill();
+ }
+
+ this._telemetry.toolClosed("jsbrowserdebugger");
+ if (this.debuggerServer) {
+ this.debuggerServer.off("connectionchange", this.emit);
+ this.debuggerServer.destroy();
+ this.debuggerServer = null;
+ }
+
+ dumpn("Chrome toolbox is now closed...");
+ this.closed = true;
+ this.emit("close", this);
+ processes.delete(this);
+
+ this._dbgProcess = null;
+ this._options = null;
+ if (this.loader) {
+ this.loader.destroy();
+ }
+ this.loader = null;
+ this._telemetry = null;
+ }
+};
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("DBG-FRONTEND: " + str + "\n");
+ }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+
+Services.prefs.addObserver("devtools.debugger.log", {
+ observe: (...args) => wantLogging = Services.prefs.getBoolPref(args.pop())
+}, false);
+
+Services.obs.notifyObservers(null, "ToolboxProcessLoaded", null);
diff --git a/devtools/client/framework/about-devtools-toolbox.js b/devtools/client/framework/about-devtools-toolbox.js
new file mode 100644
index 000000000..0ae776e37
--- /dev/null
+++ b/devtools/client/framework/about-devtools-toolbox.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Register about:devtools-toolbox which allows to open a devtools toolbox
+// in a Firefox tab or a custom html iframe in browser.html
+
+const { Ci, Cu, Cm, components } = require("chrome");
+const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+const Services = require("Services");
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const { nsIAboutModule } = Ci;
+
+function AboutURL() {}
+
+AboutURL.prototype = {
+ uri: Services.io.newURI("chrome://devtools/content/framework/toolbox.xul",
+ null, null),
+ classDescription: "about:devtools-toolbox",
+ classID: components.ID("11342911-3135-45a8-8d71-737a2b0ad469"),
+ contractID: "@mozilla.org/network/protocol/about;1?what=devtools-toolbox",
+
+ QueryInterface: XPCOMUtils.generateQI([nsIAboutModule]),
+
+ newChannel: function (aURI, aLoadInfo) {
+ let chan = Services.io.newChannelFromURIWithLoadInfo(this.uri, aLoadInfo);
+ chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+ return chan;
+ },
+
+ getURIFlags: function (aURI) {
+ return nsIAboutModule.ALLOW_SCRIPT || nsIAboutModule.ENABLE_INDEXED_DB;
+ }
+};
+
+AboutURL.createInstance = function (outer, iid) {
+ if (outer) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return new AboutURL();
+};
+
+exports.register = function () {
+ if (registrar.isCIDRegistered(AboutURL.prototype.classID)) {
+ console.error("Trying to register " + AboutURL.prototype.classDescription +
+ " more than once.");
+ } else {
+ registrar.registerFactory(AboutURL.prototype.classID,
+ AboutURL.prototype.classDescription,
+ AboutURL.prototype.contractID,
+ AboutURL);
+ }
+};
+
+exports.unregister = function () {
+ if (registrar.isCIDRegistered(AboutURL.prototype.classID)) {
+ registrar.unregisterFactory(AboutURL.prototype.classID, AboutURL);
+ }
+};
diff --git a/devtools/client/framework/attach-thread.js b/devtools/client/framework/attach-thread.js
new file mode 100644
index 000000000..db445ce23
--- /dev/null
+++ b/devtools/client/framework/attach-thread.js
@@ -0,0 +1,115 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+const {Cc, Ci, Cu} = require("chrome");
+const Services = require("Services");
+const defer = require("devtools/shared/defer");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+function handleThreadState(toolbox, event, packet) {
+ // Suppress interrupted events by default because the thread is
+ // paused/resumed a lot for various actions.
+ if (event !== "paused" || packet.why.type !== "interrupted") {
+ // TODO: Bug 1225492, we continue emitting events on the target
+ // like we used to, but we should emit these only on the
+ // threadClient now.
+ toolbox.target.emit("thread-" + event);
+ }
+
+ if (event === "paused") {
+ toolbox.highlightTool("jsdebugger");
+
+ if (packet.why.type === "debuggerStatement" ||
+ packet.why.type === "breakpoint" ||
+ packet.why.type === "exception") {
+ toolbox.raise();
+ toolbox.selectTool("jsdebugger");
+ }
+ } else if (event === "resumed") {
+ toolbox.unhighlightTool("jsdebugger");
+ }
+}
+
+function attachThread(toolbox) {
+ let deferred = defer();
+
+ let target = toolbox.target;
+ let { form: { chromeDebugger, actor } } = target;
+
+ // Sourcemaps are always turned off when using the new debugger
+ // frontend. This is because it does sourcemapping on the
+ // client-side, so the server should not do it. It also does not support
+ // blackboxing yet.
+ let useSourceMaps = false;
+ let autoBlackBox = false;
+ if(!Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
+ useSourceMaps = Services.prefs.getBoolPref("devtools.debugger.source-maps-enabled");
+ autoBlackBox = Services.prefs.getBoolPref("devtools.debugger.auto-black-box");
+ }
+ let threadOptions = { useSourceMaps, autoBlackBox };
+
+ let handleResponse = (res, threadClient) => {
+ if (res.error) {
+ deferred.reject(new Error("Couldn't attach to thread: " + res.error));
+ return;
+ }
+ threadClient.addListener("paused", handleThreadState.bind(null, toolbox));
+ threadClient.addListener("resumed", handleThreadState.bind(null, toolbox));
+
+ if (!threadClient.paused) {
+ deferred.reject(
+ new Error("Thread in wrong state when starting up, should be paused")
+ );
+ }
+
+ // These flags need to be set here because the client sends them
+ // with the `resume` request. We make sure to do this before
+ // resuming to avoid another interrupt. We can't pass it in with
+ // `threadOptions` because the resume request will override them.
+ threadClient.pauseOnExceptions(
+ Services.prefs.getBoolPref("devtools.debugger.pause-on-exceptions"),
+ Services.prefs.getBoolPref("devtools.debugger.ignore-caught-exceptions")
+ );
+
+ threadClient.resume(res => {
+ if (res.error === "wrongOrder") {
+ const box = toolbox.getNotificationBox();
+ box.appendNotification(
+ L10N.getStr("toolbox.resumeOrderWarning"),
+ "wrong-resume-order",
+ "",
+ box.PRIORITY_WARNING_HIGH
+ );
+ }
+
+ deferred.resolve(threadClient);
+ });
+ };
+
+ if (target.isTabActor) {
+ // Attaching a tab, a browser process, or a WebExtensions add-on.
+ target.activeTab.attachThread(threadOptions, handleResponse);
+ } else if (target.isAddon) {
+ // Attaching a legacy addon.
+ target.client.attachAddon(actor, res => {
+ target.client.attachThread(res.threadActor, handleResponse);
+ });
+ } else {
+ // Attaching an old browser debugger or a content process.
+ target.client.attachThread(chromeDebugger, handleResponse);
+ }
+
+ return deferred.promise;
+}
+
+function detachThread(threadClient) {
+ threadClient.removeListener("paused");
+ threadClient.removeListener("resumed");
+}
+
+module.exports = { attachThread, detachThread };
diff --git a/devtools/client/framework/browser-menus.js b/devtools/client/framework/browser-menus.js
new file mode 100644
index 000000000..3d6c4def6
--- /dev/null
+++ b/devtools/client/framework/browser-menus.js
@@ -0,0 +1,390 @@
+/* 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 module inject dynamically menu items and key shortcuts into browser UI.
+ *
+ * Menu and shortcut definitions are fetched from:
+ * - devtools/client/menus for top level entires
+ * - devtools/client/definitions for tool-specifics entries
+ */
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const MENUS_L10N = new LocalizationHelper("devtools/client/locales/menus.properties");
+
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+
+// Keep list of inserted DOM Elements in order to remove them on unload
+// Maps browser xul document => list of DOM Elements
+const FragmentsCache = new Map();
+
+function l10n(key) {
+ return MENUS_L10N.getStr(key);
+}
+
+/**
+ * Create a xul:key element
+ *
+ * @param {XULDocument} doc
+ * The document to which keys are to be added.
+ * @param {String} id
+ * key's id, automatically prefixed with "key_".
+ * @param {String} shortcut
+ * The key shortcut value.
+ * @param {String} keytext
+ * If `shortcut` refers to a function key, refers to the localized
+ * string to describe a non-character shortcut.
+ * @param {String} modifiers
+ * Space separated list of modifier names.
+ * @param {Function} oncommand
+ * The function to call when the shortcut is pressed.
+ *
+ * @return XULKeyElement
+ */
+function createKey({ doc, id, shortcut, keytext, modifiers, oncommand }) {
+ let k = doc.createElement("key");
+ k.id = "key_" + id;
+
+ if (shortcut.startsWith("VK_")) {
+ k.setAttribute("keycode", shortcut);
+ if (keytext) {
+ k.setAttribute("keytext", keytext);
+ }
+ } else {
+ k.setAttribute("key", shortcut);
+ }
+
+ if (modifiers) {
+ k.setAttribute("modifiers", modifiers);
+ }
+
+ // Bug 371900: command event is fired only if "oncommand" attribute is set.
+ k.setAttribute("oncommand", ";");
+ k.addEventListener("command", oncommand);
+
+ return k;
+}
+
+/**
+ * Create a xul:menuitem element
+ *
+ * @param {XULDocument} doc
+ * The document to which keys are to be added.
+ * @param {String} id
+ * Element id.
+ * @param {String} label
+ * Menu label.
+ * @param {String} accesskey (optional)
+ * Access key of the menuitem, used as shortcut while opening the menu.
+ * @param {Boolean} isCheckbox (optional)
+ * If true, the menuitem will act as a checkbox and have an optional
+ * tick on its left.
+ *
+ * @return XULMenuItemElement
+ */
+function createMenuItem({ doc, id, label, accesskey, isCheckbox }) {
+ let menuitem = doc.createElement("menuitem");
+ menuitem.id = id;
+ menuitem.setAttribute("label", label);
+ if (accesskey) {
+ menuitem.setAttribute("accesskey", accesskey);
+ }
+ if (isCheckbox) {
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("autocheck", "false");
+ }
+ return menuitem;
+}
+
+/**
+ * Add a <key> to <keyset id="devtoolsKeyset">.
+ * Appending a <key> element is not always enough. The <keyset> needs
+ * to be detached and reattached to make sure the <key> is taken into
+ * account (see bug 832984).
+ *
+ * @param {XULDocument} doc
+ * The document to which keys are to be added
+ * @param {XULElement} or {DocumentFragment} keys
+ * Keys to add
+ */
+function attachKeybindingsToBrowser(doc, keys) {
+ let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
+
+ if (!devtoolsKeyset) {
+ devtoolsKeyset = doc.createElement("keyset");
+ devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
+ }
+ devtoolsKeyset.appendChild(keys);
+ let mainKeyset = doc.getElementById("mainKeyset");
+ mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
+}
+
+/**
+ * Add a menu entry for a tool definition
+ *
+ * @param {Object} toolDefinition
+ * Tool definition of the tool to add a menu entry.
+ * @param {XULDocument} doc
+ * The document to which the tool menu item is to be added.
+ */
+function createToolMenuElements(toolDefinition, doc) {
+ let id = toolDefinition.id;
+ let menuId = "menuitem_" + id;
+
+ // Prevent multiple entries for the same tool.
+ if (doc.getElementById(menuId)) {
+ return;
+ }
+
+ let oncommand = function (id, event) {
+ let window = event.target.ownerDocument.defaultView;
+ gDevToolsBrowser.selectToolCommand(window.gBrowser, id);
+ }.bind(null, id);
+
+ let key = null;
+ if (toolDefinition.key) {
+ key = createKey({
+ doc,
+ id,
+ shortcut: toolDefinition.key,
+ modifiers: toolDefinition.modifiers,
+ oncommand: oncommand
+ });
+ }
+
+ let menuitem = createMenuItem({
+ doc,
+ id: "menuitem_" + id,
+ label: toolDefinition.menuLabel || toolDefinition.label,
+ accesskey: toolDefinition.accesskey
+ });
+ if (key) {
+ // Refer to the key in order to display the key shortcut at menu ends
+ menuitem.setAttribute("key", key.id);
+ }
+ menuitem.addEventListener("command", oncommand);
+
+ return {
+ key,
+ menuitem
+ };
+}
+
+/**
+ * Create xul menuitem, key elements for a given tool.
+ * And then insert them into browser DOM.
+ *
+ * @param {XULDocument} doc
+ * The document to which the tool is to be registered.
+ * @param {Object} toolDefinition
+ * Tool definition of the tool to register.
+ * @param {Object} prevDef
+ * The tool definition after which the tool menu item is to be added.
+ */
+function insertToolMenuElements(doc, toolDefinition, prevDef) {
+ let { key, menuitem } = createToolMenuElements(toolDefinition, doc);
+
+ if (key) {
+ attachKeybindingsToBrowser(doc, key);
+ }
+
+ let ref;
+ if (prevDef) {
+ let menuitem = doc.getElementById("menuitem_" + prevDef.id);
+ ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
+ } else {
+ ref = doc.getElementById("menu_devtools_separator");
+ }
+
+ if (ref) {
+ ref.parentNode.insertBefore(menuitem, ref);
+ }
+}
+exports.insertToolMenuElements = insertToolMenuElements;
+
+/**
+ * Remove a tool's menuitem from a window
+ *
+ * @param {string} toolId
+ * Id of the tool to add a menu entry for
+ * @param {XULDocument} doc
+ * The document to which the tool menu item is to be removed from
+ */
+function removeToolFromMenu(toolId, doc) {
+ let key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.remove();
+ }
+
+ let menuitem = doc.getElementById("menuitem_" + toolId);
+ if (menuitem) {
+ menuitem.remove();
+ }
+}
+exports.removeToolFromMenu = removeToolFromMenu;
+
+/**
+ * Add all tools to the developer tools menu of a window.
+ *
+ * @param {XULDocument} doc
+ * The document to which the tool items are to be added.
+ */
+function addAllToolsToMenu(doc) {
+ let fragKeys = doc.createDocumentFragment();
+ let fragMenuItems = doc.createDocumentFragment();
+
+ for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
+ if (!toolDefinition.inMenu) {
+ continue;
+ }
+
+ let elements = createToolMenuElements(toolDefinition, doc);
+
+ if (!elements) {
+ continue;
+ }
+
+ if (elements.key) {
+ fragKeys.appendChild(elements.key);
+ }
+ fragMenuItems.appendChild(elements.menuitem);
+ }
+
+ attachKeybindingsToBrowser(doc, fragKeys);
+
+ let mps = doc.getElementById("menu_devtools_separator");
+ if (mps) {
+ mps.parentNode.insertBefore(fragMenuItems, mps);
+ }
+}
+
+/**
+ * Add global menus and shortcuts that are not panel specific.
+ *
+ * @param {XULDocument} doc
+ * The document to which keys and menus are to be added.
+ */
+function addTopLevelItems(doc) {
+ let keys = doc.createDocumentFragment();
+ let menuItems = doc.createDocumentFragment();
+
+ let { menuitems } = require("../menus");
+ for (let item of menuitems) {
+ if (item.separator) {
+ let separator = doc.createElement("menuseparator");
+ separator.id = item.id;
+ menuItems.appendChild(separator);
+ } else {
+ let { id, l10nKey } = item;
+
+ // Create a <menuitem>
+ let menuitem = createMenuItem({
+ doc,
+ id,
+ label: l10n(l10nKey + ".label"),
+ accesskey: l10n(l10nKey + ".accesskey"),
+ isCheckbox: item.checkbox
+ });
+ menuitem.addEventListener("command", item.oncommand);
+ menuItems.appendChild(menuitem);
+
+ if (item.key && l10nKey) {
+ // Create a <key>
+ let shortcut = l10n(l10nKey + ".key");
+ let key = createKey({
+ doc,
+ id: item.key.id,
+ shortcut: shortcut,
+ keytext: shortcut.startsWith("VK_") ? l10n(l10nKey + ".keytext") : null,
+ modifiers: item.key.modifiers,
+ oncommand: item.oncommand
+ });
+ // Refer to the key in order to display the key shortcut at menu ends
+ menuitem.setAttribute("key", key.id);
+ keys.appendChild(key);
+ }
+ if (item.additionalKeys) {
+ // Create additional <key>
+ for (let key of item.additionalKeys) {
+ let shortcut = l10n(key.l10nKey + ".key");
+ let node = createKey({
+ doc,
+ id: key.id,
+ shortcut: shortcut,
+ keytext: shortcut.startsWith("VK_") ? l10n(key.l10nKey + ".keytext") : null,
+ modifiers: key.modifiers,
+ oncommand: item.oncommand
+ });
+ keys.appendChild(node);
+ }
+ }
+ }
+ }
+
+ // Cache all nodes before insertion to be able to remove them on unload
+ let nodes = [];
+ for (let node of keys.children) {
+ nodes.push(node);
+ }
+ for (let node of menuItems.children) {
+ nodes.push(node);
+ }
+ FragmentsCache.set(doc, nodes);
+
+ attachKeybindingsToBrowser(doc, keys);
+
+ let menu = doc.getElementById("menuWebDeveloperPopup");
+ menu.appendChild(menuItems);
+
+ // There is still "Page Source" menuitem hardcoded into browser.xul. Instead
+ // of manually inserting everything around it, move it to the expected
+ // position.
+ let pageSource = doc.getElementById("menu_pageSource");
+ let endSeparator = doc.getElementById("devToolsEndSeparator");
+ menu.insertBefore(pageSource, endSeparator);
+}
+
+/**
+ * Remove global menus and shortcuts that are not panel specific.
+ *
+ * @param {XULDocument} doc
+ * The document to which keys and menus are to be added.
+ */
+function removeTopLevelItems(doc) {
+ let nodes = FragmentsCache.get(doc);
+ if (!nodes) {
+ return;
+ }
+ FragmentsCache.delete(doc);
+ for (let node of nodes) {
+ node.remove();
+ }
+}
+
+/**
+ * Add menus and shortcuts to a browser document
+ *
+ * @param {XULDocument} doc
+ * The document to which keys and menus are to be added.
+ */
+exports.addMenus = function (doc) {
+ addTopLevelItems(doc);
+
+ addAllToolsToMenu(doc);
+};
+
+/**
+ * Remove menus and shortcuts from a browser document
+ *
+ * @param {XULDocument} doc
+ * The document to which keys and menus are to be removed.
+ */
+exports.removeMenus = function (doc) {
+ // We only remove top level entries. Per-tool entries are removed while
+ // unregistering each tool.
+ removeTopLevelItems(doc);
+};
diff --git a/devtools/client/framework/connect/connect.css b/devtools/client/framework/connect/connect.css
new file mode 100644
index 000000000..23959b93b
--- /dev/null
+++ b/devtools/client/framework/connect/connect.css
@@ -0,0 +1,112 @@
+:root {
+ font: caption;
+}
+
+html {
+ background-color: #111;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ color: white;
+ max-width: 600px;
+ margin: 30px auto 0;
+ box-shadow: 0 2px 3px black;
+ background-color: #3C3E40;
+}
+
+h1 {
+ margin: 0;
+ padding: 20px;
+ background-color: rgba(0,0,0,0.12);
+ background-image: radial-gradient(ellipse farthest-corner at center top , rgb(159, 223, 255), rgba(101, 203, 255, 0.3)), radial-gradient(ellipse farthest-side at center top , rgba(101, 203, 255, 0.4), rgba(101, 203, 255, 0));
+ background-size: 100% 2px, 100% 5px;
+ background-repeat: no-repeat;
+ border-bottom: 1px solid rgba(0,0,0,0.1);
+}
+
+form {
+ display: inline-block;
+}
+
+label {
+ display: block;
+ margin: 10px;
+}
+
+label > span {
+ display: inline-block;
+ min-width: 150px;
+ text-align: right;
+ margin-right: 10px;
+}
+
+#submit {
+ float: right;
+}
+
+input:invalid {
+ box-shadow: 0 0 2px 2px #F06;
+}
+
+section {
+ min-height: 160px;
+ margin: 60px 20px;
+ display: none; /* By default, hidden */
+}
+
+.error-message {
+ color: red;
+}
+
+.error-message:not(.active) {
+ display: none;
+}
+
+body:not(.actors-mode):not(.connecting) > #connection-form {
+ display: block;
+}
+
+body.actors-mode > #actors-list {
+ display: block;
+}
+
+body.connecting > #connecting {
+ display: block;
+}
+
+#connecting {
+ text-align: center;
+}
+
+#connecting > p > img {
+ vertical-align: top;
+}
+
+.actors {
+ padding-left: 0;
+}
+
+.actors > a {
+ display: block;
+ margin: 5px;
+ padding: 5px;
+ color: white;
+}
+
+.remote-process {
+ font-style: italic;
+ opacity: 0.8;
+}
+
+footer {
+ padding: 10px;
+ background-color: rgba(0,0,0,0.12);
+ border-top: 1px solid rgba(0,0,0,0.1);
+ font-size: small;
+}
+
+footer > a,
+footer > a:visited {
+ color: white;
+}
diff --git a/devtools/client/framework/connect/connect.js b/devtools/client/framework/connect/connect.js
new file mode 100644
index 000000000..d713231f9
--- /dev/null
+++ b/devtools/client/framework/connect/connect.js
@@ -0,0 +1,236 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+var Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var Services = require("Services");
+var {gDevTools} = require("devtools/client/framework/devtools");
+var {TargetFactory} = require("devtools/client/framework/target");
+var {Toolbox} = require("devtools/client/framework/toolbox");
+var {DebuggerClient} = require("devtools/shared/client/main");
+var {Task} = require("devtools/shared/task");
+var {LocalizationHelper} = require("devtools/shared/l10n");
+var L10N = new LocalizationHelper("devtools/client/locales/connection-screen.properties");
+
+var gClient;
+var gConnectionTimeout;
+
+/**
+ * Once DOM is ready, we prefil the host/port inputs with
+ * pref-stored values.
+ */
+window.addEventListener("DOMContentLoaded", function onDOMReady() {
+ window.removeEventListener("DOMContentLoaded", onDOMReady, true);
+ let host = Services.prefs.getCharPref("devtools.debugger.remote-host");
+ let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
+
+ if (host) {
+ document.getElementById("host").value = host;
+ }
+
+ if (port) {
+ document.getElementById("port").value = port;
+ }
+
+ let form = document.querySelector("#connection-form form");
+ form.addEventListener("submit", function () {
+ window.submit().catch(e => {
+ console.error(e);
+ // Bug 921850: catch rare exception from DebuggerClient.socketConnect
+ showError("unexpected");
+ });
+ });
+}, true);
+
+/**
+ * Called when the "connect" button is clicked.
+ */
+var submit = Task.async(function* () {
+ // Show the "connecting" screen
+ document.body.classList.add("connecting");
+
+ let host = document.getElementById("host").value;
+ let port = document.getElementById("port").value;
+
+ // Save the host/port values
+ try {
+ Services.prefs.setCharPref("devtools.debugger.remote-host", host);
+ Services.prefs.setIntPref("devtools.debugger.remote-port", port);
+ } catch (e) {
+ // Fails in e10s mode, but not a critical feature.
+ }
+
+ // Initiate the connection
+ let transport = yield DebuggerClient.socketConnect({ host, port });
+ gClient = new DebuggerClient(transport);
+ let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
+ gConnectionTimeout = setTimeout(handleConnectionTimeout, delay);
+ let response = yield gClient.connect();
+ yield onConnectionReady(...response);
+});
+
+/**
+ * Connection is ready. List actors and build buttons.
+ */
+var onConnectionReady = Task.async(function* ([aType, aTraits]) {
+ clearTimeout(gConnectionTimeout);
+
+ let response = yield gClient.listAddons();
+
+ let parent = document.getElementById("addonActors");
+ if (!response.error && response.addons.length > 0) {
+ // Add one entry for each add-on.
+ for (let addon of response.addons) {
+ if (!addon.debuggable) {
+ continue;
+ }
+ buildAddonLink(addon, parent);
+ }
+ }
+ else {
+ // Hide the section when there are no add-ons
+ parent.previousElementSibling.remove();
+ parent.remove();
+ }
+
+ response = yield gClient.listTabs();
+
+ parent = document.getElementById("tabActors");
+
+ // Add Global Process debugging...
+ let globals = Cu.cloneInto(response, {});
+ delete globals.tabs;
+ delete globals.selected;
+ // ...only if there are appropriate actors (a 'from' property will always
+ // be there).
+
+ // Add one entry for each open tab.
+ for (let i = 0; i < response.tabs.length; i++) {
+ buildTabLink(response.tabs[i], parent, i == response.selected);
+ }
+
+ let gParent = document.getElementById("globalActors");
+
+ // Build the Remote Process button
+ // If Fx<39, tab actors were used to be exposed on RootActor
+ // but in Fx>=39, chrome is debuggable via getProcess() and ChromeActor
+ if (globals.consoleActor || gClient.mainRoot.traits.allowChromeProcess) {
+ let a = document.createElement("a");
+ a.onclick = function () {
+ if (gClient.mainRoot.traits.allowChromeProcess) {
+ gClient.getProcess()
+ .then(aResponse => {
+ openToolbox(aResponse.form, true);
+ });
+ } else if (globals.consoleActor) {
+ openToolbox(globals, true, "webconsole", false);
+ }
+ };
+ a.title = a.textContent = L10N.getStr("mainProcess");
+ a.className = "remote-process";
+ a.href = "#";
+ gParent.appendChild(a);
+ }
+ // Move the selected tab on top
+ let selectedLink = parent.querySelector("a.selected");
+ if (selectedLink) {
+ parent.insertBefore(selectedLink, parent.firstChild);
+ }
+
+ document.body.classList.remove("connecting");
+ document.body.classList.add("actors-mode");
+
+ // Ensure the first link is focused
+ let firstLink = parent.querySelector("a:first-of-type");
+ if (firstLink) {
+ firstLink.focus();
+ }
+});
+
+/**
+ * Build one button for an add-on actor.
+ */
+function buildAddonLink(addon, parent) {
+ let a = document.createElement("a");
+ a.onclick = function () {
+ openToolbox(addon, true, "jsdebugger", false);
+ };
+
+ a.textContent = addon.name;
+ a.title = addon.id;
+ a.href = "#";
+
+ parent.appendChild(a);
+}
+
+/**
+ * Build one button for a tab actor.
+ */
+function buildTabLink(tab, parent, selected) {
+ let a = document.createElement("a");
+ a.onclick = function () {
+ openToolbox(tab);
+ };
+
+ a.textContent = tab.title;
+ a.title = tab.url;
+ if (!a.textContent) {
+ a.textContent = tab.url;
+ }
+ a.href = "#";
+
+ if (selected) {
+ a.classList.add("selected");
+ }
+
+ parent.appendChild(a);
+}
+
+/**
+ * An error occured. Let's show it and return to the first screen.
+ */
+function showError(type) {
+ document.body.className = "error";
+ let activeError = document.querySelector(".error-message.active");
+ if (activeError) {
+ activeError.classList.remove("active");
+ }
+ activeError = document.querySelector(".error-" + type);
+ if (activeError) {
+ activeError.classList.add("active");
+ }
+}
+
+/**
+ * Connection timeout.
+ */
+function handleConnectionTimeout() {
+ showError("timeout");
+}
+
+/**
+ * The user clicked on one of the buttons.
+ * Opens the toolbox.
+ */
+function openToolbox(form, chrome = false, tool = "webconsole", isTabActor) {
+ let options = {
+ form: form,
+ client: gClient,
+ chrome: chrome,
+ isTabActor: isTabActor
+ };
+ TargetFactory.forRemoteTab(options).then((target) => {
+ let hostType = Toolbox.HostType.WINDOW;
+ gDevTools.showToolbox(target, tool, hostType).then((toolbox) => {
+ toolbox.once("destroyed", function () {
+ gClient.close();
+ });
+ }, console.error.bind(console));
+ window.close();
+ }, console.error.bind(console));
+}
diff --git a/devtools/client/framework/connect/connect.xhtml b/devtools/client/framework/connect/connect.xhtml
new file mode 100644
index 000000000..e8f8818f6
--- /dev/null
+++ b/devtools/client/framework/connect/connect.xhtml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<!DOCTYPE html [
+<!ENTITY % connectionDTD SYSTEM "chrome://devtools/locale/connection-screen.dtd" >
+ %connectionDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <head>
+ <title>&title;</title>
+ <link rel="stylesheet" href="chrome://devtools/skin/dark-theme.css" type="text/css"/>
+ <link rel="stylesheet" href="chrome://devtools/content/framework/connect/connect.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="connect.js"></script>
+ </head>
+ <body>
+ <h1>&header;</h1>
+ <section id="connection-form">
+ <form validate="validate" action="#">
+ <label>
+ <span>&host;</span>
+ <input required="required" class="devtools-textinput" id="host" type="text"></input>
+ </label>
+ <label>
+ <span>&port;</span>
+ <input required="required" class="devtools-textinput" id="port" type="number" pattern="\d+"></input>
+ </label>
+ <label>
+ <input class="devtools-toolbarbutton" id="submit" standalone="true" type="submit" value="&connect;"></input>
+ </label>
+ </form>
+ <p class="error-message error-timeout">&errorTimeout;</p>
+ <p class="error-message error-refused">&errorRefused;</p>
+ <p class="error-message error-unexpected">&errorUnexpected;</p>
+ </section>
+ <section id="actors-list">
+ <p>&availableTabs;</p>
+ <ul class="actors" id="tabActors"></ul>
+ <p>&availableAddons;</p>
+ <ul class="actors" id="addonActors"></ul>
+ <p>&availableProcesses;</p>
+ <ul class="actors" id="globalActors"></ul>
+ </section>
+ <section id="connecting">
+ <p class="devtools-throbber">&connecting;</p>
+ </section>
+ <footer>&remoteHelp;<a target='_' href='https://developer.mozilla.org/docs/Tools/Remote_Debugging'>&remoteDocumentation;</a>&remoteHelpSuffix;</footer>
+ </body>
+</html>
diff --git a/devtools/client/framework/dev-edition-promo/dev-edition-logo.png b/devtools/client/framework/dev-edition-promo/dev-edition-logo.png
new file mode 100644
index 000000000..4b90768d2
--- /dev/null
+++ b/devtools/client/framework/dev-edition-promo/dev-edition-logo.png
Binary files differ
diff --git a/devtools/client/framework/dev-edition-promo/dev-edition-promo.css b/devtools/client/framework/dev-edition-promo/dev-edition-promo.css
new file mode 100644
index 000000000..01489fd47
--- /dev/null
+++ b/devtools/client/framework/dev-edition-promo/dev-edition-promo.css
@@ -0,0 +1,94 @@
+/* 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/. */
+
+window {
+ -moz-appearance: none;
+ background-color: transparent;
+}
+
+#doorhanger-container {
+ width: 450px;
+}
+
+#top-panel {
+ padding: 20px;
+ background: #343c45; /* toolbars */
+ color: #8fa1b2; /* body text */
+/*
+ * Sloppy preprocessing since UNIX_BUT_NOT_MAC is only defined
+ * in `browser/app/profile/firefox.js`, which this file cannot
+ * depend on. Must style font-size to target linux.
+ */
+%ifdef XP_UNIX
+%ifndef XP_MACOSX
+ font-size: 13px;
+%else
+ font-size: 15px;
+%endif
+%else
+ font-size: 15px;
+%endif
+ line-height: 19px;
+ min-height: 100px;
+}
+
+#top-panel h1 {
+ font-weight: bold;
+ font-family: Open Sans, sans-serif;
+ font-size: 1.1em;
+}
+
+#top-panel p {
+ font-family: Open Sans, sans-serif;
+ font-size: 0.9em;
+ width: 300px;
+ display: block;
+ margin: 5px 0px 0px 0px;
+}
+
+#icon {
+ background-image: url("chrome://devtools/content/framework/dev-edition-promo/dev-edition-logo.png");
+ background-size: 64px 64px;
+ background-repeat: no-repeat;
+ width: 64px;
+ height: 64px;
+ margin-right: 20px;
+}
+
+#lower-panel {
+ padding: 20px;
+ background-color: #252c33; /* tab toolbars */
+ min-height: 75px;
+ border-top: 1px solid #292e33; /* text high contrast (light) */
+}
+
+#button-container {
+ margin: auto 20px;
+}
+
+#button-container button {
+ font: message-box !important;
+ font-size: 16px !important;
+ cursor: pointer;
+ width: 125px;
+ opacity: 1;
+ position: static;
+ -moz-appearance: none;
+ border-radius: 5px;
+ height: 30px;
+ width: 450px;
+ /* Override embossed borders on Windows/Linux */
+ border: none;
+}
+
+#close {
+ background-color: transparent;
+ color: #8fa1b2; /* body text */
+}
+
+#go {
+ margin-left: 100px;
+ background-color: #70bf53; /* green */
+ color: #f5f7fa; /* selection text color */
+}
diff --git a/devtools/client/framework/dev-edition-promo/dev-edition-promo.xul b/devtools/client/framework/dev-edition-promo/dev-edition-promo.xul
new file mode 100644
index 000000000..ca2515ab0
--- /dev/null
+++ b/devtools/client/framework/dev-edition-promo/dev-edition-promo.xul
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet rel="stylesheet" href="chrome://devtools/content/framework/dev-edition-promo/dev-edition-promo.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="dev-edition-promo">
+ <vbox id="doorhanger-container">
+ <hbox flex="1" id="top-panel">
+ <image id="icon" />
+ <vbox id="info">
+ <h1>Using Developer Tools in your browser?</h1>
+ <p>Download Firefox Developer Edition, our first browser made just for you.</p>
+ </vbox>
+ </hbox>
+ <hbox id="lower-panel" flex="1">
+ <hbox id="button-container" flex="1">
+ <button id="close"
+ flex="1"
+ standalone="true"
+ label="No thanks">
+ </button>
+ <button id="go"
+ flex="1"
+ standalone="true"
+ label="Learn more »">
+ </button>
+ </hbox>
+ </hbox>
+ </vbox>
+</window>
diff --git a/devtools/client/framework/devtools-browser.js b/devtools/client/framework/devtools-browser.js
new file mode 100644
index 000000000..b9f4d92ba
--- /dev/null
+++ b/devtools/client/framework/devtools-browser.js
@@ -0,0 +1,758 @@
+/* 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 is the main module loaded in Firefox desktop that handles browser
+ * windows and coordinates devtools around each window.
+ *
+ * This module is loaded lazily by devtools-clhandler.js, once the first
+ * browser window is ready (i.e. fired browser-delayed-startup-finished event)
+ **/
+
+const {Cc, Ci, Cu} = require("chrome");
+const Services = require("Services");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Telemetry = require("devtools/client/shared/telemetry");
+const { gDevTools } = require("./devtools");
+const { when: unload } = require("sdk/system/unload");
+
+// Load target and 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, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "BrowserMenus", "devtools/client/framework/browser-menus");
+
+loader.lazyImporter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
+loader.lazyImporter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
+const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
+const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
+const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
+
+/**
+ * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
+ * Firefox instance.
+ */
+var gDevToolsBrowser = exports.gDevToolsBrowser = {
+ /**
+ * A record of the windows whose menus we altered, so we can undo the changes
+ * as the window is closed
+ */
+ _trackedBrowserWindows: new Set(),
+
+ _telemetry: new Telemetry(),
+
+ _tabStats: {
+ peakOpen: 0,
+ peakPinned: 0,
+ histOpen: [],
+ histPinned: []
+ },
+
+ /**
+ * This function is for the benefit of Tools:DevToolbox in
+ * browser/base/content/browser-sets.inc and should not be used outside
+ * of there
+ */
+ // used by browser-sets.inc, command
+ toggleToolboxCommand: function (gBrowser) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ // If a toolbox exists, using toggle from the Main window :
+ // - should close a docked toolbox
+ // - should focus a windowed toolbox
+ let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
+ isDocked ? gDevTools.closeToolbox(target) : gDevTools.showToolbox(target);
+ },
+
+ /**
+ * This function ensures the right commands are enabled in a window,
+ * depending on their relevant prefs. It gets run when a window is registered,
+ * or when any of the devtools prefs change.
+ */
+ updateCommandAvailability: function (win) {
+ let doc = win.document;
+
+ function toggleMenuItem(id, isEnabled) {
+ let cmd = doc.getElementById(id);
+ if (isEnabled) {
+ cmd.removeAttribute("disabled");
+ cmd.removeAttribute("hidden");
+ } else {
+ cmd.setAttribute("disabled", "true");
+ cmd.setAttribute("hidden", "true");
+ }
+ }
+
+ // Enable developer toolbar?
+ let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled");
+ toggleMenuItem("menu_devToolbar", devToolbarEnabled);
+ let focusEl = doc.getElementById("menu_devToolbar");
+ if (devToolbarEnabled) {
+ focusEl.removeAttribute("disabled");
+ } else {
+ focusEl.setAttribute("disabled", "true");
+ }
+ if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) {
+ win.DeveloperToolbar.show(false).catch(console.error);
+ }
+
+ // Enable WebIDE?
+ let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled");
+ toggleMenuItem("menu_webide", webIDEEnabled);
+
+ let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled");
+ if (webIDEEnabled && showWebIDEWidget) {
+ gDevToolsBrowser.installWebIDEWidget();
+ } else {
+ gDevToolsBrowser.uninstallWebIDEWidget();
+ }
+
+ // Enable Browser Toolbox?
+ let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
+ let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
+ let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled;
+ toggleMenuItem("menu_browserToolbox", remoteEnabled);
+ toggleMenuItem("menu_browserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
+
+ // Enable DevTools connection screen, if the preference allows this.
+ toggleMenuItem("menu_devtools_connect", devtoolsRemoteEnabled);
+ },
+
+ observe: function (subject, topic, prefName) {
+ switch (topic) {
+ case "browser-delayed-startup-finished":
+ this._registerBrowserWindow(subject);
+ break;
+ case "nsPref:changed":
+ if (prefName.endsWith("enabled")) {
+ for (let win of this._trackedBrowserWindows) {
+ this.updateCommandAvailability(win);
+ }
+ }
+ break;
+ }
+ },
+
+ _prefObserverRegistered: false,
+
+ ensurePrefObserver: function () {
+ if (!this._prefObserverRegistered) {
+ this._prefObserverRegistered = true;
+ Services.prefs.addObserver("devtools.", this, false);
+ }
+ },
+
+ /**
+ * This function is for the benefit of Tools:{toolId} commands,
+ * triggered from the WebDeveloper menu and keyboard shortcuts.
+ *
+ * selectToolCommand's behavior:
+ * - if the toolbox is closed,
+ * we open the toolbox and select the tool
+ * - if the toolbox is open, and the targeted tool is not selected,
+ * we select it
+ * - if the toolbox is open, and the targeted tool is selected,
+ * and the host is NOT a window, we close the toolbox
+ * - if the toolbox is open, and the targeted tool is selected,
+ * and the host is a window, we raise the toolbox window
+ */
+ // Used when: - registering a new tool
+ // - new xul window, to add menu items
+ selectToolCommand: function (gBrowser, toolId) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+ let toolDefinition = gDevTools.getToolDefinition(toolId);
+
+ if (toolbox &&
+ (toolbox.currentToolId == toolId ||
+ (toolId == "webconsole" && toolbox.splitConsole)))
+ {
+ toolbox.fireCustomKey(toolId);
+
+ if (toolDefinition.preventClosingOnKey || toolbox.hostType == Toolbox.HostType.WINDOW) {
+ toolbox.raise();
+ } else {
+ gDevTools.closeToolbox(target);
+ }
+ gDevTools.emit("select-tool-command", toolId);
+ } else {
+ gDevTools.showToolbox(target, toolId).then(() => {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = gDevTools.getToolbox(target);
+
+ toolbox.fireCustomKey(toolId);
+ gDevTools.emit("select-tool-command", toolId);
+ });
+ }
+ },
+
+ /**
+ * Open a tab on "about:debugging", optionally pre-select a given tab.
+ */
+ // Used by browser-sets.inc, command
+ openAboutDebugging: function (gBrowser, hash) {
+ let url = "about:debugging" + (hash ? "#" + hash : "");
+ gBrowser.selectedTab = gBrowser.addTab(url);
+ },
+
+ /**
+ * Open a tab to allow connects to a remote browser
+ */
+ // Used by browser-sets.inc, command
+ openConnectScreen: function (gBrowser) {
+ gBrowser.selectedTab = gBrowser.addTab("chrome://devtools/content/framework/connect/connect.xhtml");
+ },
+
+ /**
+ * Open WebIDE
+ */
+ // Used by browser-sets.inc, command
+ // itself, webide widget
+ openWebIDE: function () {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (win) {
+ win.focus();
+ } else {
+ Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
+ }
+ },
+
+ _getContentProcessTarget: function (processId) {
+ // Create a DebuggerServer in order to connect locally to it
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let transport = DebuggerServer.connectPipe();
+ let client = new DebuggerClient(transport);
+
+ let deferred = defer();
+ client.connect().then(() => {
+ client.getProcess(processId)
+ .then(response => {
+ let options = {
+ form: response.form,
+ client: client,
+ chrome: true,
+ isTabActor: false
+ };
+ return TargetFactory.forRemoteTab(options);
+ })
+ .then(target => {
+ // Ensure closing the connection in order to cleanup
+ // the debugger client and also the server created in the
+ // content process
+ target.on("close", () => {
+ client.close();
+ });
+ deferred.resolve(target);
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ // Used by menus.js
+ openContentProcessToolbox: function (gBrowser) {
+ let { childCount } = Services.ppmm;
+ // Get the process message manager for the current tab
+ let mm = gBrowser.selectedBrowser.messageManager.processMessageManager;
+ let processId = null;
+ for (let i = 1; i < childCount; i++) {
+ let child = Services.ppmm.getChildAt(i);
+ if (child == mm) {
+ processId = i;
+ break;
+ }
+ }
+ if (processId) {
+ this._getContentProcessTarget(processId)
+ .then(target => {
+ // Display a new toolbox, in a new window, with debugger by default
+ return gDevTools.showToolbox(target, "jsdebugger",
+ Toolbox.HostType.WINDOW);
+ });
+ } else {
+ let msg = L10N.getStr("toolbox.noContentProcessForTab.message");
+ Services.prompt.alert(null, "", msg);
+ }
+ },
+
+ /**
+ * Install Developer widget
+ */
+ installDeveloperWidget: function () {
+ let id = "developer-button";
+ let widget = CustomizableUI.getWidget(id);
+ if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
+ return;
+ }
+ CustomizableUI.createWidget({
+ id: id,
+ type: "view",
+ viewId: "PanelUI-developer",
+ shortcutId: "key_devToolboxMenuItem",
+ tooltiptext: "developer-button.tooltiptext2",
+ defaultArea: AppConstants.MOZ_DEV_EDITION ?
+ CustomizableUI.AREA_NAVBAR :
+ CustomizableUI.AREA_PANEL,
+ onViewShowing: function (aEvent) {
+ // Populate the subview with whatever menuitems are in the developer
+ // menu. We skip menu elements, because the menu panel has no way
+ // of dealing with those right now.
+ let doc = aEvent.target.ownerDocument;
+ let win = doc.defaultView;
+
+ let menu = doc.getElementById("menuWebDeveloperPopup");
+
+ let itemsToDisplay = [...menu.children];
+ // Hardcode the addition of the "work offline" menuitem at the bottom:
+ itemsToDisplay.push({localName: "menuseparator", getAttribute: () => {}});
+ itemsToDisplay.push(doc.getElementById("goOfflineMenuitem"));
+
+ let developerItems = doc.getElementById("PanelUI-developerItems");
+ // Import private helpers from CustomizableWidgets
+ let { clearSubview, fillSubviewFromMenuItems } =
+ Cu.import("resource:///modules/CustomizableWidgets.jsm", {});
+ clearSubview(developerItems);
+ fillSubviewFromMenuItems(itemsToDisplay, developerItems);
+ },
+ onBeforeCreated: function (doc) {
+ // Bug 1223127, CUI should make this easier to do.
+ if (doc.getElementById("PanelUI-developerItems")) {
+ return;
+ }
+ let view = doc.createElement("panelview");
+ view.id = "PanelUI-developerItems";
+ let panel = doc.createElement("vbox");
+ panel.setAttribute("class", "panel-subview-body");
+ view.appendChild(panel);
+ doc.getElementById("PanelUI-multiView").appendChild(view);
+ }
+ });
+ },
+
+ /**
+ * Install WebIDE widget
+ */
+ // Used by itself
+ installWebIDEWidget: function () {
+ if (this.isWebIDEWidgetInstalled()) {
+ return;
+ }
+
+ let defaultArea;
+ if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) {
+ defaultArea = CustomizableUI.AREA_NAVBAR;
+ } else {
+ defaultArea = CustomizableUI.AREA_PANEL;
+ }
+
+ CustomizableUI.createWidget({
+ id: "webide-button",
+ shortcutId: "key_webide",
+ label: "devtools-webide-button2.label",
+ tooltiptext: "devtools-webide-button2.tooltiptext",
+ defaultArea: defaultArea,
+ onCommand: function (aEvent) {
+ gDevToolsBrowser.openWebIDE();
+ }
+ });
+ },
+
+ isWebIDEWidgetInstalled: function () {
+ let widgetWrapper = CustomizableUI.getWidget("webide-button");
+ return !!(widgetWrapper && widgetWrapper.provider == CustomizableUI.PROVIDER_API);
+ },
+
+ /**
+ * The deferred promise will be resolved by WebIDE's UI.init()
+ */
+ isWebIDEInitialized: defer(),
+
+ /**
+ * Uninstall WebIDE widget
+ */
+ uninstallWebIDEWidget: function () {
+ if (this.isWebIDEWidgetInstalled()) {
+ CustomizableUI.removeWidgetFromArea("webide-button");
+ }
+ CustomizableUI.destroyWidget("webide-button");
+ },
+
+ /**
+ * Move WebIDE widget to the navbar
+ */
+ // Used by webide.js
+ moveWebIDEWidgetInNavbar: function () {
+ CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR);
+ },
+
+ /**
+ * Add this DevTools's presence to a browser window's document
+ *
+ * @param {XULDocument} doc
+ * The document to which devtools should be hooked to.
+ */
+ _registerBrowserWindow: function (win) {
+ if (gDevToolsBrowser._trackedBrowserWindows.has(win)) {
+ return;
+ }
+ gDevToolsBrowser._trackedBrowserWindows.add(win);
+
+ BrowserMenus.addMenus(win.document);
+
+ // Register the Developer widget in the Hamburger menu or navbar
+ // only once menus are registered as it depends on it.
+ gDevToolsBrowser.installDeveloperWidget();
+
+ // Inject lazily DeveloperToolbar on the chrome window
+ loader.lazyGetter(win, "DeveloperToolbar", function () {
+ let { DeveloperToolbar } = require("devtools/client/shared/developer-toolbar");
+ return new DeveloperToolbar(win);
+ });
+
+ this.updateCommandAvailability(win);
+ this.ensurePrefObserver();
+ win.addEventListener("unload", this);
+
+ let tabContainer = win.gBrowser.tabContainer;
+ tabContainer.addEventListener("TabSelect", this, false);
+ tabContainer.addEventListener("TabOpen", this, false);
+ tabContainer.addEventListener("TabClose", this, false);
+ tabContainer.addEventListener("TabPinned", this, false);
+ tabContainer.addEventListener("TabUnpinned", this, false);
+ },
+
+ /**
+ * Hook the JS debugger tool to the "Debug Script" button of the slow script
+ * dialog.
+ */
+ setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() {
+ let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
+ .getService(Ci.nsISlowScriptDebug);
+ let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+
+ function slowScriptDebugHandler(aTab, aCallback) {
+ let target = TargetFactory.forTab(aTab);
+
+ gDevTools.showToolbox(target, "jsdebugger").then(toolbox => {
+ let threadClient = toolbox.getCurrentPanel().panelWin.gThreadClient;
+
+ // Break in place, which means resuming the debuggee thread and pausing
+ // right before the next step happens.
+ switch (threadClient.state) {
+ case "paused":
+ // When the debugger is already paused.
+ threadClient.resumeThenPause();
+ aCallback();
+ break;
+ case "attached":
+ // When the debugger is already open.
+ threadClient.interrupt(() => {
+ threadClient.resumeThenPause();
+ aCallback();
+ });
+ break;
+ case "resuming":
+ // The debugger is newly opened.
+ threadClient.addOneTimeListener("resumed", () => {
+ threadClient.interrupt(() => {
+ threadClient.resumeThenPause();
+ aCallback();
+ });
+ });
+ break;
+ default:
+ throw Error("invalid thread client state in slow script debug handler: " +
+ threadClient.state);
+ }
+ });
+ }
+
+ debugService.activationHandler = function (aWindow) {
+ let chromeWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+
+ let setupFinished = false;
+ slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab,
+ () => { setupFinished = true; });
+
+ // Don't return from the interrupt handler until the debugger is brought
+ // up; no reason to continue executing the slow script.
+ let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ utils.enterModalState();
+ while (!setupFinished) {
+ tm.currentThread.processNextEvent(true);
+ }
+ utils.leaveModalState();
+ };
+
+ debugService.remoteActivationHandler = function (aBrowser, aCallback) {
+ let chromeWindow = aBrowser.ownerDocument.defaultView;
+ let tab = chromeWindow.gBrowser.getTabForBrowser(aBrowser);
+ chromeWindow.gBrowser.selected = tab;
+
+ function callback() {
+ aCallback.finishDebuggerStartup();
+ }
+
+ slowScriptDebugHandler(tab, callback);
+ };
+ },
+
+ /**
+ * Unset the slow script debug handler.
+ */
+ unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() {
+ let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
+ .getService(Ci.nsISlowScriptDebug);
+ debugService.activationHandler = undefined;
+ },
+
+ /**
+ * Add the menuitem for a tool to all open browser windows.
+ *
+ * @param {object} toolDefinition
+ * properties of the tool to add
+ */
+ _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
+ // No menu item or global shortcut is required for options panel.
+ if (!toolDefinition.inMenu) {
+ return;
+ }
+
+ // Skip if the tool is disabled.
+ try {
+ if (toolDefinition.visibilityswitch &&
+ !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
+ return;
+ }
+ } catch (e) {}
+
+ // We need to insert the new tool in the right place, which means knowing
+ // the tool that comes before the tool that we're trying to add
+ let allDefs = gDevTools.getToolDefinitionArray();
+ let prevDef;
+ for (let def of allDefs) {
+ if (!def.inMenu) {
+ continue;
+ }
+ if (def === toolDefinition) {
+ break;
+ }
+ prevDef = def;
+ }
+
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ BrowserMenus.insertToolMenuElements(win.document, toolDefinition, prevDef);
+ }
+
+ if (toolDefinition.id === "jsdebugger") {
+ gDevToolsBrowser.setSlowScriptDebugHandler();
+ }
+ },
+
+ hasToolboxOpened: function (win) {
+ let tab = win.gBrowser.selectedTab;
+ for (let [target, toolbox] of gDevTools._toolboxes) {
+ if (target.tab == tab) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Update the "Toggle Tools" checkbox in the developer tools menu. This is
+ * called when a toolbox is created or destroyed.
+ */
+ _updateMenuCheckbox: function DT_updateMenuCheckbox() {
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+
+ let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
+
+ let menu = win.document.getElementById("menu_devToolbox");
+ if (hasToolbox) {
+ menu.setAttribute("checked", "true");
+ } else {
+ menu.removeAttribute("checked");
+ }
+ }
+ },
+
+ /**
+ * Remove the menuitem for a tool to all open browser windows.
+ *
+ * @param {string} toolId
+ * id of the tool to remove
+ */
+ _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ BrowserMenus.removeToolFromMenu(toolId, win.document);
+ }
+
+ if (toolId === "jsdebugger") {
+ gDevToolsBrowser.unsetSlowScriptDebugHandler();
+ }
+ },
+
+ /**
+ * Called on browser unload to remove menu entries, toolboxes and event
+ * listeners from the closed browser window.
+ *
+ * @param {XULWindow} win
+ * The window containing the menu entry
+ */
+ _forgetBrowserWindow: function (win) {
+ if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) {
+ return;
+ }
+ gDevToolsBrowser._trackedBrowserWindows.delete(win);
+ win.removeEventListener("unload", this);
+
+ BrowserMenus.removeMenus(win.document);
+
+ // Destroy toolboxes for closed window
+ for (let [target, toolbox] of gDevTools._toolboxes) {
+ if (toolbox.win.top == win) {
+ toolbox.destroy();
+ }
+ }
+
+ // Destroy the Developer toolbar if it has been accessed
+ let desc = Object.getOwnPropertyDescriptor(win, "DeveloperToolbar");
+ if (desc && !desc.get) {
+ win.DeveloperToolbar.destroy();
+ }
+
+ let tabContainer = win.gBrowser.tabContainer;
+ tabContainer.removeEventListener("TabSelect", this, false);
+ tabContainer.removeEventListener("TabOpen", this, false);
+ tabContainer.removeEventListener("TabClose", this, false);
+ tabContainer.removeEventListener("TabPinned", this, false);
+ tabContainer.removeEventListener("TabUnpinned", this, false);
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "TabOpen":
+ case "TabClose":
+ case "TabPinned":
+ case "TabUnpinned":
+ let open = 0;
+ let pinned = 0;
+
+ for (let win of this._trackedBrowserWindows) {
+ let tabContainer = win.gBrowser.tabContainer;
+ let numPinnedTabs = win.gBrowser._numPinnedTabs || 0;
+ let numTabs = tabContainer.itemCount - numPinnedTabs;
+
+ open += numTabs;
+ pinned += numPinnedTabs;
+ }
+
+ this._tabStats.histOpen.push(open);
+ this._tabStats.histPinned.push(pinned);
+ this._tabStats.peakOpen = Math.max(open, this._tabStats.peakOpen);
+ this._tabStats.peakPinned = Math.max(pinned, this._tabStats.peakPinned);
+ break;
+ case "TabSelect":
+ gDevToolsBrowser._updateMenuCheckbox();
+ break;
+ case "unload":
+ // top-level browser window unload
+ gDevToolsBrowser._forgetBrowserWindow(event.target.defaultView);
+ break;
+ }
+ },
+
+ _pingTelemetry: function () {
+ let mean = function (arr) {
+ if (arr.length === 0) {
+ return 0;
+ }
+
+ let total = arr.reduce((a, b) => a + b);
+ return Math.ceil(total / arr.length);
+ };
+
+ let tabStats = gDevToolsBrowser._tabStats;
+ this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen);
+ this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen));
+ this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned);
+ this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned));
+ },
+
+ /**
+ * All browser windows have been closed, tidy up remaining objects.
+ */
+ destroy: function () {
+ Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
+ Services.obs.removeObserver(gDevToolsBrowser, "browser-delayed-startup-finished");
+ Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
+
+ gDevToolsBrowser._pingTelemetry();
+ gDevToolsBrowser._telemetry = null;
+
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ gDevToolsBrowser._forgetBrowserWindow(win);
+ }
+ },
+};
+
+// Handle all already registered tools,
+gDevTools.getToolDefinitionArray()
+ .forEach(def => gDevToolsBrowser._addToolToWindows(def));
+// and the new ones.
+gDevTools.on("tool-registered", function (ev, toolId) {
+ let toolDefinition = gDevTools._tools.get(toolId);
+ gDevToolsBrowser._addToolToWindows(toolDefinition);
+});
+
+gDevTools.on("tool-unregistered", function (ev, toolId) {
+ if (typeof toolId != "string") {
+ toolId = toolId.id;
+ }
+ gDevToolsBrowser._removeToolFromWindows(toolId);
+});
+
+gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
+gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
+
+Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
+Services.obs.addObserver(gDevToolsBrowser, "browser-delayed-startup-finished", false);
+
+// Fake end of browser window load event for all already opened windows
+// that is already fully loaded.
+let enumerator = Services.wm.getEnumerator(gDevTools.chromeWindowType);
+while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext();
+ if (win.gBrowserInit && win.gBrowserInit.delayedStartupFinished) {
+ gDevToolsBrowser._registerBrowserWindow(win);
+ }
+}
+
+// Watch for module loader unload. Fires when the tools are reloaded.
+unload(function () {
+ gDevToolsBrowser.destroy();
+});
diff --git a/devtools/client/framework/devtools.js b/devtools/client/framework/devtools.js
new file mode 100644
index 000000000..90f88023b
--- /dev/null
+++ b/devtools/client/framework/devtools.js
@@ -0,0 +1,534 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+
+// Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized
+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);
+
+const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} =
+ require("devtools/client/definitions");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {JsonView} = require("devtools/client/jsonview/main");
+const AboutDevTools = require("devtools/client/framework/about-devtools-toolbox");
+const {when: unload} = require("sdk/system/unload");
+const {Task} = require("devtools/shared/task");
+
+const FORBIDDEN_IDS = new Set(["toolbox", ""]);
+const MAX_ORDINAL = 99;
+
+/**
+ * DevTools is a class that represents a set of developer tools, it holds a
+ * set of tools and keeps track of open toolboxes in the browser.
+ */
+this.DevTools = function DevTools() {
+ this._tools = new Map(); // Map<toolId, tool>
+ this._themes = new Map(); // Map<themeId, theme>
+ this._toolboxes = new Map(); // Map<target, toolbox>
+ // List of toolboxes that are still in process of creation
+ this._creatingToolboxes = new Map(); // Map<target, toolbox Promise>
+
+ // destroy() is an observer's handler so we need to preserve context.
+ this.destroy = this.destroy.bind(this);
+
+ // JSON Viewer for 'application/json' documents.
+ JsonView.initialize();
+
+ AboutDevTools.register();
+
+ EventEmitter.decorate(this);
+
+ Services.obs.addObserver(this.destroy, "quit-application", false);
+
+ // This is important step in initialization codepath where we are going to
+ // start registering all default tools and themes: create menuitems, keys, emit
+ // related events.
+ this.registerDefaults();
+};
+
+DevTools.prototype = {
+ // The windowtype of the main window, used in various tools. This may be set
+ // to something different by other gecko apps.
+ chromeWindowType: "navigator:browser",
+
+ registerDefaults() {
+ // Ensure registering items in the sorted order (getDefault* functions
+ // return sorted lists)
+ this.getDefaultTools().forEach(definition => this.registerTool(definition));
+ this.getDefaultThemes().forEach(definition => this.registerTheme(definition));
+ },
+
+ unregisterDefaults() {
+ for (let definition of this.getToolDefinitionArray()) {
+ this.unregisterTool(definition.id);
+ }
+ for (let definition of this.getThemeDefinitionArray()) {
+ this.unregisterTheme(definition.id);
+ }
+ },
+
+ /**
+ * Register a new developer tool.
+ *
+ * A definition is a light object that holds different information about a
+ * developer tool. This object is not supposed to have any operational code.
+ * See it as a "manifest".
+ * The only actual code lives in the build() function, which will be used to
+ * start an instance of this tool.
+ *
+ * Each toolDefinition has the following properties:
+ * - id: Unique identifier for this tool (string|required)
+ * - visibilityswitch: Property name to allow us to hide this tool from the
+ * DevTools Toolbox.
+ * A falsy value indicates that it cannot be hidden.
+ * - icon: URL pointing to a graphic which will be used as the src for an
+ * 16x16 img tag (string|required)
+ * - invertIconForLightTheme: The icon can automatically have an inversion
+ * filter applied (default is false). All builtin tools are true, but
+ * addons may omit this to prevent unwanted changes to the `icon`
+ * image. filter: invert(1) is applied to the image (boolean|optional)
+ * - url: URL pointing to a XUL/XHTML document containing the user interface
+ * (string|required)
+ * - label: Localized name for the tool to be displayed to the user
+ * (string|required)
+ * - hideInOptions: Boolean indicating whether or not this tool should be
+ shown in toolbox options or not. Defaults to false.
+ * (boolean)
+ * - build: Function that takes an iframe, which has been populated with the
+ * markup from |url|, and also the toolbox containing the panel.
+ * And returns an instance of ToolPanel (function|required)
+ */
+ registerTool: function DT_registerTool(toolDefinition) {
+ let toolId = toolDefinition.id;
+
+ if (!toolId || FORBIDDEN_IDS.has(toolId)) {
+ throw new Error("Invalid definition.id");
+ }
+
+ // Make sure that additional tools will always be able to be hidden.
+ // When being called from main.js, defaultTools has not yet been exported.
+ // But, we can assume that in this case, it is a default tool.
+ if (DefaultTools.indexOf(toolDefinition) == -1) {
+ toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
+ }
+
+ this._tools.set(toolId, toolDefinition);
+
+ this.emit("tool-registered", toolId);
+ },
+
+ /**
+ * Removes all tools that match the given |toolId|
+ * Needed so that add-ons can remove themselves when they are deactivated
+ *
+ * @param {string|object} tool
+ * Definition or the id of the tool to unregister. Passing the
+ * tool id should be avoided as it is a temporary measure.
+ * @param {boolean} isQuitApplication
+ * true to indicate that the call is due to app quit, so we should not
+ * cause a cascade of costly events
+ */
+ unregisterTool: function DT_unregisterTool(tool, isQuitApplication) {
+ let toolId = null;
+ if (typeof tool == "string") {
+ toolId = tool;
+ tool = this._tools.get(tool);
+ }
+ else {
+ toolId = tool.id;
+ }
+ this._tools.delete(toolId);
+
+ if (!isQuitApplication) {
+ this.emit("tool-unregistered", tool);
+ }
+ },
+
+ /**
+ * Sorting function used for sorting tools based on their ordinals.
+ */
+ ordinalSort: function DT_ordinalSort(d1, d2) {
+ let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL;
+ let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL;
+ return o1 - o2;
+ },
+
+ getDefaultTools: function DT_getDefaultTools() {
+ return DefaultTools.sort(this.ordinalSort);
+ },
+
+ getAdditionalTools: function DT_getAdditionalTools() {
+ let tools = [];
+ for (let [key, value] of this._tools) {
+ if (DefaultTools.indexOf(value) == -1) {
+ tools.push(value);
+ }
+ }
+ return tools.sort(this.ordinalSort);
+ },
+
+ getDefaultThemes() {
+ return DefaultThemes.sort(this.ordinalSort);
+ },
+
+ /**
+ * Get a tool definition if it exists and is enabled.
+ *
+ * @param {string} toolId
+ * The id of the tool to show
+ *
+ * @return {ToolDefinition|null} tool
+ * The ToolDefinition for the id or null.
+ */
+ getToolDefinition: function DT_getToolDefinition(toolId) {
+ let tool = this._tools.get(toolId);
+ if (!tool) {
+ return null;
+ } else if (!tool.visibilityswitch) {
+ return tool;
+ }
+
+ let enabled;
+ try {
+ enabled = Services.prefs.getBoolPref(tool.visibilityswitch);
+ } catch (e) {
+ enabled = true;
+ }
+
+ return enabled ? tool : null;
+ },
+
+ /**
+ * Allow ToolBoxes to get at the list of tools that they should populate
+ * themselves with.
+ *
+ * @return {Map} tools
+ * A map of the the tool definitions registered in this instance
+ */
+ getToolDefinitionMap: function DT_getToolDefinitionMap() {
+ let tools = new Map();
+
+ for (let [id, definition] of this._tools) {
+ if (this.getToolDefinition(id)) {
+ tools.set(id, definition);
+ }
+ }
+
+ return tools;
+ },
+
+ /**
+ * Tools have an inherent ordering that can't be represented in a Map so
+ * getToolDefinitionArray provides an alternative representation of the
+ * definitions sorted by ordinal value.
+ *
+ * @return {Array} tools
+ * A sorted array of the tool definitions registered in this instance
+ */
+ getToolDefinitionArray: function DT_getToolDefinitionArray() {
+ let definitions = [];
+
+ for (let [id, definition] of this._tools) {
+ if (this.getToolDefinition(id)) {
+ definitions.push(definition);
+ }
+ }
+
+ return definitions.sort(this.ordinalSort);
+ },
+
+ /**
+ * Register a new theme for developer tools toolbox.
+ *
+ * A definition is a light object that holds various information about a
+ * theme.
+ *
+ * Each themeDefinition has the following properties:
+ * - id: Unique identifier for this theme (string|required)
+ * - label: Localized name for the theme to be displayed to the user
+ * (string|required)
+ * - stylesheets: Array of URLs pointing to a CSS document(s) containing
+ * the theme style rules (array|required)
+ * - classList: Array of class names identifying the theme within a document.
+ * These names are set to document element when applying
+ * the theme (array|required)
+ * - onApply: Function that is executed by the framework when the theme
+ * is applied. The function takes the current iframe window
+ * and the previous theme id as arguments (function)
+ * - onUnapply: Function that is executed by the framework when the theme
+ * is unapplied. The function takes the current iframe window
+ * and the new theme id as arguments (function)
+ */
+ registerTheme: function DT_registerTheme(themeDefinition) {
+ let themeId = themeDefinition.id;
+
+ if (!themeId) {
+ throw new Error("Invalid theme id");
+ }
+
+ if (this._themes.get(themeId)) {
+ throw new Error("Theme with the same id is already registered");
+ }
+
+ this._themes.set(themeId, themeDefinition);
+
+ this.emit("theme-registered", themeId);
+ },
+
+ /**
+ * Removes an existing theme from the list of registered themes.
+ * Needed so that add-ons can remove themselves when they are deactivated
+ *
+ * @param {string|object} theme
+ * Definition or the id of the theme to unregister.
+ */
+ unregisterTheme: function DT_unregisterTheme(theme) {
+ let themeId = null;
+ if (typeof theme == "string") {
+ themeId = theme;
+ theme = this._themes.get(theme);
+ }
+ else {
+ themeId = theme.id;
+ }
+
+ let currTheme = Services.prefs.getCharPref("devtools.theme");
+
+ // Note that we can't check if `theme` is an item
+ // of `DefaultThemes` as we end up reloading definitions
+ // module and end up with different theme objects
+ let isCoreTheme = DefaultThemes.some(t => t.id === themeId);
+
+ // Reset the theme if an extension theme that's currently applied
+ // is being removed.
+ // Ignore shutdown since addons get disabled during that time.
+ if (!Services.startup.shuttingDown &&
+ !isCoreTheme &&
+ theme.id == currTheme) {
+ Services.prefs.setCharPref("devtools.theme", "light");
+
+ let data = {
+ pref: "devtools.theme",
+ newValue: "light",
+ oldValue: currTheme
+ };
+
+ this.emit("pref-changed", data);
+
+ this.emit("theme-unregistered", theme);
+ }
+
+ this._themes.delete(themeId);
+ },
+
+ /**
+ * Get a theme definition if it exists.
+ *
+ * @param {string} themeId
+ * The id of the theme
+ *
+ * @return {ThemeDefinition|null} theme
+ * The ThemeDefinition for the id or null.
+ */
+ getThemeDefinition: function DT_getThemeDefinition(themeId) {
+ let theme = this._themes.get(themeId);
+ if (!theme) {
+ return null;
+ }
+ return theme;
+ },
+
+ /**
+ * Get map of registered themes.
+ *
+ * @return {Map} themes
+ * A map of the the theme definitions registered in this instance
+ */
+ getThemeDefinitionMap: function DT_getThemeDefinitionMap() {
+ let themes = new Map();
+
+ for (let [id, definition] of this._themes) {
+ if (this.getThemeDefinition(id)) {
+ themes.set(id, definition);
+ }
+ }
+
+ return themes;
+ },
+
+ /**
+ * Get registered themes definitions sorted by ordinal value.
+ *
+ * @return {Array} themes
+ * A sorted array of the theme definitions registered in this instance
+ */
+ getThemeDefinitionArray: function DT_getThemeDefinitionArray() {
+ let definitions = [];
+
+ for (let [id, definition] of this._themes) {
+ if (this.getThemeDefinition(id)) {
+ definitions.push(definition);
+ }
+ }
+
+ return definitions.sort(this.ordinalSort);
+ },
+
+ /**
+ * Show a Toolbox for a target (either by creating a new one, or if a toolbox
+ * already exists for the target, by bring to the front the existing one)
+ * If |toolId| is specified then the displayed toolbox will have the
+ * specified tool selected.
+ * If |hostType| is specified then the toolbox will be displayed using the
+ * specified HostType.
+ *
+ * @param {Target} target
+ * The target the toolbox will debug
+ * @param {string} toolId
+ * The id of the tool to show
+ * @param {Toolbox.HostType} hostType
+ * The type of host (bottom, window, side)
+ * @param {object} hostOptions
+ * Options for host specifically
+ *
+ * @return {Toolbox} toolbox
+ * The toolbox that was opened
+ */
+ showToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
+ let toolbox = this._toolboxes.get(target);
+ if (toolbox) {
+
+ if (hostType != null && toolbox.hostType != hostType) {
+ yield toolbox.switchHost(hostType);
+ }
+
+ if (toolId != null && toolbox.currentToolId != toolId) {
+ yield toolbox.selectTool(toolId);
+ }
+
+ toolbox.raise();
+ } else {
+ // As toolbox object creation is async, we have to be careful about races
+ // Check for possible already in process of loading toolboxes before
+ // actually trying to create a new one.
+ let promise = this._creatingToolboxes.get(target);
+ if (promise) {
+ return yield promise;
+ }
+ let toolboxPromise = this.createToolbox(target, toolId, hostType, hostOptions);
+ this._creatingToolboxes.set(target, toolboxPromise);
+ toolbox = yield toolboxPromise;
+ this._creatingToolboxes.delete(target);
+ }
+ return toolbox;
+ }),
+
+ createToolbox: Task.async(function* (target, toolId, hostType, hostOptions) {
+ let manager = new ToolboxHostManager(target, hostType, hostOptions);
+
+ let toolbox = yield manager.create(toolId);
+
+ this._toolboxes.set(target, toolbox);
+
+ this.emit("toolbox-created", toolbox);
+
+ toolbox.once("destroy", () => {
+ this.emit("toolbox-destroy", target);
+ });
+
+ toolbox.once("destroyed", () => {
+ this._toolboxes.delete(target);
+ this.emit("toolbox-destroyed", target);
+ });
+
+ yield toolbox.open();
+ this.emit("toolbox-ready", toolbox);
+
+ return toolbox;
+ }),
+
+ /**
+ * Return the toolbox for a given target.
+ *
+ * @param {object} target
+ * Target value e.g. the target that owns this toolbox
+ *
+ * @return {Toolbox} toolbox
+ * The toolbox that is debugging the given target
+ */
+ getToolbox: function DT_getToolbox(target) {
+ return this._toolboxes.get(target);
+ },
+
+ /**
+ * Close the toolbox for a given target
+ *
+ * @return promise
+ * This promise will resolve to false if no toolbox was found
+ * associated to the target. true, if the toolbox was successfully
+ * closed.
+ */
+ closeToolbox: Task.async(function* (target) {
+ let toolbox = yield this._creatingToolboxes.get(target);
+ if (!toolbox) {
+ toolbox = this._toolboxes.get(target);
+ }
+ if (!toolbox) {
+ return false;
+ }
+ yield toolbox.destroy();
+ return true;
+ }),
+
+ /**
+ * Called to tear down a tools provider.
+ */
+ _teardown: function DT_teardown() {
+ for (let [target, toolbox] of this._toolboxes) {
+ toolbox.destroy();
+ }
+ AboutDevTools.unregister();
+ },
+
+ /**
+ * All browser windows have been closed, tidy up remaining objects.
+ */
+ destroy: function () {
+ Services.obs.removeObserver(this.destroy, "quit-application");
+
+ for (let [key, tool] of this.getToolDefinitionMap()) {
+ this.unregisterTool(key, true);
+ }
+
+ JsonView.destroy();
+
+ gDevTools.unregisterDefaults();
+
+ // Cleaning down the toolboxes: i.e.
+ // for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
+ // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
+ },
+
+ /**
+ * Iterator that yields each of the toolboxes.
+ */
+ *[Symbol.iterator ]() {
+ for (let toolbox of this._toolboxes) {
+ yield toolbox;
+ }
+ }
+};
+
+const gDevTools = exports.gDevTools = new DevTools();
+
+// Watch for module loader unload. Fires when the tools are reloaded.
+unload(function () {
+ gDevTools._teardown();
+});
diff --git a/devtools/client/framework/gDevTools.jsm b/devtools/client/framework/gDevTools.jsm
new file mode 100644
index 000000000..d825c0eaa
--- /dev/null
+++ b/devtools/client/framework/gDevTools.jsm
@@ -0,0 +1,162 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 JSM is here to keep some compatibility with existing add-ons.
+ * Please now use the modules:
+ * - devtools/client/framework/devtools for gDevTools
+ * - devtools/client/framework/devtools-browser for gDevToolsBrowser
+ */
+
+this.EXPORTED_SYMBOLS = [ "gDevTools", "gDevToolsBrowser" ];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+/**
+ * Do not directly map to the commonjs modules so that callsites of
+ * gDevTools.jsm do not have to do anything to access to the very last version
+ * of the module. The `devtools` and `browser` getter are always going to
+ * retrieve the very last version of the modules.
+ */
+Object.defineProperty(this, "require", {
+ get() {
+ let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ return require;
+ }
+});
+Object.defineProperty(this, "devtools", {
+ get() {
+ return require("devtools/client/framework/devtools").gDevTools;
+ }
+});
+Object.defineProperty(this, "browser", {
+ get() {
+ return require("devtools/client/framework/devtools-browser").gDevToolsBrowser;
+ }
+});
+
+/**
+ * gDevTools is a singleton that controls the Firefox Developer Tools.
+ *
+ * It is an instance of a DevTools class that holds a set of tools. It has the
+ * same lifetime as the browser.
+ */
+let gDevToolsMethods = [
+ // Used by the reload addon.
+ // Force reloading dependencies if the loader happens to have reloaded.
+ "reload",
+
+ // Used by: - b2g desktop.js
+ // - nsContextMenu
+ // - /devtools code
+ "showToolbox",
+
+ // Used by Addon SDK and /devtools
+ "closeToolbox",
+ "getToolbox",
+
+ // Used by Addon SDK, main.js and tests:
+ "registerTool",
+ "registerTheme",
+ "unregisterTool",
+ "unregisterTheme",
+
+ // Used by main.js and test
+ "getToolDefinitionArray",
+ "getThemeDefinitionArray",
+
+ // Used by theme-switching.js
+ "getThemeDefinition",
+ "emit",
+
+ // Used by /devtools
+ "on",
+ "off",
+ "once",
+
+ // Used by tests
+ "getToolDefinitionMap",
+ "getThemeDefinitionMap",
+ "getDefaultTools",
+ "getAdditionalTools",
+ "getToolDefinition",
+];
+this.gDevTools = {
+ // Used by tests
+ get _toolboxes() {
+ return devtools._toolboxes;
+ },
+ get _tools() {
+ return devtools._tools;
+ },
+ *[Symbol.iterator ]() {
+ for (let toolbox of this._toolboxes) {
+ yield toolbox;
+ }
+ }
+};
+gDevToolsMethods.forEach(name => {
+ this.gDevTools[name] = (...args) => {
+ return devtools[name].apply(devtools, args);
+ };
+});
+
+
+/**
+ * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
+ * Firefox instance.
+ */
+let gDevToolsBrowserMethods = [
+ // used by browser-sets.inc, command
+ "toggleToolboxCommand",
+
+ // Used by browser.js itself, by setting a oncommand string...
+ "selectToolCommand",
+
+ // Used by browser-sets.inc, command
+ "openAboutDebugging",
+
+ // Used by browser-sets.inc, command
+ "openConnectScreen",
+
+ // Used by browser-sets.inc, command
+ // itself, webide widget
+ "openWebIDE",
+
+ // Used by browser-sets.inc, command
+ "openContentProcessToolbox",
+
+ // Used by webide.js
+ "moveWebIDEWidgetInNavbar",
+
+ // Used by browser.js
+ "registerBrowserWindow",
+
+ // Used by reload addon
+ "hasToolboxOpened",
+
+ // Used by browser.js
+ "forgetBrowserWindow"
+];
+this.gDevToolsBrowser = {
+ // Used by webide.js
+ get isWebIDEInitialized() {
+ return browser.isWebIDEInitialized;
+ },
+ // Used by a test (should be removed)
+ get _trackedBrowserWindows() {
+ return browser._trackedBrowserWindows;
+ }
+};
+gDevToolsBrowserMethods.forEach(name => {
+ this.gDevToolsBrowser[name] = (...args) => {
+ return browser[name].apply(browser, args);
+ };
+});
diff --git a/devtools/client/framework/location-store.js b/devtools/client/framework/location-store.js
new file mode 100644
index 000000000..96deb0a99
--- /dev/null
+++ b/devtools/client/framework/location-store.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const SOURCE_TOKEN = "<:>";
+
+function LocationStore (store) {
+ this._store = store || new Map();
+}
+
+/**
+ * Method to get a promised location from the Store.
+ * @param location
+ * @returns Promise<Object>
+ */
+LocationStore.prototype.get = function (location) {
+ this._safeAccessInit(location.url);
+ return this._store.get(location.url).get(location);
+};
+
+/**
+ * Method to set a promised location to the Store
+ * @param location
+ * @param promisedLocation
+ */
+LocationStore.prototype.set = function (location, promisedLocation = null) {
+ this._safeAccessInit(location.url);
+ this._store.get(location.url).set(serialize(location), promisedLocation);
+};
+
+/**
+ * Utility method to verify if key exists in Store before accessing it.
+ * If not, initializing it.
+ * @param url
+ * @private
+ */
+LocationStore.prototype._safeAccessInit = function (url) {
+ if (!this._store.has(url)) {
+ this._store.set(url, new Map());
+ }
+};
+
+/**
+ * Utility proxy method to Map.clear() method
+ */
+LocationStore.prototype.clear = function () {
+ this._store.clear();
+};
+
+/**
+ * Retrieves an object containing all locations to be resolved when `source-updated`
+ * event is triggered.
+ * @param url
+ * @returns {Array<String>}
+ */
+LocationStore.prototype.getByURL = function (url){
+ if (this._store.has(url)) {
+ return [...this._store.get(url).keys()];
+ }
+ return [];
+};
+
+/**
+ * Invalidates the stale location promises from the store when `source-updated`
+ * event is triggered, and when FrameView unsubscribes from a location.
+ * @param url
+ */
+LocationStore.prototype.clearByURL = function (url) {
+ this._safeAccessInit(url);
+ this._store.set(url, new Map());
+};
+
+exports.LocationStore = LocationStore;
+exports.serialize = serialize;
+exports.deserialize = deserialize;
+
+/**
+ * Utility method to serialize the source
+ * @param source
+ * @returns {string}
+ */
+function serialize(source) {
+ let { url, line, column } = source;
+ line = line || 0;
+ column = column || 0;
+ return `${url}${SOURCE_TOKEN}${line}${SOURCE_TOKEN}${column}`;
+};
+
+/**
+ * Utility method to serialize the source
+ * @param source
+ * @returns Object
+ */
+function deserialize(source) {
+ let [ url, line, column ] = source.split(SOURCE_TOKEN);
+ line = parseInt(line);
+ column = parseInt(column);
+ if (column === 0) {
+ return { url, line };
+ }
+ return { url, line, column };
+};
diff --git a/devtools/client/framework/menu-item.js b/devtools/client/framework/menu-item.js
new file mode 100644
index 000000000..f6afefa41
--- /dev/null
+++ b/devtools/client/framework/menu-item.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+/**
+ * A partial implementation of the MenuItem API provided by electron:
+ * https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
+ *
+ * Missing features:
+ * - id String - Unique within a single menu. If defined then it can be used
+ * as a reference to this item by the position attribute.
+ * - role String - Define the action of the menu item; when specified the
+ * click property will be ignored
+ * - sublabel String
+ * - accelerator Accelerator
+ * - icon NativeImage
+ * - position String - This field allows fine-grained definition of the
+ * specific location within a given menu.
+ *
+ * Implemented features:
+ * @param Object options
+ * Function click
+ * Will be called with click(menuItem, browserWindow) when the menu item
+ * is clicked
+ * String type
+ * Can be normal, separator, submenu, checkbox or radio
+ * String label
+ * Boolean enabled
+ * If false, the menu item will be greyed out and unclickable.
+ * Boolean checked
+ * Should only be specified for checkbox or radio type menu items.
+ * Menu submenu
+ * Should be specified for submenu type menu items. If submenu is specified,
+ * the type: 'submenu' can be omitted. If the value is not a Menu then it
+ * will be automatically converted to one using Menu.buildFromTemplate.
+ * Boolean visible
+ * If false, the menu item will be entirely hidden.
+ */
+function MenuItem({
+ accesskey = null,
+ checked = false,
+ click = () => {},
+ disabled = false,
+ label = "",
+ id = null,
+ submenu = null,
+ type = "normal",
+ visible = true,
+} = { }) {
+ this.accesskey = accesskey;
+ this.checked = checked;
+ this.click = click;
+ this.disabled = disabled;
+ this.id = id;
+ this.label = label;
+ this.submenu = submenu;
+ this.type = type;
+ this.visible = visible;
+}
+
+module.exports = MenuItem;
diff --git a/devtools/client/framework/menu.js b/devtools/client/framework/menu.js
new file mode 100644
index 000000000..c96dbc2c7
--- /dev/null
+++ b/devtools/client/framework/menu.js
@@ -0,0 +1,173 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * A partial implementation of the Menu API provided by electron:
+ * https://github.com/electron/electron/blob/master/docs/api/menu.md.
+ *
+ * Extra features:
+ * - Emits an 'open' and 'close' event when the menu is opened/closed
+
+ * @param String id (non standard)
+ * Needed so tests can confirm the XUL implementation is working
+ */
+function Menu({ id = null } = {}) {
+ this.menuitems = [];
+ this.id = id;
+
+ Object.defineProperty(this, "items", {
+ get() {
+ return this.menuitems;
+ }
+ });
+
+ EventEmitter.decorate(this);
+}
+
+/**
+ * Add an item to the end of the Menu
+ *
+ * @param {MenuItem} menuItem
+ */
+Menu.prototype.append = function (menuItem) {
+ this.menuitems.push(menuItem);
+};
+
+/**
+ * Add an item to a specified position in the menu
+ *
+ * @param {int} pos
+ * @param {MenuItem} menuItem
+ */
+Menu.prototype.insert = function (pos, menuItem) {
+ throw Error("Not implemented");
+};
+
+/**
+ * Show the Menu at a specified location on the screen
+ *
+ * Missing features:
+ * - browserWindow - BrowserWindow (optional) - Default is null.
+ * - positioningItem Number - (optional) OS X
+ *
+ * @param {int} screenX
+ * @param {int} screenY
+ * @param Toolbox toolbox (non standard)
+ * Needed so we in which window to inject XUL
+ */
+Menu.prototype.popup = function (screenX, screenY, toolbox) {
+ let doc = toolbox.doc;
+ let popupset = doc.querySelector("popupset");
+ // See bug 1285229, on Windows, opening the same popup multiple times in a
+ // row ends up duplicating the popup. The newly inserted popup doesn't
+ // dismiss the old one. So remove any previously displayed popup before
+ // opening a new one.
+ let popup = popupset.querySelector("menupopup[menu-api=\"true\"]");
+ if (popup) {
+ popup.hidePopup();
+ }
+
+ popup = doc.createElement("menupopup");
+ popup.setAttribute("menu-api", "true");
+
+ if (this.id) {
+ popup.id = this.id;
+ }
+ this._createMenuItems(popup);
+
+ // Remove the menu from the DOM once it's hidden.
+ popup.addEventListener("popuphidden", (e) => {
+ if (e.target === popup) {
+ popup.remove();
+ this.emit("close");
+ }
+ });
+
+ popup.addEventListener("popupshown", (e) => {
+ if (e.target === popup) {
+ this.emit("open");
+ }
+ });
+
+ popupset.appendChild(popup);
+ popup.openPopupAtScreen(screenX, screenY, true);
+};
+
+Menu.prototype._createMenuItems = function (parent) {
+ let doc = parent.ownerDocument;
+ this.menuitems.forEach(item => {
+ if (!item.visible) {
+ return;
+ }
+
+ if (item.submenu) {
+ let menupopup = doc.createElement("menupopup");
+ item.submenu._createMenuItems(menupopup);
+
+ let menu = doc.createElement("menu");
+ menu.appendChild(menupopup);
+ menu.setAttribute("label", item.label);
+ if (item.disabled) {
+ menu.setAttribute("disabled", "true");
+ }
+ if (item.accesskey) {
+ menu.setAttribute("accesskey", item.accesskey);
+ }
+ if (item.id) {
+ menu.id = item.id;
+ }
+ parent.appendChild(menu);
+ } else if (item.type === "separator") {
+ let menusep = doc.createElement("menuseparator");
+ parent.appendChild(menusep);
+ } else {
+ let menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("label", item.label);
+ menuitem.addEventListener("command", () => {
+ item.click();
+ });
+
+ if (item.type === "checkbox") {
+ menuitem.setAttribute("type", "checkbox");
+ }
+ if (item.type === "radio") {
+ menuitem.setAttribute("type", "radio");
+ }
+ if (item.disabled) {
+ menuitem.setAttribute("disabled", "true");
+ }
+ if (item.checked) {
+ menuitem.setAttribute("checked", "true");
+ }
+ if (item.accesskey) {
+ menuitem.setAttribute("accesskey", item.accesskey);
+ }
+ if (item.id) {
+ menuitem.id = item.id;
+ }
+
+ parent.appendChild(menuitem);
+ }
+ });
+};
+
+Menu.setApplicationMenu = () => {
+ throw Error("Not implemented");
+};
+
+Menu.sendActionToFirstResponder = () => {
+ throw Error("Not implemented");
+};
+
+Menu.buildFromTemplate = () => {
+ throw Error("Not implemented");
+};
+
+module.exports = Menu;
diff --git a/devtools/client/framework/moz.build b/devtools/client/framework/moz.build
new file mode 100644
index 000000000..7b28b4b9e
--- /dev/null
+++ b/devtools/client/framework/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [
+ 'test/shared-redux-head.js',
+]
+
+DevToolsModules(
+ 'about-devtools-toolbox.js',
+ 'attach-thread.js',
+ 'browser-menus.js',
+ 'devtools-browser.js',
+ 'devtools.js',
+ 'gDevTools.jsm',
+ 'location-store.js',
+ 'menu-item.js',
+ 'menu.js',
+ 'selection.js',
+ 'sidebar.js',
+ 'source-map-service.js',
+ 'target-from-url.js',
+ 'target.js',
+ 'toolbox-highlighter-utils.js',
+ 'toolbox-host-manager.js',
+ 'toolbox-hosts.js',
+ 'toolbox-options.js',
+ 'toolbox.js',
+ 'ToolboxProcess.jsm',
+)
diff --git a/devtools/client/framework/options-panel.css b/devtools/client/framework/options-panel.css
new file mode 100644
index 000000000..4aad29e7b
--- /dev/null
+++ b/devtools/client/framework/options-panel.css
@@ -0,0 +1,107 @@
+/* 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/. */
+:root{
+ -moz-user-select: none;
+}
+
+#options-panel-container {
+ overflow: auto;
+}
+
+#options-panel {
+ display: block;
+}
+
+.options-vertical-pane {
+ display: inline;
+ float: left;
+}
+
+.options-vertical-pane {
+ margin: 5px;
+ width: calc(100%/3 - 10px);
+ min-width: 320px;
+ padding-inline-start: 5px;
+ box-sizing: border-box;
+}
+
+/* Snap to 50% width once there is not room for 3 columns anymore.
+ This prevents having 2 columns showing in a row, but taking up
+ only ~66% of the available space. */
+@media (max-width: 1000px) {
+ .options-vertical-pane {
+ width: calc(100%/2 - 10px);
+ }
+}
+
+.options-vertical-pane fieldset {
+ border: none;
+}
+
+.options-vertical-pane fieldset legend {
+ font-size: 1.4rem;
+ margin-inline-start: -15px;
+ margin-bottom: 3px;
+ cursor: default;
+}
+
+.options-vertical-pane fieldset + fieldset {
+ margin-top: 1rem;
+}
+
+.options-groupbox {
+ margin-inline-start: 15px;
+ padding: 2px;
+}
+
+.options-groupbox label {
+ display: flex;
+ padding: 4px 0;
+ align-items: center;
+}
+
+/* Add padding for label of select inputs in order to
+ align it with surrounding checkboxes */
+.options-groupbox label span:first-child {
+ padding-inline-start: 5px;
+}
+
+.options-groupbox label span + select {
+ margin-inline-start: 4px;
+}
+
+.options-groupbox.horizontal-options-groupbox label {
+ display: inline-flex;
+ align-items: flex-end;
+}
+
+.options-groupbox.horizontal-options-groupbox label + label {
+ margin-inline-start: 4px;
+}
+
+.options-groupbox > *,
+.options-groupbox > .hidden-labels-box > checkbox {
+ padding: 2px;
+}
+
+.options-groupbox > .hidden-labels-box {
+ padding: 0;
+}
+
+.options-citation-label {
+ display: inline-block;
+ font-size: 1rem;
+ font-style: italic;
+ /* To align it with the checkbox */
+ padding: 4px 0 0;
+ padding-inline-end: 4px;
+}
+
+#devtools-sourceeditor-keybinding-select {
+ min-width: 130px;
+}
+
+#devtools-sourceeditor-tabsize-select {
+ min-width: 80px;
+}
diff --git a/devtools/client/framework/selection.js b/devtools/client/framework/selection.js
new file mode 100644
index 000000000..8125f8508
--- /dev/null
+++ b/devtools/client/framework/selection.js
@@ -0,0 +1,247 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const nodeConstants = require("devtools/shared/dom-node-constants");
+var EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * API
+ *
+ * new Selection(walker=null)
+ * destroy()
+ * node (readonly)
+ * setNode(node, origin="unknown")
+ *
+ * Helpers:
+ *
+ * window
+ * document
+ * isRoot()
+ * isNode()
+ * isHTMLNode()
+ *
+ * Check the nature of the node:
+ *
+ * isElementNode()
+ * isAttributeNode()
+ * isTextNode()
+ * isCDATANode()
+ * isEntityRefNode()
+ * isEntityNode()
+ * isProcessingInstructionNode()
+ * isCommentNode()
+ * isDocumentNode()
+ * isDocumentTypeNode()
+ * isDocumentFragmentNode()
+ * isNotationNode()
+ *
+ * Events:
+ * "new-node-front" when the inner node changed
+ * "attribute-changed" when an attribute is changed
+ * "detached-front" when the node (or one of its parents) is removed from
+ * the document
+ * "reparented" when the node (or one of its parents) is moved under
+ * a different node
+ */
+
+/**
+ * A Selection object. Hold a reference to a node.
+ * Includes some helpers, fire some helpful events.
+ */
+function Selection(walker) {
+ EventEmitter.decorate(this);
+
+ this._onMutations = this._onMutations.bind(this);
+ this.setWalker(walker);
+}
+
+exports.Selection = Selection;
+
+Selection.prototype = {
+ _walker: null,
+
+ _onMutations: function (mutations) {
+ let attributeChange = false;
+ let pseudoChange = false;
+ let detached = false;
+ let parentNode = null;
+
+ for (let m of mutations) {
+ if (!attributeChange && m.type == "attributes") {
+ attributeChange = true;
+ }
+ if (m.type == "childList") {
+ if (!detached && !this.isConnected()) {
+ if (this.isNode()) {
+ parentNode = m.target;
+ }
+ detached = true;
+ }
+ }
+ if (m.type == "pseudoClassLock") {
+ pseudoChange = true;
+ }
+ }
+
+ // Fire our events depending on what changed in the mutations array
+ if (attributeChange) {
+ this.emit("attribute-changed");
+ }
+ if (pseudoChange) {
+ this.emit("pseudoclass");
+ }
+ if (detached) {
+ this.emit("detached-front", parentNode);
+ }
+ },
+
+ destroy: function () {
+ this.setWalker(null);
+ },
+
+ setWalker: function (walker) {
+ if (this._walker) {
+ this._walker.off("mutations", this._onMutations);
+ }
+ this._walker = walker;
+ if (this._walker) {
+ this._walker.on("mutations", this._onMutations);
+ }
+ },
+
+ setNodeFront: function (value, reason = "unknown") {
+ this.reason = reason;
+
+ // If an inlineTextChild text node is being set, then set it's parent instead.
+ let parentNode = value && value.parentNode();
+ if (value && parentNode && parentNode.inlineTextChild === value) {
+ value = parentNode;
+ }
+
+ this._nodeFront = value;
+ this.emit("new-node-front", value, this.reason);
+ },
+
+ get documentFront() {
+ return this._walker.document(this._nodeFront);
+ },
+
+ get nodeFront() {
+ return this._nodeFront;
+ },
+
+ isRoot: function () {
+ return this.isNode() &&
+ this.isConnected() &&
+ this._nodeFront.isDocumentElement;
+ },
+
+ isNode: function () {
+ return !!this._nodeFront;
+ },
+
+ isConnected: function () {
+ let node = this._nodeFront;
+ if (!node || !node.actorID) {
+ return false;
+ }
+
+ while (node) {
+ if (node === this._walker.rootNode) {
+ return true;
+ }
+ node = node.parentNode();
+ }
+ return false;
+ },
+
+ isHTMLNode: function () {
+ let xhtmlNs = "http://www.w3.org/1999/xhtml";
+ return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
+ },
+
+ // Node type
+
+ isElementNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE;
+ },
+
+ isPseudoElementNode: function () {
+ return this.isNode() && this.nodeFront.isPseudoElement;
+ },
+
+ isAnonymousNode: function () {
+ return this.isNode() && this.nodeFront.isAnonymous;
+ },
+
+ isAttributeNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE;
+ },
+
+ isTextNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
+ },
+
+ isCDATANode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE;
+ },
+
+ isEntityRefNode: function () {
+ return this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE;
+ },
+
+ isEntityNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE;
+ },
+
+ isProcessingInstructionNode: function () {
+ return this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE;
+ },
+
+ isCommentNode: function () {
+ return this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE;
+ },
+
+ isDocumentNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE;
+ },
+
+ /**
+ * @returns true if the selection is the <body> HTML element.
+ */
+ isBodyNode: function () {
+ return this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "BODY";
+ },
+
+ /**
+ * @returns true if the selection is the <head> HTML element.
+ */
+ isHeadNode: function () {
+ return this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "HEAD";
+ },
+
+ isDocumentTypeNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE;
+ },
+
+ isDocumentFragmentNode: function () {
+ return this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE;
+ },
+
+ isNotationNode: function () {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE;
+ },
+};
diff --git a/devtools/client/framework/sidebar.js b/devtools/client/framework/sidebar.js
new file mode 100644
index 000000000..c27732b5d
--- /dev/null
+++ b/devtools/client/framework/sidebar.js
@@ -0,0 +1,592 @@
+/* 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";
+
+var Services = require("Services");
+var {Task} = require("devtools/shared/task");
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/client/shared/telemetry");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * ToolSidebar provides methods to register tabs in the sidebar.
+ * It's assumed that the sidebar contains a xul:tabbox.
+ * Typically, you'll want the tabbox parameter to be a XUL tabbox like this:
+ *
+ * <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs">
+ * <tabs/>
+ * <tabpanels flex="1"/>
+ * </tabbox>
+ *
+ * The ToolSidebar API has a method to add new tabs, so the tabs and tabpanels
+ * nodes can be empty. But they can also already contain items before the
+ * ToolSidebar is created.
+ *
+ * Tabs added through the addTab method are only identified by an ID and a URL
+ * which is used as the href of an iframe node that is inserted in the newly
+ * created tabpanel.
+ * Tabs already present before the ToolSidebar is created may contain anything.
+ * However, these tabs must have ID attributes if it is required for the various
+ * methods that accept an ID as argument to work here.
+ *
+ * @param {Node} tabbox
+ * <tabbox> node;
+ * @param {ToolPanel} panel
+ * Related ToolPanel instance;
+ * @param {String} uid
+ * Unique ID
+ * @param {Object} options
+ * - hideTabstripe: Should the tabs be hidden. Defaults to false
+ * - showAllTabsMenu: Should a drop-down menu be displayed in case tabs
+ * become hidden. Defaults to false.
+ * - disableTelemetry: By default, switching tabs on and off in the sidebar
+ * will record tool usage in telemetry, pass this option to true to avoid it.
+ *
+ * Events raised:
+ * - new-tab-registered : After a tab has been added via addTab. The tab ID
+ * is passed with the event. This however, is raised before the tab iframe
+ * is fully loaded.
+ * - <tabid>-ready : After the tab iframe has been loaded
+ * - <tabid>-selected : After tab <tabid> was selected
+ * - select : Same as above, but for any tab, the ID is passed with the event
+ * - <tabid>-unselected : After tab <tabid> is unselected
+ */
+function ToolSidebar(tabbox, panel, uid, options = {}) {
+ EventEmitter.decorate(this);
+
+ this._tabbox = tabbox;
+ this._uid = uid;
+ this._panelDoc = this._tabbox.ownerDocument;
+ this._toolPanel = panel;
+ this._options = options;
+
+ this._onTabBoxOverflow = this._onTabBoxOverflow.bind(this);
+ this._onTabBoxUnderflow = this._onTabBoxUnderflow.bind(this);
+
+ try {
+ this._width = Services.prefs.getIntPref("devtools.toolsidebar-width." + this._uid);
+ } catch (e) {}
+
+ if (!options.disableTelemetry) {
+ this._telemetry = new Telemetry();
+ }
+
+ this._tabbox.tabpanels.addEventListener("select", this, true);
+
+ this._tabs = new Map();
+
+ // Check for existing tabs in the DOM and add them.
+ this.addExistingTabs();
+
+ if (this._options.hideTabstripe) {
+ this._tabbox.setAttribute("hidetabs", "true");
+ }
+
+ if (this._options.showAllTabsMenu) {
+ this.addAllTabsMenu();
+ }
+
+ this._toolPanel.emit("sidebar-created", this);
+}
+
+exports.ToolSidebar = ToolSidebar;
+
+ToolSidebar.prototype = {
+ TAB_ID_PREFIX: "sidebar-tab-",
+
+ TABPANEL_ID_PREFIX: "sidebar-panel-",
+
+ /**
+ * Add a "…" button at the end of the tabstripe that toggles a dropdown menu
+ * containing the list of all tabs if any become hidden due to lack of room.
+ *
+ * If the ToolSidebar was created with the "showAllTabsMenu" option set to
+ * true, this is already done automatically. If not, you may call this
+ * function at any time to add the menu.
+ */
+ addAllTabsMenu: function () {
+ if (this._allTabsBtn) {
+ return;
+ }
+
+ let tabs = this._tabbox.tabs;
+
+ // Create a container and insert it first in the tabbox
+ let allTabsContainer = this._panelDoc.createElementNS(XULNS, "stack");
+ this._tabbox.insertBefore(allTabsContainer, tabs);
+
+ // Move the tabs inside and make them flex
+ allTabsContainer.appendChild(tabs);
+ tabs.setAttribute("flex", "1");
+
+ // Create the dropdown menu next to the tabs
+ this._allTabsBtn = this._panelDoc.createElementNS(XULNS, "toolbarbutton");
+ this._allTabsBtn.setAttribute("class", "devtools-sidebar-alltabs");
+ this._allTabsBtn.setAttribute("end", "0");
+ this._allTabsBtn.setAttribute("top", "0");
+ this._allTabsBtn.setAttribute("width", "15");
+ this._allTabsBtn.setAttribute("type", "menu");
+ this._allTabsBtn.setAttribute("tooltiptext",
+ L10N.getStr("sidebar.showAllTabs.tooltip"));
+ this._allTabsBtn.setAttribute("hidden", "true");
+ allTabsContainer.appendChild(this._allTabsBtn);
+
+ let menuPopup = this._panelDoc.createElementNS(XULNS, "menupopup");
+ this._allTabsBtn.appendChild(menuPopup);
+
+ // Listening to tabs overflow event to toggle the alltabs button
+ tabs.addEventListener("overflow", this._onTabBoxOverflow, false);
+ tabs.addEventListener("underflow", this._onTabBoxUnderflow, false);
+
+ // Add menuitems to the alltabs menu if there are already tabs in the
+ // sidebar
+ for (let [id, tab] of this._tabs) {
+ let item = this._addItemToAllTabsMenu(id, tab, {
+ selected: tab.hasAttribute("selected")
+ });
+ if (tab.hidden) {
+ item.hidden = true;
+ }
+ }
+ },
+
+ removeAllTabsMenu: function () {
+ if (!this._allTabsBtn) {
+ return;
+ }
+
+ let tabs = this._tabbox.tabs;
+
+ tabs.removeEventListener("overflow", this._onTabBoxOverflow, false);
+ tabs.removeEventListener("underflow", this._onTabBoxUnderflow, false);
+
+ // Moving back the tabs as a first child of the tabbox
+ this._tabbox.insertBefore(tabs, this._tabbox.tabpanels);
+ this._tabbox.querySelector("stack").remove();
+
+ this._allTabsBtn = null;
+ },
+
+ _onTabBoxOverflow: function () {
+ this._allTabsBtn.removeAttribute("hidden");
+ },
+
+ _onTabBoxUnderflow: function () {
+ this._allTabsBtn.setAttribute("hidden", "true");
+ },
+
+ /**
+ * Add an item in the allTabs menu for a given tab.
+ */
+ _addItemToAllTabsMenu: function (id, tab, options) {
+ if (!this._allTabsBtn) {
+ return;
+ }
+
+ let item = this._panelDoc.createElementNS(XULNS, "menuitem");
+ let idPrefix = "sidebar-alltabs-item-";
+ item.setAttribute("id", idPrefix + id);
+ item.setAttribute("label", tab.getAttribute("label"));
+ item.setAttribute("type", "checkbox");
+ if (options.selected) {
+ item.setAttribute("checked", true);
+ }
+ // The auto-checking of menuitems in this menu doesn't work, so let's do
+ // it manually
+ item.setAttribute("autocheck", false);
+
+ let menu = this._allTabsBtn.querySelector("menupopup");
+ if (options.insertBefore) {
+ let referenceItem = menu.querySelector(`#${idPrefix}${options.insertBefore}`);
+ menu.insertBefore(item, referenceItem);
+ } else {
+ menu.appendChild(item);
+ }
+
+ item.addEventListener("click", () => {
+ this._tabbox.selectedTab = tab;
+ }, false);
+
+ tab.allTabsMenuItem = item;
+
+ return item;
+ },
+
+ /**
+ * Register a tab. A tab is a document.
+ * The document must have a title, which will be used as the name of the tab.
+ *
+ * @param {string} id The unique id for this tab.
+ * @param {string} url The URL of the document to load in this new tab.
+ * @param {Object} options A set of options for this new tab:
+ * - {Boolean} selected Set to true to make this new tab selected by default.
+ * - {String} insertBefore By default, the new tab is appended at the end of the
+ * tabbox, pass the ID of an existing tab to insert it before that tab instead.
+ */
+ addTab: function (id, url, options = {}) {
+ let iframe = this._panelDoc.createElementNS(XULNS, "iframe");
+ iframe.className = "iframe-" + id;
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("src", url);
+ iframe.tooltip = "aHTMLTooltip";
+
+ // Creating the tab and adding it to the tabbox
+ let tab = this._panelDoc.createElementNS(XULNS, "tab");
+
+ tab.setAttribute("id", this.TAB_ID_PREFIX + id);
+ tab.setAttribute("crop", "end");
+ // Avoid showing "undefined" while the tab is loading
+ tab.setAttribute("label", "");
+
+ if (options.insertBefore) {
+ let referenceTab = this.getTab(options.insertBefore);
+ this._tabbox.tabs.insertBefore(tab, referenceTab);
+ } else {
+ this._tabbox.tabs.appendChild(tab);
+ }
+
+ // Add the tab to the allTabs menu if exists
+ let allTabsItem = this._addItemToAllTabsMenu(id, tab, options);
+
+ let onIFrameLoaded = (event) => {
+ let doc = event.target;
+ let win = doc.defaultView;
+ tab.setAttribute("label", doc.title);
+
+ if (allTabsItem) {
+ allTabsItem.setAttribute("label", doc.title);
+ }
+
+ iframe.removeEventListener("load", onIFrameLoaded, true);
+ if ("setPanel" in win) {
+ win.setPanel(this._toolPanel, iframe);
+ }
+ this.emit(id + "-ready");
+ };
+
+ iframe.addEventListener("load", onIFrameLoaded, true);
+
+ let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel");
+ tabpanel.setAttribute("id", this.TABPANEL_ID_PREFIX + id);
+ tabpanel.appendChild(iframe);
+
+ if (options.insertBefore) {
+ let referenceTabpanel = this.getTabPanel(options.insertBefore);
+ this._tabbox.tabpanels.insertBefore(tabpanel, referenceTabpanel);
+ } else {
+ this._tabbox.tabpanels.appendChild(tabpanel);
+ }
+
+ this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip");
+ this._tooltip.id = "aHTMLTooltip";
+ tabpanel.appendChild(this._tooltip);
+ this._tooltip.page = true;
+
+ tab.linkedPanel = this.TABPANEL_ID_PREFIX + id;
+
+ // We store the index of this tab.
+ this._tabs.set(id, tab);
+
+ if (options.selected) {
+ this._selectTabSoon(id);
+ }
+
+ this.emit("new-tab-registered", id);
+ },
+
+ untitledTabsIndex: 0,
+
+ /**
+ * Search for existing tabs in the markup that aren't know yet and add them.
+ */
+ addExistingTabs: function () {
+ let knownTabs = [...this._tabs.values()];
+
+ for (let tab of this._tabbox.tabs.querySelectorAll("tab")) {
+ if (knownTabs.indexOf(tab) !== -1) {
+ continue;
+ }
+
+ // Find an ID for this unknown tab
+ let id = tab.getAttribute("id") || "untitled-tab-" + (this.untitledTabsIndex++);
+
+ // If the existing tab contains the tab ID prefix, extract the ID of the
+ // tab
+ if (id.startsWith(this.TAB_ID_PREFIX)) {
+ id = id.split(this.TAB_ID_PREFIX).pop();
+ }
+
+ // Register the tab
+ this._tabs.set(id, tab);
+ this.emit("new-tab-registered", id);
+ }
+ },
+
+ /**
+ * Remove an existing tab.
+ * @param {String} tabId The ID of the tab that was used to register it, or
+ * the tab id attribute value if the tab existed before the sidebar got created.
+ * @param {String} tabPanelId Optional. If provided, this ID will be used
+ * instead of the tabId to retrieve and remove the corresponding <tabpanel>
+ */
+ removeTab: Task.async(function* (tabId, tabPanelId) {
+ // Remove the tab if it can be found
+ let tab = this.getTab(tabId);
+ if (!tab) {
+ return;
+ }
+
+ let win = this.getWindowForTab(tabId);
+ if (win && ("destroy" in win)) {
+ yield win.destroy();
+ }
+
+ tab.remove();
+
+ // Also remove the tabpanel
+ let panel = this.getTabPanel(tabPanelId || tabId);
+ if (panel) {
+ panel.remove();
+ }
+
+ this._tabs.delete(tabId);
+ this.emit("tab-unregistered", tabId);
+ }),
+
+ /**
+ * Show or hide a specific tab.
+ * @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
+ * @param {String} id The ID of the tab to be hidden.
+ */
+ toggleTab: function (isVisible, id) {
+ // Toggle the tab.
+ let tab = this.getTab(id);
+ if (!tab) {
+ return;
+ }
+ tab.hidden = !isVisible;
+
+ // Toggle the item in the allTabs menu.
+ if (this._allTabsBtn) {
+ this._allTabsBtn.querySelector("#sidebar-alltabs-item-" + id).hidden = !isVisible;
+ }
+ },
+
+ /**
+ * Select a specific tab.
+ */
+ select: function (id) {
+ let tab = this.getTab(id);
+ if (tab) {
+ this._tabbox.selectedTab = tab;
+ }
+ },
+
+ /**
+ * Hack required to select a tab right after it was created.
+ *
+ * @param {String} id
+ * The sidebar tab id to select.
+ */
+ _selectTabSoon: function (id) {
+ this._panelDoc.defaultView.setTimeout(() => {
+ this.select(id);
+ }, 0);
+ },
+
+ /**
+ * Return the id of the selected tab.
+ */
+ getCurrentTabID: function () {
+ let currentID = null;
+ for (let [id, tab] of this._tabs) {
+ if (this._tabbox.tabs.selectedItem == tab) {
+ currentID = id;
+ break;
+ }
+ }
+ return currentID;
+ },
+
+ /**
+ * Returns the requested tab panel based on the id.
+ * @param {String} id
+ * @return {DOMNode}
+ */
+ getTabPanel: function (id) {
+ // Search with and without the ID prefix as there might have been existing
+ // tabpanels by the time the sidebar got created
+ return this._tabbox.tabpanels.querySelector("#" + this.TABPANEL_ID_PREFIX + id + ", #" + id);
+ },
+
+ /**
+ * Return the tab based on the provided id, if one was registered with this id.
+ * @param {String} id
+ * @return {DOMNode}
+ */
+ getTab: function (id) {
+ return this._tabs.get(id);
+ },
+
+ /**
+ * Event handler.
+ */
+ handleEvent: function (event) {
+ if (event.type !== "select" || this._destroyed) {
+ return;
+ }
+
+ if (this._currentTool == this.getCurrentTabID()) {
+ // Tool hasn't changed.
+ return;
+ }
+
+ let previousTool = this._currentTool;
+ this._currentTool = this.getCurrentTabID();
+ if (previousTool) {
+ if (this._telemetry) {
+ this._telemetry.toolClosed(previousTool);
+ }
+ this.emit(previousTool + "-unselected");
+ }
+
+ if (this._telemetry) {
+ this._telemetry.toolOpened(this._currentTool);
+ }
+
+ this.emit(this._currentTool + "-selected");
+ this.emit("select", this._currentTool);
+
+ // Handlers for "select"/"...-selected"/"...-unselected" events might have
+ // destroyed the sidebar in the meantime.
+ if (this._destroyed) {
+ return;
+ }
+
+ // Handle menuitem selection if the allTabsMenu is there by unchecking all
+ // items except the selected one.
+ let tab = this._tabbox.selectedTab;
+ if (tab.allTabsMenuItem) {
+ for (let otherItem of this._allTabsBtn.querySelectorAll("menuitem")) {
+ otherItem.removeAttribute("checked");
+ }
+ tab.allTabsMenuItem.setAttribute("checked", true);
+ }
+ },
+
+ /**
+ * Toggle sidebar's visibility state.
+ */
+ toggle: function () {
+ if (this._tabbox.hasAttribute("hidden")) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ },
+
+ /**
+ * Show the sidebar.
+ *
+ * @param {String} id
+ * The sidebar tab id to select.
+ */
+ show: function (id) {
+ if (this._width) {
+ this._tabbox.width = this._width;
+ }
+ this._tabbox.removeAttribute("hidden");
+
+ // If an id is given, select the corresponding sidebar tab and record the
+ // tool opened.
+ if (id) {
+ this._currentTool = id;
+
+ if (this._telemetry) {
+ this._telemetry.toolOpened(this._currentTool);
+ }
+
+ this._selectTabSoon(id);
+ }
+
+ this.emit("show");
+ },
+
+ /**
+ * Show the sidebar.
+ */
+ hide: function () {
+ Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
+ this._tabbox.setAttribute("hidden", "true");
+ this._panelDoc.activeElement.blur();
+
+ this.emit("hide");
+ },
+
+ /**
+ * Return the window containing the tab content.
+ */
+ getWindowForTab: function (id) {
+ if (!this._tabs.has(id)) {
+ return null;
+ }
+
+ // Get the tabpanel and make sure it contains an iframe
+ let panel = this.getTabPanel(id);
+ if (!panel || !panel.firstChild || !panel.firstChild.contentWindow) {
+ return;
+ }
+ return panel.firstChild.contentWindow;
+ },
+
+ /**
+ * Clean-up.
+ */
+ destroy: Task.async(function* () {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ Services.prefs.setIntPref("devtools.toolsidebar-width." + this._uid, this._tabbox.width);
+
+ if (this._allTabsBtn) {
+ this.removeAllTabsMenu();
+ }
+
+ this._tabbox.tabpanels.removeEventListener("select", this, true);
+
+ // Note that we check for the existence of this._tabbox.tabpanels at each
+ // step as the container window may have been closed by the time one of the
+ // panel's destroy promise resolves.
+ while (this._tabbox.tabpanels && this._tabbox.tabpanels.hasChildNodes()) {
+ let panel = this._tabbox.tabpanels.firstChild;
+ let win = panel.firstChild.contentWindow;
+ if (win && ("destroy" in win)) {
+ yield win.destroy();
+ }
+ panel.remove();
+ }
+
+ while (this._tabbox.tabs && this._tabbox.tabs.hasChildNodes()) {
+ this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild);
+ }
+
+ if (this._currentTool && this._telemetry) {
+ this._telemetry.toolClosed(this._currentTool);
+ }
+
+ this._toolPanel.emit("sidebar-destroyed", this);
+
+ this._tabs = null;
+ this._tabbox = null;
+ this._panelDoc = null;
+ this._toolPanel = null;
+ })
+};
diff --git a/devtools/client/framework/source-map-service.js b/devtools/client/framework/source-map-service.js
new file mode 100644
index 000000000..838adc392
--- /dev/null
+++ b/devtools/client/framework/source-map-service.js
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Task } = require("devtools/shared/task");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { LocationStore, serialize, deserialize } = require("./location-store");
+
+/**
+ * A manager class that wraps a TabTarget and listens to source changes
+ * from source maps and resolves non-source mapped locations to the source mapped
+ * versions and back and forth, and creating smart elements with a location that
+ * auto-update when the source changes (from pretty printing, source maps loading, etc)
+ *
+ * @param {TabTarget} target
+ */
+
+function SourceMapService(target) {
+ this._target = target;
+ this._locationStore = new LocationStore();
+ this._isNotSourceMapped = new Map();
+
+ EventEmitter.decorate(this);
+
+ this._onSourceUpdated = this._onSourceUpdated.bind(this);
+ this._resolveLocation = this._resolveLocation.bind(this);
+ this._resolveAndUpdate = this._resolveAndUpdate.bind(this);
+ this.subscribe = this.subscribe.bind(this);
+ this.unsubscribe = this.unsubscribe.bind(this);
+ this.reset = this.reset.bind(this);
+ this.destroy = this.destroy.bind(this);
+
+ target.on("source-updated", this._onSourceUpdated);
+ target.on("navigate", this.reset);
+ target.on("will-navigate", this.reset);
+}
+
+/**
+ * Clears the store containing the cached promised locations
+ */
+SourceMapService.prototype.reset = function () {
+ // Guard to prevent clearing the store when it is not initialized yet.
+ if (!this._locationStore) {
+ return;
+ }
+ this._locationStore.clear();
+ this._isNotSourceMapped.clear();
+};
+
+SourceMapService.prototype.destroy = function () {
+ this.reset();
+ this._target.off("source-updated", this._onSourceUpdated);
+ this._target.off("navigate", this.reset);
+ this._target.off("will-navigate", this.reset);
+ this._target.off("close", this.destroy);
+ this._target = this._locationStore = this._isNotSourceMapped = null;
+};
+
+/**
+ * Sets up listener for the callback to update the FrameView
+ * and tries to resolve location, if it is source-mappable
+ * @param location
+ * @param callback
+ */
+SourceMapService.prototype.subscribe = function (location, callback) {
+ // A valid candidate location for source-mapping should have a url and line.
+ // Abort if there's no `url`, which means it's unsourcemappable anyway,
+ // like an eval script.
+ // From previous attempts to source-map locations, we also determine if a location
+ // is not source-mapped.
+ if (!location.url || !location.line || this._isNotSourceMapped.get(location.url)) {
+ return;
+ }
+ this.on(serialize(location), callback);
+ this._locationStore.set(location);
+ this._resolveAndUpdate(location);
+};
+
+/**
+ * Removes the listener for the location and clears cached locations
+ * @param location
+ * @param callback
+ */
+SourceMapService.prototype.unsubscribe = function (location, callback) {
+ this.off(serialize(location), callback);
+ // Check to see if the store exists before attempting to clear a location
+ // Sometimes un-subscribe happens during the destruction cascades and this
+ // condition is to protect against that. Could be looked into in the future.
+ if (!this._locationStore) {
+ return;
+ }
+ this._locationStore.clearByURL(location.url);
+};
+
+/**
+ * Tries to resolve the location and if successful,
+ * emits the resolved location
+ * @param location
+ * @private
+ */
+SourceMapService.prototype._resolveAndUpdate = function (location) {
+ this._resolveLocation(location).then(resolvedLocation => {
+ // We try to source map the first console log to initiate the source-updated
+ // event from target. The isSameLocation check is to make sure we don't update
+ // the frame, if the location is not source-mapped.
+ if (resolvedLocation && !isSameLocation(location, resolvedLocation)) {
+ this.emit(serialize(location), location, resolvedLocation);
+ }
+ });
+};
+
+/**
+ * Checks if there is existing promise to resolve location, if so returns cached promise
+ * if not, tries to resolve location and returns a promised location
+ * @param location
+ * @return Promise<Object>
+ * @private
+ */
+SourceMapService.prototype._resolveLocation = Task.async(function* (location) {
+ let resolvedLocation;
+ const cachedLocation = this._locationStore.get(location);
+ if (cachedLocation) {
+ resolvedLocation = cachedLocation;
+ } else {
+ const promisedLocation = resolveLocation(this._target, location);
+ if (promisedLocation) {
+ this._locationStore.set(location, promisedLocation);
+ resolvedLocation = promisedLocation;
+ }
+ }
+ return resolvedLocation;
+});
+
+/**
+ * Checks if the `source-updated` event is fired from the target.
+ * Checks to see if location store has the source url in its cache,
+ * if so, tries to update each stale location in the store.
+ * Determines if the source should be source-mapped or not.
+ * @param _
+ * @param sourceEvent
+ * @private
+ */
+SourceMapService.prototype._onSourceUpdated = function (_, sourceEvent) {
+ let { type, source } = sourceEvent;
+
+ // If we get a new source, and it's not a source map, abort;
+ // we can have no actionable updates as this is just a new normal source.
+ // Check Source Actor for sourceMapURL property (after Firefox 48)
+ // If not present, utilize isSourceMapped and isPrettyPrinted properties
+ // to estimate if a source is not source-mapped.
+ const isNotSourceMapped = !(source.sourceMapURL ||
+ source.isSourceMapped || source.isPrettyPrinted);
+ if (type === "newSource" && isNotSourceMapped) {
+ this._isNotSourceMapped.set(source.url, true);
+ return;
+ }
+ let sourceUrl = null;
+ if (source.generatedUrl && source.isSourceMapped) {
+ sourceUrl = source.generatedUrl;
+ } else if (source.url && source.isPrettyPrinted) {
+ sourceUrl = source.url;
+ }
+ const locationsToResolve = this._locationStore.getByURL(sourceUrl);
+ if (locationsToResolve.length) {
+ this._locationStore.clearByURL(sourceUrl);
+ for (let location of locationsToResolve) {
+ this._resolveAndUpdate(deserialize(location));
+ }
+ }
+};
+
+exports.SourceMapService = SourceMapService;
+
+/**
+ * Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve
+ * the location to the latest location (so a source mapped location, or if pretty print
+ * status has been updated)
+ *
+ * @param {TabTarget} target
+ * @param {Object} location
+ * @return {Promise<Object>}
+ */
+function resolveLocation(target, location) {
+ return Task.spawn(function* () {
+ let newLocation = yield target.resolveLocation({
+ url: location.url,
+ line: location.line,
+ column: location.column || Infinity
+ });
+ // Source or mapping not found, so don't do anything
+ if (newLocation.error) {
+ return null;
+ }
+ return newLocation;
+ });
+}
+
+/**
+ * Returns true if the original location and resolved location are the same
+ * @param location
+ * @param resolvedLocation
+ * @returns {boolean}
+ */
+function isSameLocation(location, resolvedLocation) {
+ return location.url === resolvedLocation.url &&
+ location.line === resolvedLocation.line &&
+ location.column === resolvedLocation.column;
+}
diff --git a/devtools/client/framework/source-map-util.js b/devtools/client/framework/source-map-util.js
new file mode 100644
index 000000000..0bb25b3df
--- /dev/null
+++ b/devtools/client/framework/source-map-util.js
@@ -0,0 +1,20 @@
+function originalToGeneratedId(originalId) {
+ const match = originalId.match(/(.*)\/originalSource/);
+ return match ? match[1] : "";
+}
+
+function generatedToOriginalId(generatedId, url) {
+ return generatedId + "/originalSource-" + url.replace(/ \//, '-');
+}
+
+function isOriginalId(id) {
+ return !!id.match(/\/originalSource/);
+}
+
+function isGeneratedId(id) {
+ return !isOriginalId(id);
+}
+
+module.exports = {
+ originalToGeneratedId, generatedToOriginalId, isOriginalId, isGeneratedId
+};
diff --git a/devtools/client/framework/source-map-worker.js b/devtools/client/framework/source-map-worker.js
new file mode 100644
index 000000000..c68732f38
--- /dev/null
+++ b/devtools/client/framework/source-map-worker.js
@@ -0,0 +1,220 @@
+const { fetch, assert } = require("devtools/shared/DevToolsUtils");
+const { joinURI } = require("devtools/shared/path");
+const path = require("sdk/fs/path");
+const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
+const { isJavaScript } = require("./source");
+const {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId
+} = require("./source-map-util");
+
+let sourceMapRequests = new Map();
+let sourceMapsEnabled = false;
+
+function clearSourceMaps() {
+ sourceMapRequests.clear();
+}
+
+function enableSourceMaps() {
+ sourceMapsEnabled = true;
+}
+
+function _resolveSourceMapURL(source) {
+ const { url = "", sourceMapURL = "" } = source;
+ if (path.isURL(sourceMapURL) || url == "") {
+ // If it's already a full URL or the source doesn't have a URL,
+ // don't resolve anything.
+ return sourceMapURL;
+ } else if (path.isAbsolute(sourceMapURL)) {
+ // If it's an absolute path, it should be resolved relative to the
+ // host of the source.
+ const { protocol = "", host = "" } = parse(url);
+ return `${protocol}//${host}${sourceMapURL}`;
+ }
+ // Otherwise, it's a relative path and should be resolved relative
+ // to the source.
+ return dirname(url) + "/" + sourceMapURL;
+}
+
+/**
+ * Sets the source map's sourceRoot to be relative to the source map url.
+ * @memberof utils/source-map-worker
+ * @static
+ */
+function _setSourceMapRoot(sourceMap, absSourceMapURL, source) {
+ // No need to do this fiddling if we won't be fetching any sources over the
+ // wire.
+ if (sourceMap.hasContentsOfAllSources()) {
+ return;
+ }
+
+ const base = dirname(
+ (absSourceMapURL.indexOf("data:") === 0 && source.url) ?
+ source.url :
+ absSourceMapURL
+ );
+
+ if (sourceMap.sourceRoot) {
+ sourceMap.sourceRoot = joinURI(base, sourceMap.sourceRoot);
+ } else {
+ sourceMap.sourceRoot = base;
+ }
+
+ return sourceMap;
+}
+
+function _getSourceMap(generatedSourceId)
+ : ?Promise<SourceMapConsumer> {
+ return sourceMapRequests.get(generatedSourceId);
+}
+
+async function _resolveAndFetch(generatedSource) : SourceMapConsumer {
+ // Fetch the sourcemap over the network and create it.
+ const sourceMapURL = _resolveSourceMapURL(generatedSource);
+ const fetched = await fetch(
+ sourceMapURL, { loadFromCache: false }
+ );
+
+ // Create the source map and fix it up.
+ const map = new SourceMapConsumer(fetched.content);
+ _setSourceMapRoot(map, sourceMapURL, generatedSource);
+ return map;
+}
+
+function _fetchSourceMap(generatedSource) {
+ const existingRequest = sourceMapRequests.get(generatedSource.id);
+ if (existingRequest) {
+ // If it has already been requested, return the request. Make sure
+ // to do this even if sourcemapping is turned off, because
+ // pretty-printing uses sourcemaps.
+ //
+ // An important behavior here is that if it's in the middle of
+ // requesting it, all subsequent calls will block on the initial
+ // request.
+ return existingRequest;
+ } else if (!generatedSource.sourceMapURL || !sourceMapsEnabled) {
+ return Promise.resolve(null);
+ }
+
+ // Fire off the request, set it in the cache, and return it.
+ // Suppress any errors and just return null (ignores bogus
+ // sourcemaps).
+ const req = _resolveAndFetch(generatedSource).catch(() => null);
+ sourceMapRequests.set(generatedSource.id, req);
+ return req;
+}
+
+async function getOriginalURLs(generatedSource) {
+ const map = await _fetchSourceMap(generatedSource);
+ return map && map.sources;
+}
+
+async function getGeneratedLocation(location: Location, originalSource: Source)
+ : Promise<Location> {
+ if (!isOriginalId(location.sourceId)) {
+ return location;
+ }
+
+ const generatedSourceId = originalToGeneratedId(location.sourceId);
+ const map = await _getSourceMap(generatedSourceId);
+ if (!map) {
+ return location;
+ }
+
+ const { line, column } = map.generatedPositionFor({
+ source: originalSource.url,
+ line: location.line,
+ column: location.column == null ? 0 : location.column
+ });
+
+ return {
+ sourceId: generatedSourceId,
+ line: line,
+ // Treat 0 as no column so that line breakpoints work correctly.
+ column: column === 0 ? undefined : column
+ };
+}
+
+async function getOriginalLocation(location) {
+ if (!isGeneratedId(location.sourceId)) {
+ return location;
+ }
+
+ const map = await _getSourceMap(location.sourceId);
+ if (!map) {
+ return location;
+ }
+
+ const { source: url, line, column } = map.originalPositionFor({
+ line: location.line,
+ column: location.column == null ? Infinity : location.column
+ });
+
+ if (url == null) {
+ // No url means the location didn't map.
+ return location;
+ }
+
+ return {
+ sourceId: generatedToOriginalId(location.sourceId, url),
+ line,
+ column
+ };
+}
+
+async function getOriginalSourceText(originalSource) {
+ assert(isOriginalId(originalSource.id),
+ "Source is not an original source");
+
+ const generatedSourceId = originalToGeneratedId(originalSource.id);
+ const map = await _getSourceMap(generatedSourceId);
+ if (!map) {
+ return null;
+ }
+
+ let text = map.sourceContentFor(originalSource.url);
+ if (!text) {
+ text = (await fetch(
+ originalSource.url, { loadFromCache: false }
+ )).content;
+ }
+
+ return {
+ text,
+ contentType: isJavaScript(originalSource.url || "") ?
+ "text/javascript" :
+ "text/plain"
+ };
+}
+
+function applySourceMap(generatedId, url, code, mappings) {
+ const generator = new SourceMapGenerator({ file: url });
+ mappings.forEach(mapping => generator.addMapping(mapping));
+ generator.setSourceContent(url, code);
+
+ const map = SourceMapConsumer(generator.toJSON());
+ sourceMapRequests.set(generatedId, Promise.resolve(map));
+}
+
+const publicInterface = {
+ getOriginalURLs,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalSourceText,
+ enableSourceMaps,
+ applySourceMap,
+ clearSourceMaps
+};
+
+self.onmessage = function(msg) {
+ const { id, method, args } = msg.data;
+ const response = publicInterface[method].apply(undefined, args);
+ if (response instanceof Promise) {
+ response.then(val => self.postMessage({ id, response: val }),
+ err => self.postMessage({ id, error: err }));
+ } else {
+ self.postMessage({ id, response });
+ }
+};
diff --git a/devtools/client/framework/source-map.js b/devtools/client/framework/source-map.js
new file mode 100644
index 000000000..7c6805c85
--- /dev/null
+++ b/devtools/client/framework/source-map.js
@@ -0,0 +1,84 @@
+// @flow
+
+const {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId
+} = require("./source-map-util");
+
+function workerTask(worker, method) {
+ return function(...args: any) {
+ return new Promise((resolve, reject) => {
+ const id = msgId++;
+ worker.postMessage({ id, method, args });
+
+ const listener = ({ data: result }) => {
+ if (result.id !== id) {
+ return;
+ }
+
+ worker.removeEventListener("message", listener);
+ if (result.error) {
+ reject(result.error);
+ } else {
+ resolve(result.response);
+ }
+ };
+
+ worker.addEventListener("message", listener);
+ });
+ };
+}
+
+let sourceMapWorker;
+function restartWorker() {
+ if (sourceMapWorker) {
+ sourceMapWorker.terminate();
+ }
+ sourceMapWorker = new Worker(
+ "resource://devtools/client/framework/source-map-worker.js"
+ );
+
+ if (Services.prefs.getBoolPref("devtools.debugger.client-source-maps-enabled")) {
+ sourceMapWorker.postMessage({ id: 0, method: "enableSourceMaps" });
+ }
+}
+restartWorker();
+
+function destroyWorker() {
+ if (sourceMapWorker) {
+ sourceMapWorker.terminate();
+ sourceMapWorker = null;
+ }
+}
+
+function shouldSourceMap() {
+ return Services.prefs.getBoolPref("devtools.debugger.client-source-maps-enabled");
+}
+
+const getOriginalURLs = workerTask(sourceMapWorker, "getOriginalURLs");
+const getGeneratedLocation = workerTask(sourceMapWorker,
+ "getGeneratedLocation");
+const getOriginalLocation = workerTask(sourceMapWorker,
+ "getOriginalLocation");
+const getOriginalSourceText = workerTask(sourceMapWorker,
+ "getOriginalSourceText");
+const applySourceMap = workerTask(sourceMapWorker, "applySourceMap");
+const clearSourceMaps = workerTask(sourceMapWorker, "clearSourceMaps");
+
+module.exports = {
+ originalToGeneratedId,
+ generatedToOriginalId,
+ isGeneratedId,
+ isOriginalId,
+
+ getOriginalURLs,
+ getGeneratedLocation,
+ getOriginalLocation,
+ getOriginalSourceText,
+ applySourceMap,
+ clearSourceMaps,
+ destroyWorker,
+ shouldSourceMap
+};
diff --git a/devtools/client/framework/target-from-url.js b/devtools/client/framework/target-from-url.js
new file mode 100644
index 000000000..4e2c30377
--- /dev/null
+++ b/devtools/client/framework/target-from-url.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Cu, Ci } = require("chrome");
+
+const { TargetFactory } = require("devtools/client/framework/target");
+const { DebuggerServer } = require("devtools/server/main");
+const { DebuggerClient } = require("devtools/shared/client/main");
+const { Task } = require("devtools/shared/task");
+
+/**
+ * Construct a Target for a given URL object having various query parameters:
+ *
+ * host:
+ * {String} The hostname or IP address to connect to.
+ * port:
+ * {Number} The TCP port to connect to, to use with `host` argument.
+ * ws:
+ * {Boolean} If true, connect via websocket instread of regular TCP connection.
+ *
+ * type: tab, process
+ * {String} The type of target to connect to. Currently tabs and processes are supported types.
+ *
+ * If type="tab":
+ * id:
+ * {Number} the tab outerWindowID
+ * chrome: Optional
+ * {Boolean} Force the creation of a chrome target. Gives more privileges to the tab
+ * actor. Allows chrome execution in the webconsole and see chrome files in
+ * the debugger. (handy when contributing to firefox)
+ *
+ * If type="process":
+ * id:
+ * {Number} the process id to debug. Default to 0, which is the parent process.
+ *
+ * @param {URL} url
+ * The url to fetch query params from.
+ *
+ * @return A target object
+ */
+exports.targetFromURL = Task.async(function* (url) {
+ let params = url.searchParams;
+ let type = params.get("type");
+ if (!type) {
+ throw new Error("targetFromURL, missing type parameter");
+ }
+ let id = params.get("id");
+ // Allows to spawn a chrome enabled target for any context
+ // (handy to debug chrome stuff in a child process)
+ let chrome = params.has("chrome");
+
+ let client = yield createClient(params);
+
+ yield client.connect();
+
+ let form, isTabActor;
+ if (type === "tab") {
+ // Fetch target for a remote tab
+ id = parseInt(id);
+ if (isNaN(id)) {
+ throw new Error("targetFromURL, wrong tab id:'" + id + "', should be a number");
+ }
+ try {
+ let response = yield client.getTab({ outerWindowID: id });
+ form = response.tab;
+ } catch (ex) {
+ if (ex.error == "noTab") {
+ throw new Error("targetFromURL, tab with outerWindowID:'" + id + "' doesn't exist");
+ }
+ throw ex;
+ }
+ } else if (type == "process") {
+ // Fetch target for a remote chrome actor
+ DebuggerServer.allowChromeProcess = true;
+ try {
+ id = parseInt(id);
+ if (isNaN(id)) {
+ id = 0;
+ }
+ let response = yield client.getProcess(id);
+ form = response.form;
+ chrome = true;
+ if (id != 0) {
+ // Child process are not exposing tab actors and only support debugger+console
+ isTabActor = false;
+ }
+ } catch (ex) {
+ if (ex.error == "noProcess") {
+ throw new Error("targetFromURL, process with id:'" + id + "' doesn't exist");
+ }
+ throw ex;
+ }
+ } else {
+ throw new Error("targetFromURL, unsupported type='" + type + "' parameter");
+ }
+
+ return TargetFactory.forRemoteTab({ client, form, chrome, isTabActor });
+});
+
+function* createClient(params) {
+ let host = params.get("host");
+ let port = params.get("port");
+ let webSocket = !!params.get("ws");
+
+ let transport;
+ if (port) {
+ transport = yield DebuggerClient.socketConnect({ host, port, webSocket });
+ } else {
+ // Setup a server if we don't have one already running
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ transport = DebuggerServer.connectPipe()
+ }
+ return new DebuggerClient(transport);
+}
diff --git a/devtools/client/framework/target.js b/devtools/client/framework/target.js
new file mode 100644
index 000000000..30a720b7e
--- /dev/null
+++ b/devtools/client/framework/target.js
@@ -0,0 +1,825 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Ci } = require("chrome");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const EventEmitter = require("devtools/shared/event-emitter");
+const Services = require("Services");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "gDevTools",
+ "devtools/client/framework/devtools", true);
+
+const targets = new WeakMap();
+const promiseTargets = new WeakMap();
+
+/**
+ * Functions for creating Targets
+ */
+const TargetFactory = exports.TargetFactory = {
+ /**
+ * Construct a Target
+ * @param {XULTab} tab
+ * The tab to use in creating a new target.
+ *
+ * @return A target object
+ */
+ forTab: function (tab) {
+ let target = targets.get(tab);
+ if (target == null) {
+ target = new TabTarget(tab);
+ targets.set(tab, target);
+ }
+ return target;
+ },
+
+ /**
+ * Return a promise of a Target for a remote tab.
+ * @param {Object} options
+ * The options object has the following properties:
+ * {
+ * form: the remote protocol form of a tab,
+ * client: a DebuggerClient instance
+ * (caller owns this and is responsible for closing),
+ * chrome: true if the remote target is the whole process
+ * }
+ *
+ * @return A promise of a target object
+ */
+ forRemoteTab: function (options) {
+ let targetPromise = promiseTargets.get(options);
+ if (targetPromise == null) {
+ let target = new TabTarget(options);
+ targetPromise = target.makeRemote().then(() => target);
+ promiseTargets.set(options, targetPromise);
+ }
+ return targetPromise;
+ },
+
+ forWorker: function (workerClient) {
+ let target = targets.get(workerClient);
+ if (target == null) {
+ target = new WorkerTarget(workerClient);
+ targets.set(workerClient, target);
+ }
+ return target;
+ },
+
+ /**
+ * Creating a target for a tab that is being closed is a problem because it
+ * allows a leak as a result of coming after the close event which normally
+ * clears things up. This function allows us to ask if there is a known
+ * target for a tab without creating a target
+ * @return true/false
+ */
+ isKnownTab: function (tab) {
+ return targets.has(tab);
+ },
+};
+
+/**
+ * A Target represents something that we can debug. Targets are generally
+ * read-only. Any changes that you wish to make to a target should be done via
+ * a Tool that attaches to the target. i.e. a Target is just a pointer saying
+ * "the thing to debug is over there".
+ *
+ * Providing a generalized abstraction of a web-page or web-browser (available
+ * either locally or remotely) is beyond the scope of this class (and maybe
+ * also beyond the scope of this universe) However Target does attempt to
+ * abstract some common events and read-only properties common to many Tools.
+ *
+ * Supported read-only properties:
+ * - name, isRemote, url
+ *
+ * Target extends EventEmitter and provides support for the following events:
+ * - close: The target window has been closed. All tools attached to this
+ * target should close. This event is not currently cancelable.
+ * - navigate: The target window has navigated to a different URL
+ *
+ * Optional events:
+ * - will-navigate: The target window will navigate to a different URL
+ * - hidden: The target is not visible anymore (for TargetTab, another tab is
+ * selected)
+ * - visible: The target is visible (for TargetTab, tab is selected)
+ *
+ * Comparing Targets: 2 instances of a Target object can point at the same
+ * thing, so t1 !== t2 and t1 != t2 even when they represent the same object.
+ * To compare to targets use 't1.equals(t2)'.
+ */
+
+/**
+ * A TabTarget represents a page living in a browser tab. Generally these will
+ * be web pages served over http(s), but they don't have to be.
+ */
+function TabTarget(tab) {
+ EventEmitter.decorate(this);
+ this.destroy = this.destroy.bind(this);
+ this.activeTab = this.activeConsole = null;
+ // Only real tabs need initialization here. Placeholder objects for remote
+ // targets will be initialized after a makeRemote method call.
+ if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) {
+ this._tab = tab;
+ this._setupListeners();
+ } else {
+ this._form = tab.form;
+ this._url = this._form.url;
+ this._title = this._form.title;
+
+ this._client = tab.client;
+ this._chrome = tab.chrome;
+ }
+ // Default isTabActor to true if not explicitly specified
+ if (typeof tab.isTabActor == "boolean") {
+ this._isTabActor = tab.isTabActor;
+ } else {
+ this._isTabActor = true;
+ }
+}
+
+TabTarget.prototype = {
+ _webProgressListener: null,
+
+ /**
+ * Returns a promise for the protocol description from the root actor. Used
+ * internally with `target.actorHasMethod`. Takes advantage of caching if
+ * definition was fetched previously with the corresponding actor information.
+ * Actors are lazily loaded, so not only must the tool using a specific actor
+ * be in use, the actors are only registered after invoking a method (for
+ * performance reasons, added in bug 988237), so to use these actor detection
+ * methods, one must already be communicating with a specific actor of that
+ * type.
+ *
+ * Must be a remote target.
+ *
+ * @return {Promise}
+ * {
+ * "category": "actor",
+ * "typeName": "longstractor",
+ * "methods": [{
+ * "name": "substring",
+ * "request": {
+ * "type": "substring",
+ * "start": {
+ * "_arg": 0,
+ * "type": "primitive"
+ * },
+ * "end": {
+ * "_arg": 1,
+ * "type": "primitive"
+ * }
+ * },
+ * "response": {
+ * "substring": {
+ * "_retval": "primitive"
+ * }
+ * }
+ * }],
+ * "events": {}
+ * }
+ */
+ getActorDescription: function (actorName) {
+ if (!this.client) {
+ throw new Error("TabTarget#getActorDescription() can only be called on " +
+ "remote tabs.");
+ }
+
+ let deferred = defer();
+
+ if (this._protocolDescription &&
+ this._protocolDescription.types[actorName]) {
+ deferred.resolve(this._protocolDescription.types[actorName]);
+ } else {
+ this.client.mainRoot.protocolDescription(description => {
+ this._protocolDescription = description;
+ deferred.resolve(description.types[actorName]);
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Returns a boolean indicating whether or not the specific actor
+ * type exists. Must be a remote target.
+ *
+ * @param {String} actorName
+ * @return {Boolean}
+ */
+ hasActor: function (actorName) {
+ if (!this.client) {
+ throw new Error("TabTarget#hasActor() can only be called on remote " +
+ "tabs.");
+ }
+ if (this.form) {
+ return !!this.form[actorName + "Actor"];
+ }
+ return false;
+ },
+
+ /**
+ * Queries the protocol description to see if an actor has
+ * an available method. The actor must already be lazily-loaded (read
+ * the restrictions in the `getActorDescription` comments),
+ * so this is for use inside of tool. Returns a promise that
+ * resolves to a boolean. Must be a remote target.
+ *
+ * @param {String} actorName
+ * @param {String} methodName
+ * @return {Promise}
+ */
+ actorHasMethod: function (actorName, methodName) {
+ if (!this.client) {
+ throw new Error("TabTarget#actorHasMethod() can only be called on " +
+ "remote tabs.");
+ }
+ return this.getActorDescription(actorName).then(desc => {
+ if (desc && desc.methods) {
+ return !!desc.methods.find(method => method.name === methodName);
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Returns a trait from the root actor.
+ *
+ * @param {String} traitName
+ * @return {Mixed}
+ */
+ getTrait: function (traitName) {
+ if (!this.client) {
+ throw new Error("TabTarget#getTrait() can only be called on remote " +
+ "tabs.");
+ }
+
+ // If the targeted actor exposes traits and has a defined value for this
+ // traits, override the root actor traits
+ if (this.form.traits && traitName in this.form.traits) {
+ return this.form.traits[traitName];
+ }
+
+ return this.client.traits[traitName];
+ },
+
+ get tab() {
+ return this._tab;
+ },
+
+ get form() {
+ return this._form;
+ },
+
+ // Get a promise of the root form returned by a listTabs request. This promise
+ // is cached.
+ get root() {
+ if (!this._root) {
+ this._root = this._getRoot();
+ }
+ return this._root;
+ },
+
+ _getRoot: function () {
+ return new Promise((resolve, reject) => {
+ this.client.listTabs(response => {
+ if (response.error) {
+ reject(new Error(response.error + ": " + response.message));
+ return;
+ }
+
+ resolve(response);
+ });
+ });
+ },
+
+ get client() {
+ return this._client;
+ },
+
+ // Tells us if we are debugging content document
+ // or if we are debugging chrome stuff.
+ // Allows to controls which features are available against
+ // a chrome or a content document.
+ get chrome() {
+ return this._chrome;
+ },
+
+ // Tells us if the related actor implements TabActor interface
+ // and requires to call `attach` request before being used
+ // and `detach` during cleanup
+ get isTabActor() {
+ return this._isTabActor;
+ },
+
+ get window() {
+ // XXX - this is a footgun for e10s - there .contentWindow will be null,
+ // and even though .contentWindowAsCPOW *might* work, it will not work
+ // in all contexts. Consumers of .window need to be refactored to not
+ // rely on this.
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ console.error("The .window getter on devtools' |target| object isn't " +
+ "e10s friendly!\n" + Error().stack);
+ }
+ // Be extra careful here, since this may be called by HS_getHudByWindow
+ // during shutdown.
+ if (this._tab && this._tab.linkedBrowser) {
+ return this._tab.linkedBrowser.contentWindow;
+ }
+ return null;
+ },
+
+ get name() {
+ if (this.isAddon) {
+ return this._form.name;
+ }
+ return this._title;
+ },
+
+ get url() {
+ return this._url;
+ },
+
+ get isRemote() {
+ return !this.isLocalTab;
+ },
+
+ get isAddon() {
+ return !!(this._form && this._form.actor && (
+ this._form.actor.match(/conn\d+\.addon\d+/) ||
+ this._form.actor.match(/conn\d+\.webExtension\d+/)
+ ));
+ },
+
+ get isWebExtension() {
+ return !!(this._form && this._form.actor &&
+ this._form.actor.match(/conn\d+\.webExtension\d+/));
+ },
+
+ get isLocalTab() {
+ return !!this._tab;
+ },
+
+ get isMultiProcess() {
+ return !this.window;
+ },
+
+ /**
+ * Adds remote protocol capabilities to the target, so that it can be used
+ * for tools that support the Remote Debugging Protocol even for local
+ * connections.
+ */
+ makeRemote: function () {
+ if (this._remote) {
+ return this._remote.promise;
+ }
+
+ this._remote = defer();
+
+ if (this.isLocalTab) {
+ // Since a remote protocol connection will be made, let's start the
+ // DebuggerServer here, once and for all tools.
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ this._client = new DebuggerClient(DebuggerServer.connectPipe());
+ // A local TabTarget will never perform chrome debugging.
+ this._chrome = false;
+ }
+
+ this._setupRemoteListeners();
+
+ let attachTab = () => {
+ this._client.attachTab(this._form.actor, (response, tabClient) => {
+ if (!tabClient) {
+ this._remote.reject("Unable to attach to the tab");
+ return;
+ }
+ this.activeTab = tabClient;
+ this.threadActor = response.threadActor;
+
+ attachConsole();
+ });
+ };
+
+ let onConsoleAttached = (response, consoleClient) => {
+ if (!consoleClient) {
+ this._remote.reject("Unable to attach to the console");
+ return;
+ }
+ this.activeConsole = consoleClient;
+ this._remote.resolve(null);
+ };
+
+ let attachConsole = () => {
+ this._client.attachConsole(this._form.consoleActor,
+ [ "NetworkActivity" ],
+ onConsoleAttached);
+ };
+
+ if (this.isLocalTab) {
+ this._client.connect()
+ .then(() => this._client.getTab({ tab: this.tab }))
+ .then(response => {
+ this._form = response.tab;
+ this._url = this._form.url;
+ this._title = this._form.title;
+
+ attachTab();
+ }, e => this._remote.reject(e));
+ } else if (this.isTabActor) {
+ // In the remote debugging case, the protocol connection will have been
+ // already initialized in the connection screen code.
+ attachTab();
+ } else {
+ // AddonActor and chrome debugging on RootActor doesn't inherits from
+ // TabActor and doesn't need to be attached.
+ attachConsole();
+ }
+
+ return this._remote.promise;
+ },
+
+ /**
+ * Listen to the different events.
+ */
+ _setupListeners: function () {
+ this._webProgressListener = new TabWebProgressListener(this);
+ this.tab.linkedBrowser.addProgressListener(this._webProgressListener);
+ this.tab.addEventListener("TabClose", this);
+ this.tab.parentNode.addEventListener("TabSelect", this);
+ this.tab.ownerDocument.defaultView.addEventListener("unload", this);
+ this.tab.addEventListener("TabRemotenessChange", this);
+ },
+
+ /**
+ * Teardown event listeners.
+ */
+ _teardownListeners: function () {
+ if (this._webProgressListener) {
+ this._webProgressListener.destroy();
+ }
+
+ this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
+ this._tab.removeEventListener("TabClose", this);
+ this._tab.parentNode.removeEventListener("TabSelect", this);
+ this._tab.removeEventListener("TabRemotenessChange", this);
+ },
+
+ /**
+ * Setup listeners for remote debugging, updating existing ones as necessary.
+ */
+ _setupRemoteListeners: function () {
+ this.client.addListener("closed", this.destroy);
+
+ this._onTabDetached = (aType, aPacket) => {
+ // We have to filter message to ensure that this detach is for this tab
+ if (aPacket.from == this._form.actor) {
+ this.destroy();
+ }
+ };
+ this.client.addListener("tabDetached", this._onTabDetached);
+
+ this._onTabNavigated = (aType, aPacket) => {
+ let event = Object.create(null);
+ event.url = aPacket.url;
+ event.title = aPacket.title;
+ event.nativeConsoleAPI = aPacket.nativeConsoleAPI;
+ event.isFrameSwitching = aPacket.isFrameSwitching;
+
+ if (!aPacket.isFrameSwitching) {
+ // Update the title and url unless this is a frame switch.
+ this._url = aPacket.url;
+ this._title = aPacket.title;
+ }
+
+ // Send any stored event payload (DOMWindow or nsIRequest) for backwards
+ // compatibility with non-remotable tools.
+ if (aPacket.state == "start") {
+ event._navPayload = this._navRequest;
+ this.emit("will-navigate", event);
+ this._navRequest = null;
+ } else {
+ event._navPayload = this._navWindow;
+ this.emit("navigate", event);
+ this._navWindow = null;
+ }
+ };
+ this.client.addListener("tabNavigated", this._onTabNavigated);
+
+ this._onFrameUpdate = (aType, aPacket) => {
+ this.emit("frame-update", aPacket);
+ };
+ this.client.addListener("frameUpdate", this._onFrameUpdate);
+
+ this._onSourceUpdated = (event, packet) => this.emit("source-updated", packet);
+ this.client.addListener("newSource", this._onSourceUpdated);
+ this.client.addListener("updatedSource", this._onSourceUpdated);
+ },
+
+ /**
+ * Teardown listeners for remote debugging.
+ */
+ _teardownRemoteListeners: function () {
+ this.client.removeListener("closed", this.destroy);
+ this.client.removeListener("tabNavigated", this._onTabNavigated);
+ this.client.removeListener("tabDetached", this._onTabDetached);
+ this.client.removeListener("frameUpdate", this._onFrameUpdate);
+ this.client.removeListener("newSource", this._onSourceUpdated);
+ this.client.removeListener("updatedSource", this._onSourceUpdated);
+ },
+
+ /**
+ * Handle tabs events.
+ */
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "TabClose":
+ case "unload":
+ this.destroy();
+ break;
+ case "TabSelect":
+ if (this.tab.selected) {
+ this.emit("visible", event);
+ } else {
+ this.emit("hidden", event);
+ }
+ break;
+ case "TabRemotenessChange":
+ this.onRemotenessChange();
+ break;
+ }
+ },
+
+ // Automatically respawn the toolbox when the tab changes between being
+ // loaded within the parent process and loaded from a content process.
+ // Process change can go in both ways.
+ onRemotenessChange: function () {
+ // Responsive design do a crazy dance around tabs and triggers
+ // remotenesschange events. But we should ignore them as at the end
+ // the content doesn't change its remoteness.
+ if (this._tab.isResponsiveDesignMode) {
+ return;
+ }
+
+ // Save a reference to the tab as it will be nullified on destroy
+ let tab = this._tab;
+ let onToolboxDestroyed = (event, target) => {
+ if (target != this) {
+ return;
+ }
+ gDevTools.off("toolbox-destroyed", target);
+
+ // Recreate a fresh target instance as the current one is now destroyed
+ let newTarget = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(newTarget);
+ };
+ gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
+ },
+
+ /**
+ * Target is not alive anymore.
+ */
+ destroy: function () {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer.promise;
+ }
+
+ this._destroyer = defer();
+
+ // Before taking any action, notify listeners that destruction is imminent.
+ this.emit("close");
+
+ if (this._tab) {
+ this._teardownListeners();
+ }
+
+ let cleanupAndResolve = () => {
+ this._cleanup();
+ this._destroyer.resolve(null);
+ };
+ // If this target was not remoted, the promise will be resolved before the
+ // function returns.
+ if (this._tab && !this._client) {
+ cleanupAndResolve();
+ } else if (this._client) {
+ // If, on the other hand, this target was remoted, the promise will be
+ // resolved after the remote connection is closed.
+ this._teardownRemoteListeners();
+
+ if (this.isLocalTab) {
+ // We started with a local tab and created the client ourselves, so we
+ // should close it.
+ this._client.close().then(cleanupAndResolve);
+ } else if (this.activeTab) {
+ // The client was handed to us, so we are not responsible for closing
+ // it. We just need to detach from the tab, if already attached.
+ // |detach| may fail if the connection is already dead, so proceed with
+ // cleanup directly after this.
+ this.activeTab.detach();
+ cleanupAndResolve();
+ } else {
+ cleanupAndResolve();
+ }
+ }
+
+ return this._destroyer.promise;
+ },
+
+ /**
+ * Clean up references to what this target points to.
+ */
+ _cleanup: function () {
+ if (this._tab) {
+ targets.delete(this._tab);
+ } else {
+ promiseTargets.delete(this._form);
+ }
+
+ this.activeTab = null;
+ this.activeConsole = null;
+ this._client = null;
+ this._tab = null;
+ this._form = null;
+ this._remote = null;
+ this._root = null;
+ this._title = null;
+ this._url = null;
+ this.threadActor = null;
+ },
+
+ toString: function () {
+ let id = this._tab ? this._tab : (this._form && this._form.actor);
+ return `TabTarget:${id}`;
+ },
+
+ /**
+ * @see TabActor.prototype.onResolveLocation
+ */
+ resolveLocation(loc) {
+ let deferred = defer();
+
+ this.client.request(Object.assign({
+ to: this._form.actor,
+ type: "resolveLocation",
+ }, loc), deferred.resolve);
+
+ return deferred.promise;
+ },
+};
+
+/**
+ * WebProgressListener for TabTarget.
+ *
+ * @param object aTarget
+ * The TabTarget instance to work with.
+ */
+function TabWebProgressListener(aTarget) {
+ this.target = aTarget;
+}
+
+TabWebProgressListener.prototype = {
+ target: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+
+ onStateChange: function (progress, request, flag) {
+ let isStart = flag & Ci.nsIWebProgressListener.STATE_START;
+ let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+ let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST;
+
+ // Skip non-interesting states.
+ if (!isStart || !isDocument || !isRequest || !isNetwork) {
+ return;
+ }
+
+ // emit event if the top frame is navigating
+ if (progress.isTopLevel) {
+ // Emit the event if the target is not remoted or store the payload for
+ // later emission otherwise.
+ if (this.target._client) {
+ this.target._navRequest = request;
+ } else {
+ this.target.emit("will-navigate", request);
+ }
+ }
+ },
+
+ onProgressChange: function () {},
+ onSecurityChange: function () {},
+ onStatusChange: function () {},
+
+ onLocationChange: function (webProgress, request, URI, flags) {
+ if (this.target &&
+ !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
+ let window = webProgress.DOMWindow;
+ // Emit the event if the target is not remoted or store the payload for
+ // later emission otherwise.
+ if (this.target._client) {
+ this.target._navWindow = window;
+ } else {
+ this.target.emit("navigate", window);
+ }
+ }
+ },
+
+ /**
+ * Destroy the progress listener instance.
+ */
+ destroy: function () {
+ if (this.target.tab) {
+ try {
+ this.target.tab.linkedBrowser.removeProgressListener(this);
+ } catch (ex) {
+ // This can throw when a tab crashes in e10s.
+ }
+ }
+ this.target._webProgressListener = null;
+ this.target._navRequest = null;
+ this.target._navWindow = null;
+ this.target = null;
+ }
+};
+
+function WorkerTarget(workerClient) {
+ EventEmitter.decorate(this);
+ this._workerClient = workerClient;
+}
+
+/**
+ * A WorkerTarget represents a worker. Unlike TabTarget, which can represent
+ * either a local or remote tab, WorkerTarget always represents a remote worker.
+ * Moreover, unlike TabTarget, which is constructed with a placeholder object
+ * for remote tabs (from which a TabClient can then be lazily obtained),
+ * WorkerTarget is constructed with a WorkerClient directly.
+ *
+ * WorkerClient is designed to mimic the interface of TabClient as closely as
+ * possible. This allows us to debug workers as if they were ordinary tabs,
+ * requiring only minimal changes to the rest of the frontend.
+ */
+WorkerTarget.prototype = {
+ get isRemote() {
+ return true;
+ },
+
+ get isTabActor() {
+ return true;
+ },
+
+ get name() {
+ return "Worker";
+ },
+
+ get url() {
+ return this._workerClient.url;
+ },
+
+ get isWorkerTarget() {
+ return true;
+ },
+
+ get form() {
+ return {
+ consoleActor: this._workerClient.consoleActor
+ };
+ },
+
+ get activeTab() {
+ return this._workerClient;
+ },
+
+ get client() {
+ return this._workerClient.client;
+ },
+
+ destroy: function () {
+ this._workerClient.detach();
+ },
+
+ hasActor: function (name) {
+ // console is the only one actor implemented by WorkerActor
+ if (name == "console") {
+ return true;
+ }
+ return false;
+ },
+
+ getTrait: function () {
+ return undefined;
+ },
+
+ makeRemote: function () {
+ return Promise.resolve();
+ }
+};
diff --git a/devtools/client/framework/test/.eslintrc.js b/devtools/client/framework/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/framework/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/framework/test/browser.ini b/devtools/client/framework/test/browser.ini
new file mode 100644
index 000000000..f34cd66f0
--- /dev/null
+++ b/devtools/client/framework/test/browser.ini
@@ -0,0 +1,95 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ browser_toolbox_options_disable_js.html
+ browser_toolbox_options_disable_js_iframe.html
+ browser_toolbox_options_disable_cache.sjs
+ browser_toolbox_sidebar_tool.xul
+ browser_toolbox_window_title_changes_page.html
+ browser_toolbox_window_title_frame_select_page.html
+ code_binary_search.coffee
+ code_binary_search.js
+ code_binary_search.map
+ code_math.js
+ code_ugly.js
+ doc_empty-tab-01.html
+ head.js
+ shared-head.js
+ shared-redux-head.js
+ helper_disable_cache.js
+ doc_theme.css
+ doc_viewsource.html
+ browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
+ browser_toolbox_options_enable_serviceworkers_testing.html
+ serviceworker.js
+
+[browser_browser_toolbox.js]
+[browser_browser_toolbox_debugger.js]
+[browser_devtools_api.js]
+[browser_devtools_api_destroy.js]
+[browser_dynamic_tool_enabling.js]
+[browser_ignore_toolbox_network_requests.js]
+[browser_keybindings_01.js]
+[browser_keybindings_02.js]
+[browser_keybindings_03.js]
+[browser_menu_api.js]
+[browser_new_activation_workflow.js]
+[browser_source_map-01.js]
+[browser_source_map-02.js]
+[browser_target_from_url.js]
+[browser_target_events.js]
+[browser_target_remote.js]
+[browser_target_support.js]
+[browser_toolbox_custom_host.js]
+[browser_toolbox_dynamic_registration.js]
+[browser_toolbox_getpanelwhenready.js]
+[browser_toolbox_highlight.js]
+[browser_toolbox_hosts.js]
+[browser_toolbox_hosts_size.js]
+[browser_toolbox_hosts_telemetry.js]
+[browser_toolbox_keyboard_navigation.js]
+skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard Preferences
+[browser_toolbox_minimize.js]
+skip-if = true # Bug 1177463 - Temporarily hide the minimize button
+[browser_toolbox_options.js]
+[browser_toolbox_options_disable_buttons.js]
+[browser_toolbox_options_disable_cache-01.js]
+[browser_toolbox_options_disable_cache-02.js]
+[browser_toolbox_options_disable_js.js]
+[browser_toolbox_options_enable_serviceworkers_testing.js]
+# [browser_toolbox_raise.js] # Bug 962258
+# skip-if = os == "win"
+[browser_toolbox_races.js]
+[browser_toolbox_ready.js]
+[browser_toolbox_remoteness_change.js]
+run-if = e10s
+[browser_toolbox_select_event.js]
+skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
+[browser_toolbox_selected_tool_unavailable.js]
+[browser_toolbox_sidebar.js]
+[browser_toolbox_sidebar_events.js]
+[browser_toolbox_sidebar_existing_tabs.js]
+[browser_toolbox_sidebar_overflow_menu.js]
+[browser_toolbox_split_console.js]
+[browser_toolbox_target.js]
+[browser_toolbox_tabsswitch_shortcuts.js]
+[browser_toolbox_textbox_context_menu.js]
+[browser_toolbox_theme_registration.js]
+[browser_toolbox_toggle.js]
+[browser_toolbox_tool_ready.js]
+[browser_toolbox_tool_remote_reopen.js]
+[browser_toolbox_transport_events.js]
+[browser_toolbox_view_source_01.js]
+[browser_toolbox_view_source_02.js]
+[browser_toolbox_view_source_03.js]
+[browser_toolbox_view_source_04.js]
+[browser_toolbox_window_reload_target.js]
+[browser_toolbox_window_shortcuts.js]
+skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5.1" # Bug 851129 - Re-enable browser_toolbox_window_shortcuts.js test after leaks are fixed
+[browser_toolbox_window_title_changes.js]
+[browser_toolbox_window_title_frame_select.js]
+[browser_toolbox_zoom.js]
+[browser_two_tabs.js]
+# We want this test to run for mochitest-dt as well, so we include it here:
+[../../../../browser/base/content/test/general/browser_parsable_css.js]
diff --git a/devtools/client/framework/test/browser_browser_toolbox.js b/devtools/client/framework/test/browser_browser_toolbox.js
new file mode 100644
index 000000000..08c8ac190
--- /dev/null
+++ b/devtools/client/framework/test/browser_browser_toolbox.js
@@ -0,0 +1,65 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// On debug test slave, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+add_task(function* runTest() {
+ yield new Promise(done => {
+ let options = {"set": [
+ ["devtools.debugger.prompt-connection", false],
+ ["devtools.debugger.remote-enabled", true],
+ ["devtools.chrome.enabled", true],
+ // Test-only pref to allow passing `testScript` argument to the browser
+ // toolbox
+ ["devtools.browser-toolbox.allow-unsafe-script", true],
+ // On debug test slave, it takes more than the default time (20s)
+ // to get a initialized console
+ ["devtools.debugger.remote-timeout", 120000]
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ // Wait for a notification sent by a script evaluated in the webconsole
+ // of the browser toolbox.
+ let onCustomMessage = new Promise(done => {
+ Services.obs.addObserver(function listener() {
+ Services.obs.removeObserver(listener, "browser-toolbox-console-works");
+ done();
+ }, "browser-toolbox-console-works", false);
+ });
+
+ // Be careful, this JS function is going to be executed in the addon toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
+ let testScript = function () {
+ toolbox.selectTool("webconsole")
+ .then(console => {
+ let { jsterm } = console.hud;
+ let js = "Services.obs.notifyObservers(null, 'browser-toolbox-console-works', null);";
+ return jsterm.execute(js);
+ })
+ .then(() => toolbox.destroy());
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+ let closePromise;
+ yield new Promise(onRun => {
+ closePromise = new Promise(onClose => {
+ info("Opening the browser toolbox\n");
+ BrowserToolboxProcess.init(onClose, onRun);
+ });
+ });
+ ok(true, "Browser toolbox started\n");
+
+ yield onCustomMessage;
+ ok(true, "Received the custom message");
+
+ yield closePromise;
+ ok(true, "Browser toolbox process just closed");
+});
diff --git a/devtools/client/framework/test/browser_browser_toolbox_debugger.js b/devtools/client/framework/test/browser_browser_toolbox_debugger.js
new file mode 100644
index 000000000..c0971cc7c
--- /dev/null
+++ b/devtools/client/framework/test/browser_browser_toolbox_debugger.js
@@ -0,0 +1,131 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// On debug test runner, it takes about 50s to run the test.
+requestLongerTimeout(4);
+
+const { setInterval, clearInterval } = require("sdk/timers");
+
+add_task(function* runTest() {
+ yield new Promise(done => {
+ let options = {"set": [
+ ["devtools.debugger.prompt-connection", false],
+ ["devtools.debugger.remote-enabled", true],
+ ["devtools.chrome.enabled", true],
+ // Test-only pref to allow passing `testScript` argument to the browser
+ // toolbox
+ ["devtools.browser-toolbox.allow-unsafe-script", true],
+ // On debug test runner, it takes more than the default time (20s)
+ // to get a initialized console
+ ["devtools.debugger.remote-timeout", 120000]
+ ]};
+ SpecialPowers.pushPrefEnv(options, done);
+ });
+
+ let s = Cu.Sandbox("http://mozilla.org");
+ // Pass a fake URL to evalInSandbox. If we just pass a filename,
+ // Debugger is going to fail and only display root folder (`/`) listing.
+ // But it won't try to fetch this url and use sandbox content as expected.
+ let testUrl = "http://mozilla.org/browser-toolbox-test.js";
+ Cu.evalInSandbox("(" + function () {
+ this.plop = function plop() {
+ return 1;
+ };
+ } + ").call(this)", s, "1.8", testUrl, 0);
+
+ // Execute the function every second in order to trigger the breakpoint
+ let interval = setInterval(s.plop, 1000);
+
+ // Be careful, this JS function is going to be executed in the browser toolbox,
+ // which lives in another process. So do not try to use any scope variable!
+ let env = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ let testScript = function () {
+ const { Task } = Components.utils.import("resource://gre/modules/Task.jsm", {});
+ dump("Opening the browser toolbox and debugger panel\n");
+ let window, document;
+ let testUrl = "http://mozilla.org/browser-toolbox-test.js";
+ Task.spawn(function* () {
+ dump("Waiting for debugger load\n");
+ let panel = yield toolbox.selectTool("jsdebugger");
+ let window = panel.panelWin;
+ let document = window.document;
+
+ yield window.once(window.EVENTS.SOURCE_SHOWN);
+
+ dump("Loaded, selecting the test script to debug\n");
+ let item = document.querySelector(`.dbg-source-item[tooltiptext="${testUrl}"]`);
+ let onSourceShown = window.once(window.EVENTS.SOURCE_SHOWN);
+ item.click();
+ yield onSourceShown;
+
+ dump("Selected, setting a breakpoint\n");
+ let { Sources, editor } = window.DebuggerView;
+ let onBreak = window.once(window.EVENTS.FETCHED_SCOPES);
+ editor.emit("gutterClick", 1);
+ yield onBreak;
+
+ dump("Paused, asserting breakpoint position\n");
+ let url = Sources.selectedItem.attachment.source.url;
+ if (url != testUrl) {
+ throw new Error("Breaking on unexpected script: " + url);
+ }
+ let cursor = editor.getCursor();
+ if (cursor.line != 1) {
+ throw new Error("Breaking on unexpected line: " + cursor.line);
+ }
+
+ dump("Now, stepping over\n");
+ let stepOver = window.document.querySelector("#step-over");
+ let onFetchedScopes = window.once(window.EVENTS.FETCHED_SCOPES);
+ stepOver.click();
+ yield onFetchedScopes;
+
+ dump("Stepped, asserting step position\n");
+ url = Sources.selectedItem.attachment.source.url;
+ if (url != testUrl) {
+ throw new Error("Stepping on unexpected script: " + url);
+ }
+ cursor = editor.getCursor();
+ if (cursor.line != 2) {
+ throw new Error("Stepping on unexpected line: " + cursor.line);
+ }
+
+ dump("Resume script execution\n");
+ let resume = window.document.querySelector("#resume");
+ let onResume = toolbox.target.once("thread-resumed");
+ resume.click();
+ yield onResume;
+
+ dump("Close the browser toolbox\n");
+ toolbox.destroy();
+
+ }).catch(error => {
+ dump("Error while running code in the browser toolbox process:\n");
+ dump(error + "\n");
+ dump("stack:\n" + error.stack + "\n");
+ });
+ };
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+ registerCleanupFunction(() => {
+ env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+ });
+
+ let { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+ // Use two promises, one for each BrowserToolboxProcess.init callback
+ // arguments, to ensure that we wait for toolbox run and close events.
+ let closePromise;
+ yield new Promise(onRun => {
+ closePromise = new Promise(onClose => {
+ info("Opening the browser toolbox\n");
+ BrowserToolboxProcess.init(onClose, onRun);
+ });
+ });
+ ok(true, "Browser toolbox started\n");
+
+ yield closePromise;
+ ok(true, "Browser toolbox process just closed");
+
+ clearInterval(interval);
+});
diff --git a/devtools/client/framework/test/browser_devtools_api.js b/devtools/client/framework/test/browser_devtools_api.js
new file mode 100644
index 000000000..72d415c0b
--- /dev/null
+++ b/devtools/client/framework/test/browser_devtools_api.js
@@ -0,0 +1,264 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
+
+// When running in a standalone directory, we get this error
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.doc is undefined");
+
+// Tests devtools API
+
+const toolId1 = "test-tool-1";
+const toolId2 = "test-tool-2";
+
+var EventEmitter = require("devtools/shared/event-emitter");
+
+function test() {
+ addTab("about:blank").then(runTests1);
+}
+
+// Test scenario 1: the tool definition build method returns a promise.
+function runTests1(aTab) {
+ let toolDefinition = {
+ id: toolId1,
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel",
+ build: function (iframeWindow, toolbox) {
+ let panel = new DevToolPanel(iframeWindow, toolbox);
+ return panel.open();
+ },
+ };
+
+ ok(gDevTools, "gDevTools exists");
+ ok(!gDevTools.getToolDefinitionMap().has(toolId1),
+ "The tool is not registered");
+
+ gDevTools.registerTool(toolDefinition);
+ ok(gDevTools.getToolDefinitionMap().has(toolId1),
+ "The tool is registered");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let events = {};
+
+ // Check events on the gDevTools and toolbox objects.
+ gDevTools.once(toolId1 + "-init", (event, toolbox, iframe) => {
+ ok(iframe, "iframe argument available");
+
+ toolbox.once(toolId1 + "-init", (event, iframe) => {
+ ok(iframe, "iframe argument available");
+ events["init"] = true;
+ });
+ });
+
+ gDevTools.once(toolId1 + "-ready", (event, toolbox, panel) => {
+ ok(panel, "panel argument available");
+
+ toolbox.once(toolId1 + "-ready", (event, panel) => {
+ ok(panel, "panel argument available");
+ events["ready"] = true;
+ });
+ });
+
+ gDevTools.showToolbox(target, toolId1).then(function (toolbox) {
+ is(toolbox.target, target, "toolbox target is correct");
+ is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
+
+ ok(events["init"], "init event fired");
+ ok(events["ready"], "ready event fired");
+
+ gDevTools.unregisterTool(toolId1);
+
+ // Wait for unregisterTool to select the next tool before calling runTests2,
+ // otherwise we will receive the wrong select event when waiting for
+ // unregisterTool to select the next tool in continueTests below.
+ toolbox.once("select", runTests2);
+ });
+}
+
+// Test scenario 2: the tool definition build method returns panel instance.
+function runTests2() {
+ let toolDefinition = {
+ id: toolId2,
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel",
+ build: function (iframeWindow, toolbox) {
+ return new DevToolPanel(iframeWindow, toolbox);
+ },
+ };
+
+ ok(!gDevTools.getToolDefinitionMap().has(toolId2),
+ "The tool is not registered");
+
+ gDevTools.registerTool(toolDefinition);
+ ok(gDevTools.getToolDefinitionMap().has(toolId2),
+ "The tool is registered");
+
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let events = {};
+
+ // Check events on the gDevTools and toolbox objects.
+ gDevTools.once(toolId2 + "-init", (event, toolbox, iframe) => {
+ ok(iframe, "iframe argument available");
+
+ toolbox.once(toolId2 + "-init", (event, iframe) => {
+ ok(iframe, "iframe argument available");
+ events["init"] = true;
+ });
+ });
+
+ gDevTools.once(toolId2 + "-build", (event, toolbox, panel, iframe) => {
+ ok(panel, "panel argument available");
+
+ toolbox.once(toolId2 + "-build", (event, panel, iframe) => {
+ ok(panel, "panel argument available");
+ events["build"] = true;
+ });
+ });
+
+ gDevTools.once(toolId2 + "-ready", (event, toolbox, panel) => {
+ ok(panel, "panel argument available");
+
+ toolbox.once(toolId2 + "-ready", (event, panel) => {
+ ok(panel, "panel argument available");
+ events["ready"] = true;
+ });
+ });
+
+ gDevTools.showToolbox(target, toolId2).then(function (toolbox) {
+ is(toolbox.target, target, "toolbox target is correct");
+ is(toolbox.target.tab, gBrowser.selectedTab, "targeted tab is correct");
+
+ ok(events["init"], "init event fired");
+ ok(events["build"], "build event fired");
+ ok(events["ready"], "ready event fired");
+
+ continueTests(toolbox);
+ });
+}
+
+var continueTests = Task.async(function* (toolbox, panel) {
+ ok(toolbox.getCurrentPanel(), "panel value is correct");
+ is(toolbox.currentToolId, toolId2, "toolbox _currentToolId is correct");
+
+ ok(!toolbox.doc.getElementById("toolbox-tab-" + toolId2).hasAttribute("icon-invertable"),
+ "The tool tab does not have the invertable attribute");
+
+ ok(toolbox.doc.getElementById("toolbox-tab-inspector").hasAttribute("icon-invertable"),
+ "The builtin tool tabs do have the invertable attribute");
+
+ let toolDefinitions = gDevTools.getToolDefinitionMap();
+ ok(toolDefinitions.has(toolId2), "The tool is in gDevTools");
+
+ let toolDefinition = toolDefinitions.get(toolId2);
+ is(toolDefinition.id, toolId2, "toolDefinition id is correct");
+
+ info("Testing toolbox tool-unregistered event");
+ let toolSelected = toolbox.once("select");
+ let unregisteredTool = yield new Promise(resolve => {
+ toolbox.once("tool-unregistered", (e, id) => resolve(id));
+ gDevTools.unregisterTool(toolId2);
+ });
+ yield toolSelected;
+
+ is(unregisteredTool, toolId2, "Event returns correct id");
+ ok(!toolbox.isToolRegistered(toolId2),
+ "Toolbox: The tool is not registered");
+ ok(!gDevTools.getToolDefinitionMap().has(toolId2),
+ "The tool is no longer registered");
+
+ info("Testing toolbox tool-registered event");
+ let registeredTool = yield new Promise(resolve => {
+ toolbox.once("tool-registered", (e, id) => resolve(id));
+ gDevTools.registerTool(toolDefinition);
+ });
+
+ is(registeredTool, toolId2, "Event returns correct id");
+ ok(toolbox.isToolRegistered(toolId2),
+ "Toolbox: The tool is registered");
+ ok(gDevTools.getToolDefinitionMap().has(toolId2),
+ "The tool is registered");
+
+ info("Unregistering tool");
+ gDevTools.unregisterTool(toolId2);
+
+ destroyToolbox(toolbox);
+});
+
+function destroyToolbox(toolbox) {
+ toolbox.destroy().then(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ ok(gDevTools._toolboxes.get(target) == null, "gDevTools doesn't know about target");
+ ok(toolbox.target == null, "toolbox doesn't know about target.");
+ finishUp();
+ });
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+/**
+* When a Toolbox is started it creates a DevToolPanel for each of the tools
+* by calling toolDefinition.build(). The returned object should
+* at least implement these functions. They will be used by the ToolBox.
+*
+* There may be no benefit in doing this as an abstract type, but if nothing
+* else gives us a place to write documentation.
+*/
+function DevToolPanel(iframeWindow, toolbox) {
+ EventEmitter.decorate(this);
+
+ this._toolbox = toolbox;
+
+ /* let doc = iframeWindow.document
+ let label = doc.createElement("label");
+ let textNode = doc.createTextNode("Some Tool");
+
+ label.appendChild(textNode);
+ doc.body.appendChild(label);*/
+}
+
+DevToolPanel.prototype = {
+ open: function () {
+ let deferred = defer();
+
+ executeSoon(() => {
+ this._isReady = true;
+ this.emit("ready");
+ deferred.resolve(this);
+ });
+
+ return deferred.promise;
+ },
+
+ get target() {
+ return this._toolbox.target;
+ },
+
+ get toolbox() {
+ return this._toolbox;
+ },
+
+ get isReady() {
+ return this._isReady;
+ },
+
+ _isReady: false,
+
+ destroy: function DTI_destroy() {
+ return defer(null);
+ },
+};
diff --git a/devtools/client/framework/test/browser_devtools_api_destroy.js b/devtools/client/framework/test/browser_devtools_api_destroy.js
new file mode 100644
index 000000000..084a7a0a1
--- /dev/null
+++ b/devtools/client/framework/test/browser_devtools_api_destroy.js
@@ -0,0 +1,71 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests devtools API
+
+function test() {
+ addTab("about:blank").then(runTests);
+}
+
+function runTests(aTab) {
+ let toolDefinition = {
+ id: "testTool",
+ visibilityswitch: "devtools.testTool.enabled",
+ isTargetSupported: () => true,
+ url: "about:blank",
+ label: "someLabel",
+ build: function (iframeWindow, toolbox) {
+ let deferred = defer();
+ executeSoon(() => {
+ deferred.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: function () {},
+ });
+ });
+ return deferred.promise;
+ },
+ };
+
+ gDevTools.registerTool(toolDefinition);
+
+ let collectedEvents = [];
+
+ let target = TargetFactory.forTab(aTab);
+ gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
+ let panel = toolbox.getPanel(toolDefinition.id);
+ ok(panel, "Tool open");
+
+ gDevTools.once("toolbox-destroy", (event, toolbox, iframe) => {
+ collectedEvents.push(event);
+ });
+
+ gDevTools.once(toolDefinition.id + "-destroy", (event, toolbox, iframe) => {
+ collectedEvents.push("gDevTools-" + event);
+ });
+
+ toolbox.once("destroy", (event) => {
+ collectedEvents.push(event);
+ });
+
+ toolbox.once(toolDefinition.id + "-destroy", (event) => {
+ collectedEvents.push("toolbox-" + event);
+ });
+
+ toolbox.destroy().then(function () {
+ is(collectedEvents.join(":"),
+ "toolbox-destroy:destroy:gDevTools-testTool-destroy:toolbox-testTool-destroy",
+ "Found the right amount of collected events.");
+
+ gDevTools.unregisterTool(toolDefinition.id);
+ gBrowser.removeCurrentTab();
+
+ executeSoon(function () {
+ finish();
+ });
+ });
+ });
+}
diff --git a/devtools/client/framework/test/browser_dynamic_tool_enabling.js b/devtools/client/framework/test/browser_dynamic_tool_enabling.js
new file mode 100644
index 000000000..6420afabe
--- /dev/null
+++ b/devtools/client/framework/test/browser_dynamic_tool_enabling.js
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that toggling prefs immediately (de)activates the relevant menuitem
+
+var gItemsToTest = {
+ "menu_devToolbar": "devtools.toolbar.enabled",
+ "menu_browserToolbox": ["devtools.chrome.enabled", "devtools.debugger.remote-enabled"],
+ "menu_devtools_connect": "devtools.debugger.remote-enabled",
+};
+
+function expectedAttributeValueFromPrefs(prefs) {
+ return prefs.every((pref) => Services.prefs.getBoolPref(pref)) ?
+ "" : "true";
+}
+
+function checkItem(el, prefs) {
+ let expectedValue = expectedAttributeValueFromPrefs(prefs);
+ is(el.getAttribute("disabled"), expectedValue, "disabled attribute should match current pref state");
+ is(el.getAttribute("hidden"), expectedValue, "hidden attribute should match current pref state");
+}
+
+function test() {
+ for (let k in gItemsToTest) {
+ let el = document.getElementById(k);
+ let prefs = gItemsToTest[k];
+ if (typeof prefs == "string") {
+ prefs = [prefs];
+ }
+ checkItem(el, prefs);
+ for (let pref of prefs) {
+ Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
+ checkItem(el, prefs);
+ Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
+ checkItem(el, prefs);
+ }
+ }
+ finish();
+}
diff --git a/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js b/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js
new file mode 100644
index 000000000..1cfc22f7e
--- /dev/null
+++ b/devtools/client/framework/test/browser_ignore_toolbox_network_requests.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that network requests originating from the toolbox don't get recorded in
+// the network panel.
+
+add_task(function* () {
+ // TODO: This test tries to verify the normal behavior of the netmonitor and
+ // therefore needs to avoid the explicit check for tests. Bug 1167188 will
+ // allow us to remove this workaround.
+ let isTesting = flags.testing;
+ flags.testing = false;
+
+ let tab = yield addTab(URL_ROOT + "doc_viewsource.html");
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
+ let panel = toolbox.getPanel("styleeditor");
+
+ is(panel.UI.editors.length, 1, "correct number of editors opened");
+
+ let monitor = yield toolbox.selectTool("netmonitor");
+ let { RequestsMenu } = monitor.panelWin.NetMonitorView;
+ is(RequestsMenu.itemCount, 0, "No network requests appear in the network panel");
+
+ yield gDevTools.closeToolbox(target);
+ tab = target = toolbox = panel = null;
+ gBrowser.removeCurrentTab();
+ flags.testing = isTesting;
+});
diff --git a/devtools/client/framework/test/browser_keybindings_01.js b/devtools/client/framework/test/browser_keybindings_01.js
new file mode 100644
index 000000000..4e4effb07
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_01.js
@@ -0,0 +1,115 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the keybindings for opening and closing the inspector work as expected
+// Can probably make this a shared test that tests all of the tools global keybindings
+const TEST_URL = "data:text/html,<html><head><title>Test for the " +
+ "highlighter keybindings</title></head><body>" +
+ "<h1>Keybindings!</h1></body></html>"
+function test()
+{
+ waitForExplicitFinish();
+
+ let doc;
+ let node;
+ let inspector;
+ let keysetMap = { };
+
+ addTab(TEST_URL).then(function () {
+ doc = content.document;
+ node = doc.querySelector("h1");
+ waitForFocus(setupKeyBindingsTest);
+ });
+
+ function buildDevtoolsKeysetMap(keyset) {
+ [].forEach.call(keyset.querySelectorAll("key"), function (key) {
+
+ if (!key.getAttribute("key")) {
+ return;
+ }
+
+ let modifiers = key.getAttribute("modifiers");
+
+ keysetMap[key.id.split("_")[1]] = {
+ key: key.getAttribute("key"),
+ modifiers: modifiers,
+ modifierOpt: {
+ shiftKey: modifiers.match("shift"),
+ ctrlKey: modifiers.match("ctrl"),
+ altKey: modifiers.match("alt"),
+ metaKey: modifiers.match("meta"),
+ accelKey: modifiers.match("accel")
+ },
+ synthesizeKey: function () {
+ EventUtils.synthesizeKey(this.key, this.modifierOpt);
+ }
+ };
+ });
+ }
+
+ function setupKeyBindingsTest()
+ {
+ for (let win of gDevToolsBrowser._trackedBrowserWindows) {
+ buildDevtoolsKeysetMap(win.document.getElementById("devtoolsKeyset"));
+ }
+
+ gDevTools.once("toolbox-ready", (e, toolbox) => {
+ inspectorShouldBeOpenAndHighlighting(toolbox.getCurrentPanel(), toolbox);
+ });
+
+ keysetMap.inspector.synthesizeKey();
+ }
+
+ function inspectorShouldBeOpenAndHighlighting(aInspector, aToolbox)
+ {
+ is(aToolbox.currentToolId, "inspector", "Correct tool has been loaded");
+
+ aToolbox.once("picker-started", () => {
+ ok(true, "picker-started event received, highlighter started");
+ keysetMap.inspector.synthesizeKey();
+
+ aToolbox.once("picker-stopped", () => {
+ ok(true, "picker-stopped event received, highlighter stopped");
+ gDevTools.once("select-tool-command", () => {
+ webconsoleShouldBeSelected(aToolbox);
+ });
+ keysetMap.webconsole.synthesizeKey();
+ });
+ });
+ }
+
+ function webconsoleShouldBeSelected(aToolbox)
+ {
+ is(aToolbox.currentToolId, "webconsole", "webconsole should be selected.");
+
+ gDevTools.once("select-tool-command", () => {
+ jsdebuggerShouldBeSelected(aToolbox);
+ });
+ keysetMap.jsdebugger.synthesizeKey();
+ }
+
+ function jsdebuggerShouldBeSelected(aToolbox)
+ {
+ is(aToolbox.currentToolId, "jsdebugger", "jsdebugger should be selected.");
+
+ gDevTools.once("select-tool-command", () => {
+ netmonitorShouldBeSelected(aToolbox);
+ });
+
+ keysetMap.netmonitor.synthesizeKey();
+ }
+
+ function netmonitorShouldBeSelected(aToolbox, panel)
+ {
+ is(aToolbox.currentToolId, "netmonitor", "netmonitor should be selected.");
+ finishUp();
+ }
+
+ function finishUp() {
+ doc = node = inspector = keysetMap = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ }
+}
diff --git a/devtools/client/framework/test/browser_keybindings_02.js b/devtools/client/framework/test/browser_keybindings_02.js
new file mode 100644
index 000000000..551fef873
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_02.js
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the toolbox keybindings still work after the host is changed.
+
+const URL = "data:text/html;charset=utf8,test page";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+function getZoomValue() {
+ return parseFloat(Services.prefs.getCharPref("devtools.toolbox.zoomValue"));
+}
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ let {SIDE, BOTTOM} = Toolbox.HostType;
+ for (let type of [SIDE, BOTTOM, SIDE]) {
+ info("Switch to host type " + type);
+ yield toolbox.switchHost(type);
+
+ info("Try to use the toolbox shortcuts");
+ yield checkKeyBindings(toolbox);
+ }
+
+ Services.prefs.clearUserPref("devtools.toolbox.zoomValue");
+ Services.prefs.setCharPref("devtools.toolbox.host", BOTTOM);
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function zoomWithKey(toolbox, key) {
+ let shortcut = L10N.getStr(key);
+ if (!shortcut) {
+ info("Key was empty, skipping zoomWithKey");
+ return;
+ }
+ info("Zooming with key: " + key);
+ let currentZoom = getZoomValue();
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+ isnot(getZoomValue(), currentZoom, "The zoom level was changed in the toolbox");
+}
+
+function* checkKeyBindings(toolbox) {
+ zoomWithKey(toolbox, "toolbox.zoomIn.key");
+ zoomWithKey(toolbox, "toolbox.zoomIn2.key");
+ zoomWithKey(toolbox, "toolbox.zoomIn3.key");
+
+ zoomWithKey(toolbox, "toolbox.zoomReset.key");
+
+ zoomWithKey(toolbox, "toolbox.zoomOut.key");
+ zoomWithKey(toolbox, "toolbox.zoomOut2.key");
+
+ zoomWithKey(toolbox, "toolbox.zoomReset2.key");
+}
diff --git a/devtools/client/framework/test/browser_keybindings_03.js b/devtools/client/framework/test/browser_keybindings_03.js
new file mode 100644
index 000000000..752087a09
--- /dev/null
+++ b/devtools/client/framework/test/browser_keybindings_03.js
@@ -0,0 +1,53 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the toolbox 'switch to previous host' feature works.
+// Pressing ctrl/cmd+shift+d should switch to the last used host.
+
+const URL = "data:text/html;charset=utf8,test page for toolbox switching";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ let shortcut = L10N.getStr("toolbox.toggleHost.key");
+
+ let {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
+ checkHostType(toolbox, BOTTOM, SIDE);
+
+ info("Switching from bottom to side");
+ let onHostChanged = toolbox.once("host-changed");
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+ yield onHostChanged;
+ checkHostType(toolbox, SIDE, BOTTOM);
+
+ info("Switching from side to bottom");
+ onHostChanged = toolbox.once("host-changed");
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+ yield onHostChanged;
+ checkHostType(toolbox, BOTTOM, SIDE);
+
+ info("Switching to window");
+ yield toolbox.switchHost(WINDOW);
+ checkHostType(toolbox, WINDOW, BOTTOM);
+
+ info("Switching from window to bottom");
+ onHostChanged = toolbox.once("host-changed");
+ synthesizeKeyShortcut(shortcut, toolbox.win);
+ yield onHostChanged;
+ checkHostType(toolbox, BOTTOM, WINDOW);
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_menu_api.js b/devtools/client/framework/test/browser_menu_api.js
new file mode 100644
index 000000000..cf634ff6f
--- /dev/null
+++ b/devtools/client/framework/test/browser_menu_api.js
@@ -0,0 +1,181 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the Menu API works
+
+const URL = "data:text/html;charset=utf8,test page for menu api";
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ yield testMenuItems();
+ yield testMenuPopup(toolbox);
+ yield testSubmenu(toolbox);
+});
+
+function* testMenuItems() {
+ let menu = new Menu();
+ let menuItem1 = new MenuItem();
+ let menuItem2 = new MenuItem();
+
+ menu.append(menuItem1);
+ menu.append(menuItem2);
+
+ is(menu.items.length, 2, "Correct number of 'items'");
+ is(menu.items[0], menuItem1, "Correct reference to MenuItem");
+ is(menu.items[1], menuItem2, "Correct reference to MenuItem");
+}
+
+function* testMenuPopup(toolbox) {
+ let clickFired = false;
+
+ let menu = new Menu({
+ id: "menu-popup",
+ });
+ menu.append(new MenuItem({ type: "separator" }));
+
+ let MENU_ITEMS = [
+ new MenuItem({
+ id: "menu-item-1",
+ label: "Normal Item",
+ click: () => {
+ info("Click callback has fired for menu item");
+ clickFired = true;
+ },
+ }),
+ new MenuItem({
+ label: "Checked Item",
+ type: "checkbox",
+ checked: true,
+ }),
+ new MenuItem({
+ label: "Radio Item",
+ type: "radio",
+ }),
+ new MenuItem({
+ label: "Disabled Item",
+ disabled: true,
+ }),
+ ];
+
+ for (let item of MENU_ITEMS) {
+ menu.append(item);
+ }
+
+ // Append an invisible MenuItem, which shouldn't show up in the DOM
+ menu.append(new MenuItem({
+ label: "Invisible",
+ visible: false,
+ }));
+
+ menu.popup(0, 0, toolbox);
+
+ ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
+
+ let menuSeparators =
+ toolbox.doc.querySelectorAll("#menu-popup > menuseparator");
+ is(menuSeparators.length, 1, "A separator is in the menu");
+
+ let menuItems = toolbox.doc.querySelectorAll("#menu-popup > menuitem");
+ is(menuItems.length, MENU_ITEMS.length, "Correct number of menuitems");
+
+ is(menuItems[0].id, MENU_ITEMS[0].id, "Correct id for menuitem");
+ is(menuItems[0].getAttribute("label"), MENU_ITEMS[0].label, "Correct label");
+
+ is(menuItems[1].getAttribute("label"), MENU_ITEMS[1].label, "Correct label");
+ is(menuItems[1].getAttribute("type"), "checkbox", "Correct type attr");
+ is(menuItems[1].getAttribute("checked"), "true", "Has checked attr");
+
+ is(menuItems[2].getAttribute("label"), MENU_ITEMS[2].label, "Correct label");
+ is(menuItems[2].getAttribute("type"), "radio", "Correct type attr");
+ ok(!menuItems[2].hasAttribute("checked"), "Doesn't have checked attr");
+
+ is(menuItems[3].getAttribute("label"), MENU_ITEMS[3].label, "Correct label");
+ is(menuItems[3].getAttribute("disabled"), "true", "disabled attr menuitem");
+
+ yield once(menu, "open");
+ let closed = once(menu, "close");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {}, toolbox.win);
+ yield closed;
+ ok(clickFired, "Click has fired");
+
+ ok(!toolbox.doc.querySelector("#menu-popup"), "Popup removed from the DOM");
+}
+
+function* testSubmenu(toolbox) {
+ let clickFired = false;
+ let menu = new Menu({
+ id: "menu-popup",
+ });
+ let submenu = new Menu({
+ id: "submenu-popup",
+ });
+ submenu.append(new MenuItem({
+ label: "Submenu item",
+ click: () => {
+ info("Click callback has fired for submenu item");
+ clickFired = true;
+ },
+ }));
+ menu.append(new MenuItem({
+ label: "Submenu parent",
+ submenu: submenu,
+ }));
+ menu.append(new MenuItem({
+ label: "Submenu parent with attributes",
+ id: "submenu-parent-with-attrs",
+ submenu: submenu,
+ accesskey: "A",
+ disabled: true,
+ }));
+
+ menu.popup(0, 0, toolbox);
+ ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
+ is(toolbox.doc.querySelectorAll("#menu-popup > menuitem").length, 0,
+ "No menuitem children");
+
+ let menus = toolbox.doc.querySelectorAll("#menu-popup > menu");
+ is(menus.length, 2, "Correct number of menus");
+ is(menus[0].getAttribute("label"), "Submenu parent", "Correct label");
+ ok(!menus[0].hasAttribute("disabled"), "Correct disabled state");
+
+ is(menus[1].getAttribute("accesskey"), "A", "Correct accesskey");
+ ok(menus[1].hasAttribute("disabled"), "Correct disabled state");
+ ok(menus[1].id, "submenu-parent-with-attrs", "Correct id");
+
+ let subMenuItems = menus[0].querySelectorAll("menupopup > menuitem");
+ is(subMenuItems.length, 1, "Correct number of submenu items");
+ is(subMenuItems[0].getAttribute("label"), "Submenu item", "Correct label");
+
+ yield once(menu, "open");
+ let closed = once(menu, "close");
+
+ info("Using keyboard navigation to open, close, and reopen the submenu");
+ let shown = once(menus[0], "popupshown");
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ yield shown;
+
+ let hidden = once(menus[0], "popuphidden");
+ EventUtils.synthesizeKey("VK_LEFT", {});
+ yield hidden;
+
+ shown = once(menus[0], "popupshown");
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ yield shown;
+
+ info("Clicking the submenu item");
+ EventUtils.synthesizeMouseAtCenter(subMenuItems[0], {}, toolbox.win);
+
+ yield closed;
+ ok(clickFired, "Click has fired");
+}
diff --git a/devtools/client/framework/test/browser_new_activation_workflow.js b/devtools/client/framework/test/browser_new_activation_workflow.js
new file mode 100644
index 000000000..4092bf1a7
--- /dev/null
+++ b/devtools/client/framework/test/browser_new_activation_workflow.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests devtools API
+
+var toolbox, target;
+
+var tempScope = {};
+
+function test() {
+ addTab("about:blank").then(function (aTab) {
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ loadWebConsole(aTab).then(function () {
+ console.log("loaded");
+ });
+ });
+}
+
+function loadWebConsole(aTab) {
+ ok(gDevTools, "gDevTools exists");
+
+ return gDevTools.showToolbox(target, "webconsole").then(function (aToolbox) {
+ toolbox = aToolbox;
+ checkToolLoading();
+ });
+}
+
+function checkToolLoading() {
+ is(toolbox.currentToolId, "webconsole", "The web console is selected");
+ ok(toolbox.isReady, "toolbox is ready");
+
+ selectAndCheckById("jsdebugger").then(function () {
+ selectAndCheckById("styleeditor").then(function () {
+ testToggle();
+ });
+ });
+}
+
+function selectAndCheckById(id) {
+ return toolbox.selectTool(id).then(function () {
+ let tab = toolbox.doc.getElementById("toolbox-tab-" + id);
+ is(tab.hasAttribute("selected"), true, "The " + id + " tab is selected");
+ });
+}
+
+function testToggle() {
+ toolbox.once("destroyed", () => {
+ // Cannot reuse a target after it's destroyed.
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "styleeditor").then(function (aToolbox) {
+ toolbox = aToolbox;
+ is(toolbox.currentToolId, "styleeditor", "The style editor is selected");
+ finishUp();
+ });
+ });
+
+ toolbox.destroy();
+}
+
+function finishUp() {
+ toolbox.destroy().then(function () {
+ toolbox = null;
+ target = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_source_map-01.js b/devtools/client/framework/test/browser_source_map-01.js
new file mode 100644
index 000000000..af1808681
--- /dev/null
+++ b/devtools/client/framework/test/browser_source_map-01.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejections should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+ "TypeError: this.transport is null");
+
+/**
+ * Tests the SourceMapService updates generated sources when source maps
+ * are subsequently found. Also checks when no column is provided, and
+ * when tagging an already source mapped location initially.
+ */
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
+// Empty page
+const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
+const JS_URL = `${URL_ROOT}code_binary_search.js`;
+const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`;
+const { SourceMapService } = require("devtools/client/framework/source-map-service");
+const { serialize } = require("devtools/client/framework/location-store");
+
+add_task(function* () {
+ const toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
+ const service = new SourceMapService(toolbox.target);
+ let aggregator = new Map();
+
+ function onUpdate(e, oldLoc, newLoc) {
+ if (oldLoc.line === 6) {
+ checkLoc1(oldLoc, newLoc);
+ } else if (oldLoc.line === 8) {
+ checkLoc2(oldLoc, newLoc);
+ } else {
+ throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
+ }
+ aggregator.set(serialize(oldLoc), newLoc);
+ }
+
+ let loc1 = { url: JS_URL, line: 6 };
+ let loc2 = { url: JS_URL, line: 8, column: 3 };
+
+ service.subscribe(loc1, onUpdate);
+ service.subscribe(loc2, onUpdate);
+
+ // Inject JS script
+ let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_binary_search");
+ yield createScript(JS_URL);
+ yield sourceShown;
+
+ yield waitUntil(() => aggregator.size === 2);
+
+ aggregator = Array.from(aggregator.values());
+
+ ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location");
+ ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location");
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function checkLoc1(oldLoc, newLoc) {
+ is(oldLoc.line, 6, "Correct line for JS:6");
+ is(oldLoc.column, null, "Correct column for JS:6");
+ is(oldLoc.url, JS_URL, "Correct url for JS:6");
+ is(newLoc.line, 4, "Correct line for JS:6 -> COFFEE");
+ is(newLoc.column, 2, "Correct column for JS:6 -> COFFEE -- handles falsy column entries");
+ is(newLoc.url, COFFEE_URL, "Correct url for JS:6 -> COFFEE");
+}
+
+function checkLoc2(oldLoc, newLoc) {
+ is(oldLoc.line, 8, "Correct line for JS:8:3");
+ is(oldLoc.column, 3, "Correct column for JS:8:3");
+ is(oldLoc.url, JS_URL, "Correct url for JS:8:3");
+ is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE");
+ is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE");
+ is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE");
+}
+
+function createScript(url) {
+ info(`Creating script: ${url}`);
+ let mm = getFrameScript();
+ let command = `
+ let script = document.createElement("script");
+ script.setAttribute("src", "${url}");
+ document.body.appendChild(script);
+ null;
+ `;
+ return evalInDebuggee(mm, command);
+}
+
+function waitForSourceShown(debuggerPanel, url) {
+ let { panelWin } = debuggerPanel;
+ let deferred = defer();
+
+ info(`Waiting for source ${url} to be shown in the debugger...`);
+ panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown(_, source) {
+
+ let sourceUrl = source.url || source.generatedUrl;
+ if (sourceUrl.includes(url)) {
+ panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
+ info(`Source shown for ${url}`);
+ deferred.resolve(source);
+ }
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/framework/test/browser_source_map-02.js b/devtools/client/framework/test/browser_source_map-02.js
new file mode 100644
index 000000000..f31ce0175
--- /dev/null
+++ b/devtools/client/framework/test/browser_source_map-02.js
@@ -0,0 +1,113 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the SourceMapService updates generated sources when pretty printing
+ * and un pretty printing.
+ */
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
+// Empty page
+const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
+const JS_URL = `${URL_ROOT}code_ugly.js`;
+const { SourceMapService } = require("devtools/client/framework/source-map-service");
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
+
+ let service = new SourceMapService(toolbox.target);
+
+ let checkedPretty = false;
+ let checkedUnpretty = false;
+
+ function onUpdate(e, oldLoc, newLoc) {
+ if (oldLoc.line === 3) {
+ checkPrettified(oldLoc, newLoc);
+ checkedPretty = true;
+ } else if (oldLoc.line === 9) {
+ checkUnprettified(oldLoc, newLoc);
+ checkedUnpretty = true;
+ } else {
+ throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
+ }
+ }
+ const loc1 = { url: JS_URL, line: 3 };
+ service.subscribe(loc1, onUpdate);
+
+ // Inject JS script
+ let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+ yield createScript(JS_URL);
+ yield sourceShown;
+
+ let ppButton = toolbox.getCurrentPanel().panelWin.document.getElementById("pretty-print");
+ sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+ ppButton.click();
+ yield sourceShown;
+ yield waitUntil(() => checkedPretty);
+
+ // TODO check unprettified change once bug 1177446 fixed
+ // info("Testing un-pretty printing.");
+ // sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+ // ppButton.click();
+ // yield sourceShown;
+ // yield waitUntil(() => checkedUnpretty);
+
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+ finish();
+});
+
+function checkPrettified(oldLoc, newLoc) {
+ is(oldLoc.line, 3, "Correct line for JS:3");
+ is(oldLoc.column, null, "Correct column for JS:3");
+ is(oldLoc.url, JS_URL, "Correct url for JS:3");
+ is(newLoc.line, 9, "Correct line for JS:3 -> PRETTY");
+ is(newLoc.column, 0, "Correct column for JS:3 -> PRETTY");
+ is(newLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
+}
+
+function checkUnprettified(oldLoc, newLoc) {
+ is(oldLoc.line, 9, "Correct line for JS:3 -> PRETTY");
+ is(oldLoc.column, 0, "Correct column for JS:3 -> PRETTY");
+ is(oldLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
+ is(newLoc.line, 3, "Correct line for JS:3 -> UNPRETTIED");
+ is(newLoc.column, null, "Correct column for JS:3 -> UNPRETTIED");
+ is(newLoc.url, JS_URL, "Correct url for JS:3 -> UNPRETTIED");
+}
+
+function createScript(url) {
+ info(`Creating script: ${url}`);
+ let mm = getFrameScript();
+ let command = `
+ let script = document.createElement("script");
+ script.setAttribute("src", "${url}");
+ document.body.appendChild(script);
+ `;
+ return evalInDebuggee(mm, command);
+}
+
+function waitForSourceShown(debuggerPanel, url) {
+ let { panelWin } = debuggerPanel;
+ let deferred = defer();
+
+ info(`Waiting for source ${url} to be shown in the debugger...`);
+ panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown(_, source) {
+ let sourceUrl = source.url || source.introductionUrl;
+
+ if (sourceUrl.includes(url)) {
+ panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
+ info(`Source shown for ${url}`);
+ deferred.resolve(source);
+ }
+ });
+
+ return deferred.promise;
+}
diff --git a/devtools/client/framework/test/browser_target_events.js b/devtools/client/framework/test/browser_target_events.js
new file mode 100644
index 000000000..d0054a484
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_events.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var target;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(onLoad);
+}
+
+function onLoad() {
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ is(target.tab, gBrowser.selectedTab, "Target linked to the right tab.");
+
+ target.once("hidden", onHidden);
+ gBrowser.selectedTab = gBrowser.addTab();
+}
+
+function onHidden() {
+ ok(true, "Hidden event received");
+ target.once("visible", onVisible);
+ gBrowser.removeCurrentTab();
+}
+
+function onVisible() {
+ ok(true, "Visible event received");
+ target.once("will-navigate", onWillNavigate);
+ let mm = getFrameScript();
+ mm.sendAsyncMessage("devtools:test:navigate", { location: "data:text/html,<meta charset='utf8'/>test navigation" });
+}
+
+function onWillNavigate(event, request) {
+ ok(true, "will-navigate event received");
+ // Wait for navigation handling to complete before removing the tab, in order
+ // to avoid triggering assertions.
+ target.once("navigate", executeSoon.bind(null, onNavigate));
+}
+
+function onNavigate() {
+ ok(true, "navigate event received");
+ target.once("close", onClose);
+ gBrowser.removeCurrentTab();
+}
+
+function onClose() {
+ ok(true, "close event received");
+
+ target = null;
+ finish();
+}
diff --git a/devtools/client/framework/test/browser_target_from_url.js b/devtools/client/framework/test/browser_target_from_url.js
new file mode 100644
index 000000000..0707ee7d7
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_from_url.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8," +
+ "<p>browser_target-from-url.js</p>";
+
+const { DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { targetFromURL } = require("devtools/client/framework/target-from-url");
+
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false);
+
+SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
+ Services.prefs.clearUserPref("devtools.debugger.prompt-connection");
+});
+
+function assertIsTabTarget(target, url, chrome = false) {
+ is(target.url, url);
+ is(target.isLocalTab, false);
+ is(target.chrome, chrome);
+ is(target.isTabActor, true);
+ is(target.isRemote, true);
+}
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let browser = tab.linkedBrowser;
+ let target;
+
+ info("Test invalid type");
+ try {
+ yield targetFromURL(new URL("http://foo?type=x"));
+ ok(false, "Shouldn't pass");
+ } catch (e) {
+ is(e.message, "targetFromURL, unsupported type='x' parameter");
+ }
+
+ info("Test tab");
+ let windowId = browser.outerWindowID;
+ target = yield targetFromURL(new URL("http://foo?type=tab&id=" + windowId));
+ assertIsTabTarget(target, TEST_URI);
+
+ info("Test tab with chrome privileges");
+ target = yield targetFromURL(new URL("http://foo?type=tab&id=" + windowId + "&chrome"));
+ assertIsTabTarget(target, TEST_URI, true);
+
+ info("Test invalid tab id");
+ try {
+ yield targetFromURL(new URL("http://foo?type=tab&id=10000"));
+ ok(false, "Shouldn't pass");
+ } catch (e) {
+ is(e.message, "targetFromURL, tab with outerWindowID:'10000' doesn't exist");
+ }
+
+ info("Test parent process");
+ target = yield targetFromURL(new URL("http://foo?type=process"));
+ let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertIsTabTarget(target, topWindow.location.href, true);
+
+ yield testRemoteTCP();
+ yield testRemoteWebSocket();
+
+ gBrowser.removeCurrentTab();
+});
+
+function* setupDebuggerServer(websocket) {
+ info("Create a separate loader instance for the DebuggerServer.");
+ let loader = new DevToolsLoader();
+ let { DebuggerServer } = loader.require("devtools/server/main");
+
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ DebuggerServer.allowChromeProcess = true;
+
+ let listener = DebuggerServer.createListener();
+ ok(listener, "Socket listener created");
+ // Pass -1 to automatically choose an available port
+ listener.portOrPath = -1;
+ listener.webSocket = websocket;
+ yield listener.open();
+ is(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+ return { DebuggerServer, listener };
+}
+
+function teardownDebuggerServer({ DebuggerServer, listener }) {
+ info("Close the listener socket");
+ listener.close();
+ is(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+ info("Destroy the temporary debugger server");
+ DebuggerServer.destroy();
+}
+
+function* testRemoteTCP() {
+ info("Test remote process via TCP Connection");
+
+ let server = yield setupDebuggerServer(false);
+
+ let { port } = server.listener;
+ let target = yield targetFromURL(new URL("http://foo?type=process&host=127.0.0.1&port=" + port));
+ let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertIsTabTarget(target, topWindow.location.href, true);
+
+ let settings = target.client._transport.connectionSettings;
+ is(settings.host, "127.0.0.1");
+ is(settings.port, port);
+ is(settings.webSocket, false);
+
+ yield target.client.close();
+
+ teardownDebuggerServer(server);
+}
+
+function* testRemoteWebSocket() {
+ info("Test remote process via WebSocket Connection");
+
+ let server = yield setupDebuggerServer(true);
+
+ let { port } = server.listener;
+ let target = yield targetFromURL(new URL("http://foo?type=process&host=127.0.0.1&port=" + port + "&ws=true"));
+ let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ assertIsTabTarget(target, topWindow.location.href, true);
+
+ let settings = target.client._transport.connectionSettings;
+ is(settings.host, "127.0.0.1");
+ is(settings.port, port);
+ is(settings.webSocket, true);
+ yield target.client.close();
+
+ teardownDebuggerServer(server);
+}
diff --git a/devtools/client/framework/test/browser_target_remote.js b/devtools/client/framework/test/browser_target_remote.js
new file mode 100644
index 000000000..b828d14ff
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_remote.js
@@ -0,0 +1,25 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure target is closed if client is closed directly
+function test() {
+ waitForExplicitFinish();
+
+ getChromeActors((client, response) => {
+ let options = {
+ form: response,
+ client: client,
+ chrome: true
+ };
+
+ TargetFactory.forRemoteTab(options).then(target => {
+ target.on("close", () => {
+ ok(true, "Target was closed");
+ finish();
+ });
+ client.close();
+ });
+ });
+}
diff --git a/devtools/client/framework/test/browser_target_support.js b/devtools/client/framework/test/browser_target_support.js
new file mode 100644
index 000000000..0cdbd565a
--- /dev/null
+++ b/devtools/client/framework/test/browser_target_support.js
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test support methods on Target, such as `hasActor`, `getActorDescription`,
+// `actorHasMethod` and `getTrait`.
+
+var { WebAudioFront } =
+ require("devtools/shared/fronts/webaudio");
+
+function* testTarget(client, target) {
+ yield target.makeRemote();
+
+ is(target.hasActor("timeline"), true, "target.hasActor() true when actor exists.");
+ is(target.hasActor("webaudio"), true, "target.hasActor() true when actor exists.");
+ is(target.hasActor("notreal"), false, "target.hasActor() false when actor does not exist.");
+ // Create a front to ensure the actor is loaded
+ let front = new WebAudioFront(target.client, target.form);
+
+ let desc = yield target.getActorDescription("webaudio");
+ is(desc.typeName, "webaudio",
+ "target.getActorDescription() returns definition data for corresponding actor");
+ is(desc.events["start-context"]["type"], "startContext",
+ "target.getActorDescription() returns event data for corresponding actor");
+
+ desc = yield target.getActorDescription("nope");
+ is(desc, undefined, "target.getActorDescription() returns undefined for non-existing actor");
+ desc = yield target.getActorDescription();
+ is(desc, undefined, "target.getActorDescription() returns undefined for undefined actor");
+
+ let hasMethod = yield target.actorHasMethod("audionode", "getType");
+ is(hasMethod, true,
+ "target.actorHasMethod() returns true for existing actor with method");
+ hasMethod = yield target.actorHasMethod("audionode", "nope");
+ is(hasMethod, false,
+ "target.actorHasMethod() returns false for existing actor with no method");
+ hasMethod = yield target.actorHasMethod("nope", "nope");
+ is(hasMethod, false,
+ "target.actorHasMethod() returns false for non-existing actor with no method");
+ hasMethod = yield target.actorHasMethod();
+ is(hasMethod, false,
+ "target.actorHasMethod() returns false for undefined params");
+
+ is(target.getTrait("customHighlighters"), true,
+ "target.getTrait() returns boolean when trait exists");
+ is(target.getTrait("giddyup"), undefined,
+ "target.getTrait() returns undefined when trait does not exist");
+
+ close(target, client);
+}
+
+// Ensure target is closed if client is closed directly
+function test() {
+ waitForExplicitFinish();
+
+ getChromeActors((client, response) => {
+ let options = {
+ form: response,
+ client: client,
+ chrome: true
+ };
+
+ TargetFactory.forRemoteTab(options).then(Task.async(testTarget).bind(null, client));
+ });
+}
+
+function close(target, client) {
+ target.on("close", () => {
+ ok(true, "Target was closed");
+ finish();
+ });
+ client.close();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_custom_host.js b/devtools/client/framework/test/browser_toolbox_custom_host.js
new file mode 100644
index 000000000..5d3aeed54
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_custom_host.js
@@ -0,0 +1,57 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = "data:text/html,test custom host";
+
+function test() {
+ let {Toolbox} = require("devtools/client/framework/toolbox");
+
+ let toolbox, iframe, target;
+
+ window.addEventListener("message", onMessage);
+
+ iframe = document.createElement("iframe");
+ document.documentElement.appendChild(iframe);
+
+ addTab(TEST_URL).then(function (tab) {
+ target = TargetFactory.forTab(tab);
+ let options = {customIframe: iframe};
+ gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options)
+ .then(testCustomHost, console.error)
+ .then(null, console.error);
+ });
+
+ function onMessage(event) {
+ if (typeof(event.data) !== "string") {
+ return;
+ }
+ info("onMessage: " + event.data);
+ let json = JSON.parse(event.data);
+ if (json.name == "toolbox-close") {
+ ok("Got the `toolbox-close` message");
+ window.removeEventListener("message", onMessage);
+ cleanup();
+ }
+ }
+
+ function testCustomHost(t) {
+ toolbox = t;
+ is(toolbox.win.top, window, "Toolbox is included in browser.xul");
+ is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe");
+ executeSoon(() => gBrowser.removeCurrentTab());
+ }
+
+ function cleanup() {
+ iframe.remove();
+
+ // Even if we received "toolbox-close", the toolbox may still be destroying
+ // toolbox.destroy() returns a singleton promise that ensures
+ // everything is cleaned up before proceeding.
+ toolbox.destroy().then(() => {
+ toolbox = iframe = target = null;
+ finish();
+ });
+ }
+}
diff --git a/devtools/client/framework/test/browser_toolbox_dynamic_registration.js b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
new file mode 100644
index 000000000..2583ca68e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
@@ -0,0 +1,105 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = "data:text/html,test for dynamically registering and unregistering tools";
+
+var toolbox;
+
+function test()
+{
+ addTab(TEST_URL).then(tab => {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target).then(testRegister);
+ });
+}
+
+function testRegister(aToolbox)
+{
+ toolbox = aToolbox;
+ gDevTools.once("tool-registered", toolRegistered);
+
+ gDevTools.registerTool({
+ id: "test-tool",
+ label: "Test Tool",
+ inMenu: true,
+ isTargetSupported: () => true,
+ build: function () {},
+ key: "t"
+ });
+}
+
+function toolRegistered(event, toolId)
+{
+ is(toolId, "test-tool", "tool-registered event handler sent tool id");
+
+ ok(gDevTools.getToolDefinitionMap().has(toolId), "tool added to map");
+
+ // test that it appeared in the UI
+ let doc = toolbox.doc;
+ let tab = doc.getElementById("toolbox-tab-" + toolId);
+ ok(tab, "new tool's tab exists in toolbox UI");
+
+ let panel = doc.getElementById("toolbox-panel-" + toolId);
+ ok(panel, "new tool's panel exists in toolbox UI");
+
+ for (let win of getAllBrowserWindows()) {
+ let key = win.document.getElementById("key_" + toolId);
+ ok(key, "key for new tool added to every browser window");
+ let menuitem = win.document.getElementById("menuitem_" + toolId);
+ ok(menuitem, "menu item of new tool added to every browser window");
+ }
+
+ // then unregister it
+ testUnregister();
+}
+
+function getAllBrowserWindows() {
+ let wins = [];
+ let enumerator = Services.wm.getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements()) {
+ wins.push(enumerator.getNext());
+ }
+ return wins;
+}
+
+function testUnregister()
+{
+ gDevTools.once("tool-unregistered", toolUnregistered);
+
+ gDevTools.unregisterTool("test-tool");
+}
+
+function toolUnregistered(event, toolDefinition)
+{
+ let toolId = toolDefinition.id;
+ is(toolId, "test-tool", "tool-unregistered event handler sent tool id");
+
+ ok(!gDevTools.getToolDefinitionMap().has(toolId), "tool removed from map");
+
+ // test that it disappeared from the UI
+ let doc = toolbox.doc;
+ let tab = doc.getElementById("toolbox-tab-" + toolId);
+ ok(!tab, "tool's tab was removed from the toolbox UI");
+
+ let panel = doc.getElementById("toolbox-panel-" + toolId);
+ ok(!panel, "tool's panel was removed from toolbox UI");
+
+ for (let win of getAllBrowserWindows()) {
+ let key = win.document.getElementById("key_" + toolId);
+ ok(!key, "key removed from every browser window");
+ let menuitem = win.document.getElementById("menuitem_" + toolId);
+ ok(!menuitem, "menu item removed from every browser window");
+ }
+
+ cleanup();
+}
+
+function cleanup()
+{
+ toolbox.destroy();
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js b/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js
new file mode 100644
index 000000000..21dd236a1
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_getpanelwhenready.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that getPanelWhenReady returns the correct panel in promise
+// resolutions regardless of whether it has opened first.
+
+var toolbox = null;
+
+const URL = "data:text/html;charset=utf8,test for getPanelWhenReady";
+
+add_task(function* () {
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target);
+
+ let debuggerPanelPromise = toolbox.getPanelWhenReady("jsdebugger");
+ yield toolbox.selectTool("jsdebugger");
+ let debuggerPanel = yield debuggerPanelPromise;
+
+ is(debuggerPanel, toolbox.getPanel("jsdebugger"),
+ "The debugger panel from getPanelWhenReady before loading is the actual panel");
+
+ let debuggerPanel2 = yield toolbox.getPanelWhenReady("jsdebugger");
+ is(debuggerPanel2, toolbox.getPanel("jsdebugger"),
+ "The debugger panel from getPanelWhenReady after loading is the actual panel");
+
+ yield cleanup();
+});
+
+function* cleanup() {
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+ toolbox = null;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_highlight.js b/devtools/client/framework/test/browser_toolbox_highlight.js
new file mode 100644
index 000000000..d197fdc99
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_highlight.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+var toolbox = null;
+
+function test() {
+ const URL = "data:text/plain;charset=UTF-8,Nothing to see here, move along";
+
+ const TOOL_ID_1 = "jsdebugger";
+ const TOOL_ID_2 = "webconsole";
+
+ addTab(URL).then(() => {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, TOOL_ID_1, Toolbox.HostType.BOTTOM)
+ .then(aToolbox => {
+ toolbox = aToolbox;
+ // select tool 2
+ toolbox.selectTool(TOOL_ID_2)
+ // and highlight the first one
+ .then(highlightTab.bind(null, TOOL_ID_1))
+ // to see if it has the proper class.
+ .then(checkHighlighted.bind(null, TOOL_ID_1))
+ // Now switch back to first tool
+ .then(() => toolbox.selectTool(TOOL_ID_1))
+ // to check again. But there is no easy way to test if
+ // it is showing orange or not.
+ .then(checkNoHighlightWhenSelected.bind(null, TOOL_ID_1))
+ // Switch to tool 2 again
+ .then(() => toolbox.selectTool(TOOL_ID_2))
+ // and check again.
+ .then(checkHighlighted.bind(null, TOOL_ID_1))
+ // Now unhighlight the tool
+ .then(unhighlightTab.bind(null, TOOL_ID_1))
+ // to see the classes gone.
+ .then(checkNoHighlight.bind(null, TOOL_ID_1))
+ // Now close the toolbox and exit.
+ .then(() => executeSoon(() => {
+ toolbox.destroy()
+ .then(() => {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ }));
+ });
+ });
+}
+
+function highlightTab(toolId) {
+ info("Highlighting tool " + toolId + "'s tab.");
+ toolbox.highlightTool(toolId);
+}
+
+function unhighlightTab(toolId) {
+ info("Unhighlighting tool " + toolId + "'s tab.");
+ toolbox.unhighlightTool(toolId);
+}
+
+function checkHighlighted(toolId) {
+ let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
+ ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
+ ok(!tab.hasAttribute("selected") || tab.getAttribute("selected") != "true",
+ "The tab is not selected");
+}
+
+function checkNoHighlightWhenSelected(toolId) {
+ let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
+ ok(tab.hasAttribute("highlighted"), "The highlighted attribute is present");
+ ok(tab.hasAttribute("selected") && tab.getAttribute("selected") == "true",
+ "and the tab is selected, so the orange glow will not be present.");
+}
+
+function checkNoHighlight(toolId) {
+ let tab = toolbox.doc.getElementById("toolbox-tab-" + toolId);
+ ok(!tab.hasAttribute("highlighted"),
+ "The highlighted attribute is not present");
+}
diff --git a/devtools/client/framework/test/browser_toolbox_hosts.js b/devtools/client/framework/test/browser_toolbox_hosts.js
new file mode 100644
index 000000000..e16563ba7
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts.js
@@ -0,0 +1,139 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+var {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
+var toolbox, target;
+
+const URL = "data:text/html;charset=utf8,test for opening toolbox in different hosts";
+
+add_task(function* runTest() {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ yield testBottomHost();
+ yield testSidebarHost();
+ yield testWindowHost();
+ yield testToolSelect();
+ yield testDestroy();
+ yield testRememberHost();
+ yield testPreviousHost();
+
+ yield toolbox.destroy();
+
+ toolbox = target = null;
+ gBrowser.removeCurrentTab();
+});
+
+function* testBottomHost() {
+ checkHostType(toolbox, BOTTOM);
+
+ // test UI presence
+ let nbox = gBrowser.getNotificationBox();
+ let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
+ ok(iframe, "toolbox bottom iframe exists");
+
+ checkToolboxLoaded(iframe);
+}
+
+function* testSidebarHost() {
+ yield toolbox.switchHost(SIDE);
+ checkHostType(toolbox, SIDE);
+
+ // test UI presence
+ let nbox = gBrowser.getNotificationBox();
+ let bottom = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
+ ok(!bottom, "toolbox bottom iframe doesn't exist");
+
+ let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
+ ok(iframe, "toolbox side iframe exists");
+
+ checkToolboxLoaded(iframe);
+}
+
+function* testWindowHost() {
+ yield toolbox.switchHost(WINDOW);
+ checkHostType(toolbox, WINDOW);
+
+ let nbox = gBrowser.getNotificationBox();
+ let sidebar = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
+ ok(!sidebar, "toolbox sidebar iframe doesn't exist");
+
+ let win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ ok(win, "toolbox separate window exists");
+
+ let iframe = win.document.getElementById("toolbox-iframe");
+ checkToolboxLoaded(iframe);
+}
+
+function* testToolSelect() {
+ // make sure we can load a tool after switching hosts
+ yield toolbox.selectTool("inspector");
+}
+
+function* testDestroy() {
+ yield toolbox.destroy();
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ toolbox = yield gDevTools.showToolbox(target);
+}
+
+function* testRememberHost() {
+ // last host was the window - make sure it's the same when re-opening
+ is(toolbox.hostType, WINDOW, "host remembered");
+
+ let win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ ok(win, "toolbox separate window exists");
+}
+
+function* testPreviousHost() {
+ // last host was the window - make sure it's the same when re-opening
+ is(toolbox.hostType, WINDOW, "host remembered");
+
+ info("Switching to side");
+ yield toolbox.switchHost(SIDE);
+ checkHostType(toolbox, SIDE, WINDOW);
+
+ info("Switching to bottom");
+ yield toolbox.switchHost(BOTTOM);
+ checkHostType(toolbox, BOTTOM, SIDE);
+
+ info("Switching from bottom to side");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, SIDE, BOTTOM);
+
+ info("Switching from side to bottom");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, BOTTOM, SIDE);
+
+ info("Switching to window");
+ yield toolbox.switchHost(WINDOW);
+ checkHostType(toolbox, WINDOW, BOTTOM);
+
+ info("Switching from window to bottom");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, BOTTOM, WINDOW);
+
+ info("Forcing the previous host to match the current (bottom)");
+ Services.prefs.setCharPref("devtools.toolbox.previousHost", BOTTOM);
+
+ info("Switching from bottom to side (since previous=current=bottom");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, SIDE, BOTTOM);
+
+ info("Forcing the previous host to match the current (side)");
+ Services.prefs.setCharPref("devtools.toolbox.previousHost", SIDE);
+ info("Switching from side to bottom (since previous=current=side");
+ yield toolbox.switchToPreviousHost();
+ checkHostType(toolbox, BOTTOM, SIDE);
+}
+
+function checkToolboxLoaded(iframe) {
+ let tabs = iframe.contentDocument.getElementById("toolbox-tabs");
+ ok(tabs, "toolbox UI has been loaded into iframe");
+}
diff --git a/devtools/client/framework/test/browser_toolbox_hosts_size.js b/devtools/client/framework/test/browser_toolbox_hosts_size.js
new file mode 100644
index 000000000..4286fe438
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts_size.js
@@ -0,0 +1,69 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that getPanelWhenReady returns the correct panel in promise
+// resolutions regardless of whether it has opened first.
+
+const URL = "data:text/html;charset=utf8,test for host sizes";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+add_task(function* () {
+ // Set size prefs to make the hosts way too big, so that the size has
+ // to be clamped to fit into the browser window.
+ Services.prefs.setIntPref("devtools.toolbox.footer.height", 10000);
+ Services.prefs.setIntPref("devtools.toolbox.sidebar.width", 10000);
+
+ let tab = yield addTab(URL);
+ let nbox = gBrowser.getNotificationBox();
+ let {clientHeight: nboxHeight, clientWidth: nboxWidth} = nbox;
+ let toolbox = yield gDevTools.showToolbox(TargetFactory.forTab(tab));
+
+ is(nbox.clientHeight, nboxHeight, "Opening the toolbox hasn't changed the height of the nbox");
+ is(nbox.clientWidth, nboxWidth, "Opening the toolbox hasn't changed the width of the nbox");
+
+ let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
+ is(iframe.clientHeight, nboxHeight - 25, "The iframe fits within the available space");
+
+ yield toolbox.switchHost(Toolbox.HostType.SIDE);
+ iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
+ iframe.style.minWidth = "1px"; // Disable the min width set in css
+ is(iframe.clientWidth, nboxWidth - 25, "The iframe fits within the available space");
+
+ yield cleanup(toolbox);
+});
+
+add_task(function* () {
+ // Set size prefs to something reasonable, so we can check to make sure
+ // they are being set properly.
+ Services.prefs.setIntPref("devtools.toolbox.footer.height", 100);
+ Services.prefs.setIntPref("devtools.toolbox.sidebar.width", 100);
+
+ let tab = yield addTab(URL);
+ let nbox = gBrowser.getNotificationBox();
+ let {clientHeight: nboxHeight, clientWidth: nboxWidth} = nbox;
+ let toolbox = yield gDevTools.showToolbox(TargetFactory.forTab(tab));
+
+ is(nbox.clientHeight, nboxHeight, "Opening the toolbox hasn't changed the height of the nbox");
+ is(nbox.clientWidth, nboxWidth, "Opening the toolbox hasn't changed the width of the nbox");
+
+ let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
+ is(iframe.clientHeight, 100, "The iframe is resized properly");
+
+ yield toolbox.switchHost(Toolbox.HostType.SIDE);
+ iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
+ iframe.style.minWidth = "1px"; // Disable the min width set in css
+ is(iframe.clientWidth, 100, "The iframe is resized properly");
+
+ yield cleanup(toolbox);
+});
+
+function* cleanup(toolbox) {
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.footer.height");
+ Services.prefs.clearUserPref("devtools.toolbox.sidebar.width");
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js b/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js
new file mode 100644
index 000000000..f8ff9b3e4
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_hosts_telemetry.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {Toolbox} = require("devtools/client/framework/toolbox");
+const {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
+
+const URL = "data:text/html;charset=utf8,browser_toolbox_hosts_telemetry.js";
+
+function getHostHistogram() {
+ return Services.telemetry.getHistogramById("DEVTOOLS_TOOLBOX_HOST");
+}
+
+add_task(function* () {
+ // Reset it to make counting easier
+ getHostHistogram().clear();
+
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ yield changeToolboxHost(toolbox);
+ yield checkResults();
+ yield toolbox.destroy();
+
+ toolbox = target = null;
+ gBrowser.removeCurrentTab();
+
+ // Cleanup
+ getHostHistogram().clear();
+});
+
+function* changeToolboxHost(toolbox) {
+ info("Switch toolbox host");
+ yield toolbox.switchHost(SIDE);
+ yield toolbox.switchHost(WINDOW);
+ yield toolbox.switchHost(BOTTOM);
+ yield toolbox.switchHost(SIDE);
+ yield toolbox.switchHost(WINDOW);
+ yield toolbox.switchHost(BOTTOM);
+}
+
+function checkResults() {
+ let counts = getHostHistogram().snapshot().counts;
+ is(counts[0], 3, "Toolbox HostType bottom has 3 successful entries");
+ is(counts[1], 2, "Toolbox HostType side has 2 successful entries");
+ is(counts[2], 2, "Toolbox HostType window has 2 successful entries");
+}
diff --git a/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js b/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js
new file mode 100644
index 000000000..a22f87064
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_keyboard_navigation.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests keyboard navigation of devtools tabbar.
+
+const TEST_URL =
+ "data:text/html;charset=utf8,test page for toolbar keyboard navigation";
+
+function containsFocus(aDoc, aElm) {
+ let elm = aDoc.activeElement;
+ while (elm) {
+ if (elm === aElm) { return true; }
+ elm = elm.parentNode;
+ }
+ return false;
+}
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let toolbox = yield openNewTabAndToolbox(TEST_URL, "webconsole");
+ let doc = toolbox.doc;
+
+ let toolbar = doc.querySelector(".devtools-tabbar");
+ let toolbarControls = [...toolbar.querySelectorAll(
+ ".devtools-tab, button")].filter(elm =>
+ !elm.hidden && doc.defaultView.getComputedStyle(elm).getPropertyValue(
+ "display") !== "none");
+
+ // Put the keyboard focus onto the first toolbar control.
+ toolbarControls[0].focus();
+ ok(containsFocus(doc, toolbar), "Focus is within the toolbar");
+
+ // Move the focus away from toolbar to a next focusable element.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+ // Move the focus back to the toolbar.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ ok(containsFocus(doc, toolbar), "Focus is within the toolbar again");
+
+ // Move through the toolbar forward using the right arrow key.
+ for (let i = 0; i < toolbarControls.length; ++i) {
+ is(doc.activeElement.id, toolbarControls[i].id, "New control is focused");
+ if (i < toolbarControls.length - 1) {
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ }
+ }
+
+ // Move the focus away from toolbar to a next focusable element.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+ // Move the focus back to the toolbar.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ ok(containsFocus(doc, toolbar), "Focus is within the toolbar again");
+
+ // Move through the toolbar backward using the left arrow key.
+ for (let i = toolbarControls.length - 1; i >= 0; --i) {
+ is(doc.activeElement.id, toolbarControls[i].id, "New control is focused");
+ if (i > 0) { EventUtils.synthesizeKey("VK_LEFT", {}); }
+ }
+
+ // Move focus to the 3rd (non-first) toolbar control.
+ let expectedFocusedControl = toolbarControls[2];
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ is(doc.activeElement.id, expectedFocusedControl.id, "New control is focused");
+
+ // Move the focus away from toolbar to a next focusable element.
+ EventUtils.synthesizeKey("VK_TAB", {});
+ ok(!containsFocus(doc, toolbar), "Focus is outside of the toolbar");
+
+ // Move the focus back to the toolbar, ensure we land on the last active
+ // descendant control.
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ is(doc.activeElement.id, expectedFocusedControl.id, "New control is focused");
+});
diff --git a/devtools/client/framework/test/browser_toolbox_minimize.js b/devtools/client/framework/test/browser_toolbox_minimize.js
new file mode 100644
index 000000000..9b5126320
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_minimize.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that when the toolbox is displayed in a bottom host, that host can be
+// minimized to just the tabbar height, and maximized again.
+// Also test that while minimized, switching to a tool, clicking on the
+// settings, or clicking on the selected tool's tab maximizes the toolbox again.
+// Finally test that the minimize button doesn't exist in other host types.
+
+const URL = "data:text/html;charset=utf8,test page";
+const {Toolbox} = require("devtools/client/framework/toolbox");
+
+add_task(function* () {
+ info("Create a test tab and open the toolbox");
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+ let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
+ ok(button, "The minimize button exists in the default bottom host");
+
+ info("Try to minimize the toolbox");
+ yield minimize(toolbox);
+ ok(parseInt(toolbox._host.frame.style.marginBottom, 10) < 0,
+ "The toolbox host has been hidden away with a negative-margin");
+
+ info("Try to maximize again the toolbox");
+ yield maximize(toolbox);
+ ok(parseInt(toolbox._host.frame.style.marginBottom, 10) == 0,
+ "The toolbox host is shown again");
+
+ info("Try to minimize again using the keyboard shortcut");
+ yield minimizeWithShortcut(toolbox);
+ ok(parseInt(toolbox._host.frame.style.marginBottom, 10) < 0,
+ "The toolbox host has been hidden away with a negative-margin");
+
+ info("Try to maximize again using the keyboard shortcut");
+ yield maximizeWithShortcut(toolbox);
+ ok(parseInt(toolbox._host.frame.style.marginBottom, 10) == 0,
+ "The toolbox host is shown again");
+
+ info("Minimize again and switch to another tool");
+ yield minimize(toolbox);
+ let onMaximized = toolbox._host.once("maximized");
+ yield toolbox.selectTool("inspector");
+ yield onMaximized;
+
+ info("Minimize again and click on the tab of the current tool");
+ yield minimize(toolbox);
+ onMaximized = toolbox._host.once("maximized");
+ let tabButton = toolbox.doc.querySelector("#toolbox-tab-inspector");
+ EventUtils.synthesizeMouseAtCenter(tabButton, {}, toolbox.win);
+ yield onMaximized;
+
+ info("Minimize again and click on the settings tab");
+ yield minimize(toolbox);
+ onMaximized = toolbox._host.once("maximized");
+ let settingsButton = toolbox.doc.querySelector("#toolbox-tab-options");
+ EventUtils.synthesizeMouseAtCenter(settingsButton, {}, toolbox.win);
+ yield onMaximized;
+
+ info("Switch to a different host");
+ yield toolbox.switchHost(Toolbox.HostType.SIDE);
+ button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
+ ok(!button, "The minimize button doesn't exist in the side host");
+
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function* minimize(toolbox) {
+ let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
+ let onMinimized = toolbox._host.once("minimized");
+ EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win);
+ yield onMinimized;
+}
+
+function* minimizeWithShortcut(toolbox) {
+ let key = toolbox.doc.getElementById("toolbox-minimize-key")
+ .getAttribute("key");
+ let onMinimized = toolbox._host.once("minimized");
+ EventUtils.synthesizeKey(key, {accelKey: true, shiftKey: true},
+ toolbox.win);
+ yield onMinimized;
+}
+
+function* maximize(toolbox) {
+ let button = toolbox.doc.querySelector("#toolbox-dock-bottom-minimize");
+ let onMaximized = toolbox._host.once("maximized");
+ EventUtils.synthesizeMouseAtCenter(button, {}, toolbox.win);
+ yield onMaximized;
+}
+
+function* maximizeWithShortcut(toolbox) {
+ let key = toolbox.doc.getElementById("toolbox-minimize-key")
+ .getAttribute("key");
+ let onMaximized = toolbox._host.once("maximized");
+ EventUtils.synthesizeKey(key, {accelKey: true, shiftKey: true},
+ toolbox.win);
+ yield onMaximized;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options.js b/devtools/client/framework/test/browser_toolbox_options.js
new file mode 100644
index 000000000..569ed86fb
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -0,0 +1,297 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from shared-head.js */
+"use strict";
+
+// 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");
+
+add_task(function* () {
+ const URL = "data:text/html;charset=utf8,test for dynamically registering " +
+ "and unregistering tools";
+ registerNewTool();
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target);
+ doc = toolbox.doc;
+ yield testSelectTool();
+ yield testOptionsShortcut();
+ yield testOptions();
+ yield testToggleTools();
+ yield cleanup();
+});
+
+function registerNewTool() {
+ let toolDefinition = {
+ id: "test-tool",
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel"
+ };
+
+ ok(gDevTools, "gDevTools exists");
+ ok(!gDevTools.getToolDefinitionMap().has("test-tool"),
+ "The tool is not registered");
+
+ gDevTools.registerTool(toolDefinition);
+ ok(gDevTools.getToolDefinitionMap().has("test-tool"),
+ "The tool is registered");
+}
+
+function* testSelectTool() {
+ info("Checking to make sure that the options panel can be selected.");
+
+ let onceSelected = toolbox.once("options-selected");
+ toolbox.selectTool("options");
+ yield onceSelected;
+ ok(true, "Toolbox selected via selectTool method");
+}
+
+function* testOptionsShortcut() {
+ info("Selecting another tool, then reselecting options panel with keyboard.");
+
+ yield toolbox.selectTool("webconsole");
+ is(toolbox.currentToolId, "webconsole", "webconsole is selected");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
+ is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (1)");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
+ is(toolbox.currentToolId, "webconsole", "webconsole is selected (1)");
+
+ yield toolbox.selectTool("webconsole");
+ is(toolbox.currentToolId, "webconsole", "webconsole is selected");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
+ is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.options.key"));
+ is(toolbox.currentToolId, "webconsole", "webconsole is reselected (2)");
+ synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
+ is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
+}
+
+function* testOptions() {
+ let tool = toolbox.getPanel("options");
+ panelWin = tool.panelWin;
+ 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) {
+ let prefValue = GetPref(node.getAttribute("data-pref"));
+
+ // Test clicking the checkbox for each options pref
+ yield testMouseClick(node, prefValue);
+
+ // Do again with opposite values to reset prefs
+ yield testMouseClick(node, !prefValue);
+ }
+
+ let prefSelects = tool.panelDoc.querySelectorAll("select[data-pref]");
+ for (let node of prefSelects) {
+ yield testSelect(node);
+ }
+}
+
+function* testSelect(select) {
+ let pref = select.getAttribute("data-pref");
+ let options = Array.from(select.options);
+ info("Checking select for: " + pref);
+
+ is(select.options[select.selectedIndex].value, GetPref(pref),
+ "select starts out selected");
+
+ for (let option of options) {
+ if (options.indexOf(option) === select.selectedIndex) {
+ continue;
+ }
+
+ let deferred = defer();
+ gDevTools.once("pref-changed", (event, data) => {
+ if (data.pref == pref) {
+ ok(true, "Correct pref was changed");
+ is(GetPref(pref), option.value, "Preference been switched for " + pref);
+ } else {
+ ok(false, "Pref " + pref + " was not changed correctly");
+ }
+ deferred.resolve();
+ });
+
+ select.selectedIndex = options.indexOf(option);
+ let changeEvent = new Event("change");
+ select.dispatchEvent(changeEvent);
+
+ yield deferred.promise;
+ }
+}
+
+function* testMouseClick(node, prefValue) {
+ let deferred = defer();
+
+ let pref = node.getAttribute("data-pref");
+ gDevTools.once("pref-changed", (event, data) => {
+ if (data.pref == pref) {
+ ok(true, "Correct pref was changed");
+ is(data.oldValue, prefValue, "Previous value is correct for " + pref);
+ is(data.newValue, !prefValue, "New value is correct for " + pref);
+ } else {
+ ok(false, "Pref " + pref + " was not changed correctly");
+ }
+ deferred.resolve();
+ });
+
+ node.scrollIntoView();
+
+ // We use executeSoon here to ensure that the element is in view and
+ // clickable.
+ executeSoon(function () {
+ info("Click event synthesized for pref " + pref);
+ EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
+ });
+
+ yield deferred.promise;
+}
+
+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());
+
+ 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);
+ }
+
+ // 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) {
+ yield toggleTool(node);
+ }
+ // Toggle again to reset tool enablement state
+ for (let node of toolNodes) {
+ yield toggleTool(node);
+ }
+
+ // Test that a tool can still be added when no tabs are present:
+ // Disable all tools
+ for (let node of enabledTools) {
+ yield toggleTool(node);
+ }
+ // Re-enable the tools which are enabled by default
+ for (let node of enabledTools) {
+ yield toggleTool(node);
+ }
+
+ // Toggle first, middle, and last tools to ensure that toolbox tabs are
+ // inserted in order
+ let firstTool = toolNodes[0];
+ let middleTool = toolNodes[(toolNodes.length / 2) | 0];
+ let lastTool = toolNodes[toolNodes.length - 1];
+
+ yield toggleTool(firstTool);
+ yield toggleTool(firstTool);
+ yield toggleTool(middleTool);
+ yield toggleTool(middleTool);
+ yield toggleTool(lastTool);
+ yield toggleTool(lastTool);
+}
+
+function* toggleTool(node) {
+ let deferred = defer();
+
+ let toolId = node.getAttribute("id");
+ if (node.checked) {
+ gDevTools.once("tool-unregistered",
+ checkUnregistered.bind(null, toolId, deferred));
+ } else {
+ gDevTools.once("tool-registered",
+ checkRegistered.bind(null, toolId, deferred));
+ }
+ node.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
+
+ yield deferred.promise;
+}
+
+function checkUnregistered(toolId, deferred, event, data) {
+ if (data.id == toolId) {
+ ok(true, "Correct tool removed");
+ // checking tab on the toolbox
+ ok(!doc.getElementById("toolbox-tab-" + toolId),
+ "Tab removed for " + toolId);
+ } else {
+ ok(false, "Something went wrong, " + toolId + " was not unregistered");
+ }
+ deferred.resolve();
+}
+
+function checkRegistered(toolId, deferred, event, data) {
+ if (data == toolId) {
+ ok(true, "Correct tool added back");
+ // checking tab on the toolbox
+ let radio = doc.getElementById("toolbox-tab-" + toolId);
+ ok(radio, "Tab added back for " + toolId);
+ if (radio.previousSibling) {
+ ok(+radio.getAttribute("ordinal") >=
+ +radio.previousSibling.getAttribute("ordinal"),
+ "Inserted tab's ordinal is greater than equal to its previous tab." +
+ "Expected " + radio.getAttribute("ordinal") + " >= " +
+ radio.previousSibling.getAttribute("ordinal"));
+ }
+ if (radio.nextSibling) {
+ ok(+radio.getAttribute("ordinal") <
+ +radio.nextSibling.getAttribute("ordinal"),
+ "Inserted tab's ordinal is less than its next tab. Expected " +
+ radio.getAttribute("ordinal") + " < " +
+ radio.nextSibling.getAttribute("ordinal"));
+ }
+ } else {
+ ok(false, "Something went wrong, " + toolId + " was not registered");
+ }
+ deferred.resolve();
+}
+
+function GetPref(name) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.getCharPref(name);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.getIntPref(name);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.getBoolPref(name);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function* cleanup() {
+ gDevTools.unregisterTool("test-tool");
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+ for (let pref of modifiedPrefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+ toolbox = doc = panelWin = modifiedPrefs = null;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
new file mode 100644
index 000000000..09cde4393
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
@@ -0,0 +1,163 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from shared-head.js */
+"use strict";
+
+const TEST_URL = "data:text/html;charset=utf8,test for dynamically " +
+ "registering and unregistering tools";
+var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
+
+function test() {
+ addTab(TEST_URL).then(tab => {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target)
+ .then(testSelectTool)
+ .then(testToggleToolboxButtons)
+ .then(testPrefsAreRespectedWhenReopeningToolbox)
+ .then(cleanup, errorHandler);
+ });
+}
+
+function testPrefsAreRespectedWhenReopeningToolbox() {
+ let deferred = defer();
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ info("Closing toolbox to test after reopening");
+ gDevTools.closeToolbox(target).then(() => {
+ let tabTarget = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(tabTarget)
+ .then(testSelectTool)
+ .then(() => {
+ info("Toolbox has been reopened. Checking UI state.");
+ testPreferenceAndUIStateIsConsistent();
+ deferred.resolve();
+ });
+ });
+
+ return deferred.promise;
+}
+
+function testSelectTool(devtoolsToolbox) {
+ let deferred = defer();
+ info("Selecting the options panel");
+
+ toolbox = devtoolsToolbox;
+ doc = toolbox.doc;
+ toolbox.once("options-selected", (event, tool) => {
+ ok(true, "Options panel selected via selectTool method");
+ panelWin = tool.panelWin;
+ deferred.resolve();
+ });
+ toolbox.selectTool("options");
+
+ return deferred.promise;
+}
+
+function testPreferenceAndUIStateIsConsistent() {
+ let checkNodes = [...panelWin.document.querySelectorAll(
+ "#enabled-toolbox-buttons-box input[type=checkbox]")];
+ let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
+ toolboxButtonNodes.push(doc.getElementById("command-button-frames"));
+ let toggleableTools = toolbox.toolboxButtons;
+
+ // The noautohide button is only displayed in the browser toolbox
+ toggleableTools = toggleableTools.filter(
+ tool => tool.id != "command-button-noautohide");
+
+ for (let tool of toggleableTools) {
+ let isVisible = getBoolPref(tool.visibilityswitch);
+
+ let button = toolboxButtonNodes.filter(
+ toolboxButton => toolboxButton.id === tool.id)[0];
+ is(!button.hasAttribute("hidden"), isVisible,
+ "Button visibility matches pref for " + tool.id);
+
+ let check = checkNodes.filter(node => node.id === tool.id)[0];
+ is(check.checked, isVisible,
+ "Checkbox should be selected based on current pref for " + tool.id);
+ }
+}
+
+function testToggleToolboxButtons() {
+ let checkNodes = [...panelWin.document.querySelectorAll(
+ "#enabled-toolbox-buttons-box input[type=checkbox]")];
+ let toolboxButtonNodes = [...doc.querySelectorAll(".command-button")];
+ let toggleableTools = toolbox.toolboxButtons;
+
+ // The noautohide button is only displayed in the browser toolbox, and the element
+ // picker button is not toggleable.
+ toggleableTools = toggleableTools.filter(
+ tool => tool.id != "command-button-noautohide" && tool.id != "command-button-pick");
+ toolboxButtonNodes = toolboxButtonNodes.filter(
+ btn => btn.id != "command-button-noautohide" && btn.id != "command-button-pick");
+
+ is(checkNodes.length, toggleableTools.length,
+ "All of the buttons are toggleable.");
+ is(checkNodes.length, toolboxButtonNodes.length,
+ "All of the DOM buttons are toggleable.");
+
+ for (let tool of toggleableTools) {
+ let id = tool.id;
+ let matchedCheckboxes = checkNodes.filter(node => node.id === id);
+ let matchedButtons = toolboxButtonNodes.filter(button => button.id === id);
+ is(matchedCheckboxes.length, 1,
+ "There should be a single toggle checkbox for: " + id);
+ is(matchedButtons.length, 1,
+ "There should be a DOM button for: " + id);
+ is(matchedButtons[0], tool.button,
+ "DOM buttons should match for: " + id);
+
+ is(matchedCheckboxes[0].nextSibling.textContent, tool.label,
+ "The label for checkbox matches the tool definition.");
+ is(matchedButtons[0].getAttribute("title"), tool.label,
+ "The tooltip for button matches the tool definition.");
+ }
+
+ // Store modified pref names so that they can be cleared on error.
+ for (let tool of toggleableTools) {
+ let pref = tool.visibilityswitch;
+ modifiedPrefs.push(pref);
+ }
+
+ // Try checking each checkbox, making sure that it changes the preference
+ for (let node of checkNodes) {
+ let tool = toggleableTools.filter(
+ toggleableTool => toggleableTool.id === node.id)[0];
+ let isVisible = getBoolPref(tool.visibilityswitch);
+
+ testPreferenceAndUIStateIsConsistent();
+ node.click();
+ testPreferenceAndUIStateIsConsistent();
+
+ let isVisibleAfterClick = getBoolPref(tool.visibilityswitch);
+
+ is(isVisible, !isVisibleAfterClick,
+ "Clicking on the node should have toggled visibility preference for " +
+ tool.visibilityswitch);
+ }
+
+ return promise.resolve();
+}
+
+function getBoolPref(key) {
+ return Services.prefs.getBoolPref(key);
+}
+
+function cleanup() {
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+ for (let pref of modifiedPrefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+ toolbox = doc = panelWin = modifiedPrefs = null;
+ finish();
+ });
+}
+
+function errorHandler(error) {
+ ok(false, "Unexpected error: " + error);
+ cleanup();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js b/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js
new file mode 100644
index 000000000..6badf069e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache-01.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+// Tests that disabling the cache for a tab works as it should when toolboxes
+// are not toggled.
+loadHelperScript("helper_disable_cache.js");
+
+add_task(function* () {
+ // Ensure that the setting is cleared after the test.
+ registerCleanupFunction(() => {
+ info("Resetting devtools.cache.disabled to false.");
+ Services.prefs.setBoolPref("devtools.cache.disabled", false);
+ });
+
+ // Initialise tabs: 1 and 2 with a toolbox, 3 and 4 without.
+ for (let tab of tabs) {
+ yield initTab(tab, tab.startToolbox);
+ }
+
+ // Ensure cache is enabled for all tabs.
+ yield checkCacheStateForAllTabs([true, true, true, true]);
+
+ // Check the checkbox in tab 0 and ensure cache is disabled for tabs 0 and 1.
+ yield setDisableCacheCheckboxChecked(tabs[0], true);
+ yield checkCacheStateForAllTabs([false, false, true, true]);
+
+ yield finishUp();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js b/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js
new file mode 100644
index 000000000..38c381cef
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache-02.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+// Tests that disabling the cache for a tab works as it should when toolboxes
+// are toggled.
+loadHelperScript("helper_disable_cache.js");
+
+add_task(function* () {
+ // Ensure that the setting is cleared after the test.
+ registerCleanupFunction(() => {
+ info("Resetting devtools.cache.disabled to false.");
+ Services.prefs.setBoolPref("devtools.cache.disabled", false);
+ });
+
+ // Initialise tabs: 1 and 2 with a toolbox, 3 and 4 without.
+ for (let tab of tabs) {
+ yield initTab(tab, tab.startToolbox);
+ }
+
+ // Disable cache in tab 0
+ yield setDisableCacheCheckboxChecked(tabs[0], true);
+
+ // Open toolbox in tab 2 and ensure the cache is then disabled.
+ tabs[2].toolbox = yield gDevTools.showToolbox(tabs[2].target, "options");
+ yield checkCacheEnabled(tabs[2], false);
+
+ // Close toolbox in tab 2 and ensure the cache is enabled again
+ yield tabs[2].toolbox.destroy();
+ tabs[2].target = TargetFactory.forTab(tabs[2].tab);
+ yield checkCacheEnabled(tabs[2], true);
+
+ // Open toolbox in tab 2 and ensure the cache is then disabled.
+ tabs[2].toolbox = yield gDevTools.showToolbox(tabs[2].target, "options");
+ yield checkCacheEnabled(tabs[2], false);
+
+ // Check the checkbox in tab 2 and ensure cache is enabled for all tabs.
+ yield setDisableCacheCheckboxChecked(tabs[2], false);
+ yield checkCacheStateForAllTabs([true, true, true, true]);
+
+ yield finishUp();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs b/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs
new file mode 100644
index 000000000..c6c336981
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_cache.sjs
@@ -0,0 +1,28 @@
+ /* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ let Etag = '"4d881ab-b03-435f0a0f9ef00"';
+ let IfNoneMatch = request.hasHeader("If-None-Match")
+ ? request.getHeader("If-None-Match")
+ : "";
+
+ let guid = 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ let r = Math.random() * 16 | 0;
+ let v = c === "x" ? r : (r & 0x3 | 0x8);
+
+ return v.toString(16);
+ });
+
+ let page = "<!DOCTYPE html><html><body><h1>" + guid + "</h1></body></html>";
+
+ response.setHeader("Etag", Etag, false);
+
+ if (IfNoneMatch === Etag) {
+ response.setStatusLine(request.httpVersion, "304", "Not Modified");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.write(page);
+ }
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js.html b/devtools/client/framework/test/browser_toolbox_options_disable_js.html
new file mode 100644
index 000000000..8df1119f6
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>browser_toolbox_options_disablejs.html</title>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ width: 260px;
+ height: 24px;
+ border: 1px solid #000;
+ margin-top: 10px;
+ }
+
+ iframe {
+ height: 90px;
+ border: 1px solid #000;
+ }
+
+ h1 {
+ font-size: 20px
+ }
+ </style>
+ <script type="application/javascript;version=1.8">
+ function log(msg) {
+ let output = document.getElementById("output");
+
+ output.innerHTML = msg;
+ }
+ </script>
+ </head>
+ <body>
+ <h1>Test in page</h1>
+ <input id="logJSEnabled"
+ type="button"
+ value="Log JS Enabled"
+ onclick="log('JavaScript Enabled')"/>
+ <input id="logJSDisabled"
+ type="button"
+ value="Log JS Disabled"
+ onclick="log('JavaScript Disabled')"/>
+ <br>
+ <div id="output">No output</div>
+ <h1>Test in iframe</h1>
+ <iframe src="browser_toolbox_options_disable_js_iframe.html"></iframe>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js.js b/devtools/client/framework/test/browser_toolbox_options_disable_js.js
new file mode 100644
index 000000000..b0c14a805
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.js
@@ -0,0 +1,119 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that disabling JavaScript for a tab works as it should.
+
+const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_js.html";
+
+function test() {
+ addTab(TEST_URI).then(tab => {
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target).then(testSelectTool);
+ });
+}
+
+function testSelectTool(toolbox) {
+ toolbox.once("options-selected", () => testToggleJS(toolbox));
+ toolbox.selectTool("options");
+}
+
+let testToggleJS = Task.async(function* (toolbox) {
+ ok(true, "Toolbox selected via selectTool method");
+
+ yield testJSEnabled();
+ yield testJSEnabledIframe();
+
+ // Disable JS.
+ yield toggleJS(toolbox);
+
+ yield testJSDisabled();
+ yield testJSDisabledIframe();
+
+ // Re-enable JS.
+ yield toggleJS(toolbox);
+
+ yield testJSEnabled();
+ yield testJSEnabledIframe();
+
+ finishUp(toolbox);
+});
+
+function* testJSEnabled() {
+ info("Testing that JS is enabled");
+
+ // We use waitForTick here because switching docShell.allowJavascript to true
+ // takes a while to become live.
+ yield waitForTick();
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let output = doc.getElementById("output");
+ doc.querySelector("#logJSEnabled").click();
+ is(output.textContent, "JavaScript Enabled", 'Output is "JavaScript Enabled"');
+ });
+}
+
+function* testJSEnabledIframe() {
+ info("Testing that JS is enabled in the iframe");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let iframe = doc.querySelector("iframe");
+ let iframeDoc = iframe.contentDocument;
+ let output = iframeDoc.getElementById("output");
+ iframeDoc.querySelector("#logJSEnabled").click();
+ is(output.textContent, "JavaScript Enabled",
+ 'Output is "JavaScript Enabled" in iframe');
+ });
+}
+
+function* toggleJS(toolbox) {
+ let panel = toolbox.getCurrentPanel();
+ let cbx = panel.panelDoc.getElementById("devtools-disable-javascript");
+
+ if (cbx.checked) {
+ info("Clearing checkbox to re-enable JS");
+ } else {
+ info("Checking checkbox to disable JS");
+ }
+
+ let browserLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ cbx.click();
+ yield browserLoaded;
+}
+
+function* testJSDisabled() {
+ info("Testing that JS is disabled");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let output = doc.getElementById("output");
+ doc.querySelector("#logJSDisabled").click();
+
+ ok(output.textContent !== "JavaScript Disabled",
+ 'output is not "JavaScript Disabled"');
+ });
+}
+
+function* testJSDisabledIframe() {
+ info("Testing that JS is disabled in the iframe");
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let iframe = doc.querySelector("iframe");
+ let iframeDoc = iframe.contentDocument;
+ let output = iframeDoc.getElementById("output");
+ iframeDoc.querySelector("#logJSDisabled").click();
+ ok(output.textContent !== "JavaScript Disabled",
+ 'output is not "JavaScript Disabled" in iframe');
+ });
+}
+
+function finishUp(toolbox) {
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html b/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html
new file mode 100644
index 000000000..777bf86bf
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js_iframe.html
@@ -0,0 +1,33 @@
+<html>
+ <head>
+ <title>browser_toolbox_options_disablejs.html</title>
+ <meta charset="UTF-8">
+ <style>
+ div {
+ width: 260px;
+ height: 24px;
+ border: 1px solid #000;
+ margin-top: 10px;
+ }
+ </style>
+ <script type="application/javascript;version=1.8">
+ function log(msg) {
+ let output = document.getElementById("output");
+
+ output.innerHTML = msg;
+ }
+ </script>
+ </head>
+ <body>
+ <input id="logJSEnabled"
+ type="button"
+ value="Log JS Enabled"
+ onclick="log('JavaScript Enabled')"/>
+ <input id="logJSDisabled"
+ type="button"
+ value="Log JS Disabled"
+ onclick="log('JavaScript Disabled')"/>
+ <br>
+ <div id="output">No output</div>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html
new file mode 100644
index 000000000..0e4b824cb
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>browser_toolbox_options_enable_serviceworkers_testing.html</title>
+ <meta charset="UTF-8">
+ </head>
+ <body>
+ <h1>SW-test</h1>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js
new file mode 100644
index 000000000..3273f4395
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js
@@ -0,0 +1,126 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that enabling Service Workers testing option enables the
+// mServiceWorkersTestingEnabled attribute added to nsPIDOMWindow.
+
+const COMMON_FRAME_SCRIPT_URL =
+ "chrome://devtools/content/shared/frame-script-utils.js";
+const ROOT_TEST_DIR =
+ getRootDirectory(gTestPath);
+const FRAME_SCRIPT_URL =
+ ROOT_TEST_DIR +
+ "browser_toolbox_options_enable_serviceworkers_testing_frame_script.js";
+const TEST_URI = URL_ROOT +
+ "browser_toolbox_options_enable_serviceworkers_testing.html";
+
+const ELEMENT_ID = "devtools-enable-serviceWorkersTesting";
+
+var toolbox;
+
+function test() {
+ // Note: Pref dom.serviceWorkers.testing.enabled is false since we are testing
+ // the same capabilities are enabled with the devtool pref.
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", false]
+ ]}, init);
+}
+
+function init() {
+ addTab(TEST_URI).then(tab => {
+ let target = TargetFactory.forTab(tab);
+ let linkedBrowser = tab.linkedBrowser;
+
+ linkedBrowser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
+ linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+
+ gDevTools.showToolbox(target).then(testSelectTool);
+ });
+}
+
+function testSelectTool(aToolbox) {
+ toolbox = aToolbox;
+ toolbox.once("options-selected", start);
+ toolbox.selectTool("options");
+}
+
+function register() {
+ return executeInContent("devtools:sw-test:register");
+}
+
+function unregister(swr) {
+ return executeInContent("devtools:sw-test:unregister");
+}
+
+function registerAndUnregisterInFrame() {
+ return executeInContent("devtools:sw-test:iframe:register-and-unregister");
+}
+
+function testRegisterFails(data) {
+ is(data.success, false, "Register should fail with security error");
+ return promise.resolve();
+}
+
+function toggleServiceWorkersTestingCheckbox() {
+ let panel = toolbox.getCurrentPanel();
+ let cbx = panel.panelDoc.getElementById(ELEMENT_ID);
+
+ cbx.scrollIntoView();
+
+ if (cbx.checked) {
+ info("Clearing checkbox to disable service workers testing");
+ } else {
+ info("Checking checkbox to enable service workers testing");
+ }
+
+ cbx.click();
+
+ return promise.resolve();
+}
+
+function reload() {
+ let deferred = defer();
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+ gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+ deferred.resolve();
+ }, true);
+
+ executeInContent("devtools:test:reload", {}, {}, false);
+ return deferred.promise;
+}
+
+function testRegisterSuccesses(data) {
+ is(data.success, true, "Register should success");
+ return promise.resolve();
+}
+
+function start() {
+ register()
+ .then(testRegisterFails)
+ .then(toggleServiceWorkersTestingCheckbox)
+ .then(reload)
+ .then(register)
+ .then(testRegisterSuccesses)
+ .then(unregister)
+ .then(registerAndUnregisterInFrame)
+ .then(testRegisterSuccesses)
+ // Workers should be turned back off when we closes the toolbox
+ .then(toolbox.destroy.bind(toolbox))
+ .then(reload)
+ .then(register)
+ .then(testRegisterFails)
+ .catch(function (e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(finishUp);
+}
+
+function finishUp() {
+ gBrowser.removeCurrentTab();
+ toolbox = null;
+ finish();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing_frame_script.js b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
new file mode 100644
index 000000000..ec5ab3762
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// A helper frame-script for devtools/client/framework service worker tests.
+
+"use strict";
+
+addMessageListener("devtools:sw-test:register", function (msg) {
+ content.navigator.serviceWorker.register("serviceworker.js")
+ .then(swr => {
+ sendAsyncMessage("devtools:sw-test:register", {success: true});
+ }, error => {
+ sendAsyncMessage("devtools:sw-test:register", {success: false});
+ });
+});
+
+addMessageListener("devtools:sw-test:unregister", function (msg) {
+ content.navigator.serviceWorker.getRegistration().then(swr => {
+ swr.unregister().then(result => {
+ sendAsyncMessage("devtools:sw-test:unregister",
+ {success: result ? true : false});
+ });
+ });
+});
+
+addMessageListener("devtools:sw-test:iframe:register-and-unregister", function (msg) {
+ var frame = content.document.createElement("iframe");
+ frame.addEventListener("load", function onLoad() {
+ frame.removeEventListener("load", onLoad);
+ frame.contentWindow.navigator.serviceWorker.register("serviceworker.js")
+ .then(swr => {
+ return swr.unregister();
+ }).then(_ => {
+ frame.remove();
+ sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
+ {success: true});
+ }).catch(error => {
+ sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
+ {success: false});
+ });
+ });
+ frame.src = "browser_toolbox_options_enabled_serviceworkers_testing.html";
+ content.document.body.appendChild(frame);
+});
diff --git a/devtools/client/framework/test/browser_toolbox_races.js b/devtools/client/framework/test/browser_toolbox_races.js
new file mode 100644
index 000000000..fedbc4402
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_races.js
@@ -0,0 +1,81 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the toolbox quickly and see if there is any race breaking it.
+
+const URL = "data:text/html;charset=utf-8,Toggling devtools quickly";
+
+add_task(function* () {
+ // Make sure this test starts with the selectedTool pref cleared. Previous
+ // tests select various tools, and that sets this pref.
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+
+ let tab = yield addTab(URL);
+
+ let created = 0, ready = 0, destroy = 0, destroyed = 0;
+ let onCreated = () => {
+ created++;
+ };
+ let onReady = () => {
+ ready++;
+ };
+ let onDestroy = () => {
+ destroy++;
+ };
+ let onDestroyed = () => {
+ destroyed++;
+ };
+ gDevTools.on("toolbox-created", onCreated);
+ gDevTools.on("toolbox-ready", onReady);
+ gDevTools.on("toolbox-destroy", onDestroy);
+ gDevTools.on("toolbox-destroyed", onDestroyed);
+
+ // The current implementation won't toggle the toolbox many times,
+ // instead it will ignore toggles that happens while the toolbox is still
+ // creating or still destroying.
+
+ // Toggle the toolbox at least 3 times.
+ info("Trying to toggle the toolbox 3 times");
+ while (created < 3) {
+ // Sent multiple event to try to race the code during toolbox creation and destruction
+ toggle();
+ toggle();
+ toggle();
+
+ // Release the event loop to let a chance to actually create or destroy the toolbox!
+ yield wait(50);
+ }
+ info("Toggled the toolbox 3 times");
+
+ // Now wait for the 3rd toolbox to be fully ready before closing it.
+ // We close the last toolbox manually, out of the first while() loop to
+ // avoid races and be sure we end up we no toolbox and waited for all the
+ // requests to be done.
+ while (ready != 3) {
+ yield wait(100);
+ }
+ toggle();
+ while (destroyed != 3) {
+ yield wait(100);
+ }
+
+ is(created, 3, "right number of created events");
+ is(ready, 3, "right number of ready events");
+ is(destroy, 3, "right number of destroy events");
+ is(destroyed, 3, "right number of destroyed events");
+
+ gDevTools.off("toolbox-created", onCreated);
+ gDevTools.off("toolbox-ready", onReady);
+ gDevTools.off("toolbox-destroy", onDestroy);
+ gDevTools.off("toolbox-destroyed", onDestroyed);
+
+ gBrowser.removeCurrentTab();
+});
+
+function toggle() {
+ EventUtils.synthesizeKey("VK_F12", {});
+}
diff --git a/devtools/client/framework/test/browser_toolbox_raise.js b/devtools/client/framework/test/browser_toolbox_raise.js
new file mode 100644
index 000000000..0af1a4571
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_raise.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = "data:text/html,test for opening toolbox in different hosts";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+var toolbox, tab1, tab2;
+
+function test() {
+ addTab(TEST_URL).then(tab => {
+ tab2 = gBrowser.addTab();
+ let target = TargetFactory.forTab(tab);
+ gDevTools.showToolbox(target)
+ .then(testBottomHost, console.error)
+ .then(null, console.error);
+ });
+}
+
+function testBottomHost(aToolbox) {
+ toolbox = aToolbox;
+
+ // switch to another tab and test toolbox.raise()
+ gBrowser.selectedTab = tab2;
+ executeSoon(function () {
+ is(gBrowser.selectedTab, tab2, "Correct tab is selected before calling raise");
+ toolbox.raise();
+ executeSoon(function () {
+ is(gBrowser.selectedTab, tab1, "Correct tab was selected after calling raise");
+
+ toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost).then(null, console.error);
+ });
+ });
+}
+
+function testWindowHost() {
+ // Make sure toolbox is not focused.
+ window.addEventListener("focus", onFocus, true);
+
+ // Need to wait for focus as otherwise window.focus() is overridden by
+ // toolbox window getting focused first on Linux and Mac.
+ let onToolboxFocus = () => {
+ toolbox.win.parent.removeEventListener("focus", onToolboxFocus, true);
+ info("focusing main window.");
+ window.focus();
+ };
+ // Need to wait for toolbox window to get focus.
+ toolbox.win.parent.addEventListener("focus", onToolboxFocus, true);
+}
+
+function onFocus() {
+ info("Main window is focused before calling toolbox.raise()");
+ window.removeEventListener("focus", onFocus, true);
+
+ // Check if toolbox window got focus.
+ let onToolboxFocusAgain = () => {
+ toolbox.win.parent.removeEventListener("focus", onToolboxFocusAgain, false);
+ ok(true, "Toolbox window is the focused window after calling toolbox.raise()");
+ cleanup();
+ };
+ toolbox.win.parent.addEventListener("focus", onToolboxFocusAgain, false);
+
+ // Now raise toolbox.
+ toolbox.raise();
+}
+
+function cleanup() {
+ Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
+
+ toolbox.destroy().then(function () {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_ready.js b/devtools/client/framework/test/browser_toolbox_ready.js
new file mode 100644
index 000000000..e1a59b3f0
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_ready.js
@@ -0,0 +1,21 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URL = "data:text/html,test for toolbox being ready";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+ let target = TargetFactory.forTab(tab);
+
+ const toolbox = yield gDevTools.showToolbox(target, "webconsole");
+ ok(toolbox.isReady, "toolbox isReady is set");
+ ok(toolbox.threadClient, "toolbox has a thread client");
+
+ const toolbox2 = yield gDevTools.showToolbox(toolbox.target, toolbox.toolId);
+ is(toolbox2, toolbox, "same toolbox");
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_remoteness_change.js b/devtools/client/framework/test/browser_toolbox_remoteness_change.js
new file mode 100644
index 000000000..b30d633fa
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_remoteness_change.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const URL_1 = "about:robots";
+const URL_2 = "data:text/html;charset=UTF-8," +
+ encodeURIComponent("<div id=\"remote-page\">foo</div>");
+
+add_task(function* () {
+ info("Open a tab on a URL supporting only running in parent process");
+ let tab = yield addTab(URL_1);
+ is(tab.linkedBrowser.currentURI.spec, URL_1, "We really are on the expected document");
+ is(tab.linkedBrowser.getAttribute("remote"), "", "And running in parent process");
+
+ let toolbox = yield openToolboxForTab(tab);
+
+ let onToolboxDestroyed = toolbox.once("destroyed");
+ let onToolboxCreated = gDevTools.once("toolbox-created");
+
+ info("Navigate to a URL supporting remote process");
+ let onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ gBrowser.loadURI(URL_2);
+ yield onLoaded;
+
+ is(tab.linkedBrowser.getAttribute("remote"), "true", "Navigated to a data: URI and switching to remote");
+
+ info("Waiting for the toolbox to be destroyed");
+ yield onToolboxDestroyed;
+
+ info("Waiting for a new toolbox to be created");
+ toolbox = yield onToolboxCreated;
+
+ info("Waiting for the new toolbox to be ready");
+ yield toolbox.once("ready");
+
+ info("Veryify we are inspecting the new document");
+ let console = yield toolbox.selectTool("webconsole");
+ let { jsterm } = console.hud;
+ let url = yield jsterm.execute("document.location.href");
+ // Uses includes as the old console frontend prints a timestamp
+ ok(url.textContent.includes(URL_2), "The console inspects the second document");
+});
diff --git a/devtools/client/framework/test/browser_toolbox_select_event.js b/devtools/client/framework/test/browser_toolbox_select_event.js
new file mode 100644
index 000000000..ae104524e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_select_event.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PAGE_URL = "data:text/html;charset=utf-8,test select events";
+
+requestLongerTimeout(2);
+
+add_task(function* () {
+ let tab = yield addTab(PAGE_URL);
+
+ let toolbox = yield openToolboxForTab(tab, "webconsole", "bottom");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+
+ yield testToolSelectEvent("inspector");
+ yield testToolSelectEvent("webconsole");
+ yield testToolSelectEvent("styleeditor");
+ yield toolbox.destroy();
+
+ toolbox = yield openToolboxForTab(tab, "webconsole", "side");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield toolbox.destroy();
+
+ toolbox = yield openToolboxForTab(tab, "webconsole", "window");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield testSelectEvent("inspector");
+ yield testSelectEvent("webconsole");
+ yield testSelectEvent("styleeditor");
+ yield toolbox.destroy();
+
+ yield testSelectToolRace();
+
+ /**
+ * Assert that selecting the given toolId raises a select event
+ * @param {toolId} Id of the tool to test
+ */
+ function* testSelectEvent(toolId) {
+ let onSelect = toolbox.once("select");
+ toolbox.selectTool(toolId);
+ let id = yield onSelect;
+ is(id, toolId, toolId + " selected");
+ }
+
+ /**
+ * Assert that selecting the given toolId raises its corresponding
+ * selected event
+ * @param {toolId} Id of the tool to test
+ */
+ function* testToolSelectEvent(toolId) {
+ let onSelected = toolbox.once(toolId + "-selected");
+ toolbox.selectTool(toolId);
+ yield onSelected;
+ is(toolbox.currentToolId, toolId, toolId + " tool selected");
+ }
+
+ /**
+ * Assert that two calls to selectTool won't race
+ */
+ function* testSelectToolRace() {
+ let toolbox = yield openToolboxForTab(tab, "webconsole");
+ let selected = false;
+ let onSelect = (event, id) => {
+ if (selected) {
+ ok(false, "Got more than one 'select' event");
+ } else {
+ selected = true;
+ }
+ };
+ toolbox.once("select", onSelect);
+ let p1 = toolbox.selectTool("inspector")
+ let p2 = toolbox.selectTool("inspector");
+ // Check that both promises don't resolve too early
+ let checkSelectToolResolution = panel => {
+ ok(selected, "selectTool resolves only after 'select' event is fired");
+ let inspector = toolbox.getPanel("inspector");
+ is(panel, inspector, "selecTool resolves to the panel instance");
+ };
+ p1.then(checkSelectToolResolution);
+ p2.then(checkSelectToolResolution);
+ yield p1;
+ yield p2;
+
+ yield toolbox.destroy();
+ }
+});
+
diff --git a/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js b/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js
new file mode 100644
index 000000000..d7cc5c94d
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_selected_tool_unavailable.js
@@ -0,0 +1,48 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that opening the toolbox doesn't throw when the previously selected
+// tool is not supported.
+
+const testToolDefinition = {
+ id: "test-tool",
+ isTargetSupported: () => true,
+ visibilityswitch: "devtools.test-tool.enabled",
+ url: "about:blank",
+ label: "someLabel",
+ build: (iframeWindow, toolbox) => {
+ return {
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: () => {},
+ panelDoc: iframeWindow.document
+ };
+ }
+};
+
+add_task(function* () {
+ gDevTools.registerTool(testToolDefinition);
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+
+ let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+ is(toolbox.currentToolId, "test-tool", "test-tool was selected");
+ yield gDevTools.closeToolbox(target);
+
+ // Make the previously selected tool unavailable.
+ testToolDefinition.isTargetSupported = () => false;
+
+ target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target);
+ is(toolbox.currentToolId, "webconsole", "web console was selected");
+
+ yield gDevTools.closeToolbox(target);
+ gDevTools.unregisterTool(testToolDefinition.id);
+ tab = toolbox = target = null;
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar.js b/devtools/client/framework/test/browser_toolbox_sidebar.js
new file mode 100644
index 000000000..897f8cba5
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar.js
@@ -0,0 +1,181 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ const Cu = Components.utils;
+ let {ToolSidebar} = require("devtools/client/framework/sidebar");
+
+ const toolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
+ "<hbox flex='1'><description flex='1'>foo</description><splitter class='devtools-side-splitter'/>" +
+ "<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'><tabs/><tabpanels flex='1'/></tabbox>" +
+ "</hbox>" +
+ "</window>";
+
+ const tab1URL = "data:text/html;charset=utf8,<title>1</title><p>1</p>";
+ const tab2URL = "data:text/html;charset=utf8,<title>2</title><p>2</p>";
+ const tab3URL = "data:text/html;charset=utf8,<title>3</title><p>3</p>";
+
+ let panelDoc;
+ let tab1Selected = false;
+ let registeredTabs = {};
+ let readyTabs = {};
+
+ let toolDefinition = {
+ id: "fakeTool4242",
+ visibilityswitch: "devtools.fakeTool4242.enabled",
+ url: toolURL,
+ label: "FAKE TOOL!!!",
+ isTargetSupported: () => true,
+ build: function (iframeWindow, toolbox) {
+ let deferred = defer();
+ executeSoon(() => {
+ deferred.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: function () {},
+ panelDoc: iframeWindow.document,
+ });
+ });
+ return deferred.promise;
+ },
+ };
+
+ gDevTools.registerTool(toolDefinition);
+
+ addTab("about:blank").then(function (aTab) {
+ let target = TargetFactory.forTab(aTab);
+ gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
+ let panel = toolbox.getPanel(toolDefinition.id);
+ panel.toolbox = toolbox;
+ ok(true, "Tool open");
+
+ let tabbox = panel.panelDoc.getElementById("sidebar");
+ panel.sidebar = new ToolSidebar(tabbox, panel, "testbug865688", true);
+
+ panel.sidebar.on("new-tab-registered", function (event, id) {
+ registeredTabs[id] = true;
+ });
+
+ panel.sidebar.once("tab1-ready", function (event) {
+ info(event);
+ readyTabs.tab1 = true;
+ allTabsReady(panel);
+ });
+
+ panel.sidebar.once("tab2-ready", function (event) {
+ info(event);
+ readyTabs.tab2 = true;
+ allTabsReady(panel);
+ });
+
+ panel.sidebar.once("tab3-ready", function (event) {
+ info(event);
+ readyTabs.tab3 = true;
+ allTabsReady(panel);
+ });
+
+ panel.sidebar.once("tab1-selected", function (event) {
+ info(event);
+ tab1Selected = true;
+ allTabsReady(panel);
+ });
+
+ panel.sidebar.addTab("tab1", tab1URL, {selected: true});
+ panel.sidebar.addTab("tab2", tab2URL);
+ panel.sidebar.addTab("tab3", tab3URL);
+
+ panel.sidebar.show();
+ }).then(null, console.error);
+ });
+
+ function allTabsReady(panel) {
+ if (!tab1Selected || !readyTabs.tab1 || !readyTabs.tab2 || !readyTabs.tab3) {
+ return;
+ }
+
+ ok(registeredTabs.tab1, "tab1 registered");
+ ok(registeredTabs.tab2, "tab2 registered");
+ ok(registeredTabs.tab3, "tab3 registered");
+ ok(readyTabs.tab1, "tab1 ready");
+ ok(readyTabs.tab2, "tab2 ready");
+ ok(readyTabs.tab3, "tab3 ready");
+
+ let tabs = panel.sidebar._tabbox.querySelectorAll("tab");
+ let panels = panel.sidebar._tabbox.querySelectorAll("tabpanel");
+ let label = 1;
+ for (let tab of tabs) {
+ is(tab.getAttribute("label"), label++, "Tab has the right title");
+ }
+
+ is(label, 4, "Found the right amount of tabs.");
+ is(panel.sidebar._tabbox.selectedPanel, panels[0], "First tab is selected");
+ is(panel.sidebar.getCurrentTabID(), "tab1", "getCurrentTabID() is correct");
+
+ panel.sidebar.once("tab1-unselected", function () {
+ ok(true, "received 'unselected' event");
+ panel.sidebar.once("tab2-selected", function () {
+ ok(true, "received 'selected' event");
+ tabs[1].focus();
+ is(panel.sidebar._panelDoc.activeElement, tabs[1],
+ "Focus is set to second tab");
+ panel.sidebar.hide();
+ isnot(panel.sidebar._panelDoc.activeElement, tabs[1],
+ "Focus is reset for sidebar");
+ is(panel.sidebar._tabbox.getAttribute("hidden"), "true", "Sidebar hidden");
+ is(panel.sidebar.getWindowForTab("tab1").location.href, tab1URL, "Window is accessible");
+ testRemoval(panel);
+ });
+ });
+
+ panel.sidebar.select("tab2");
+ }
+
+ function testRemoval(panel) {
+ panel.sidebar.once("tab-unregistered", function (event, id) {
+ info(event);
+ registeredTabs[id] = false;
+
+ is(id, "tab3", "The right tab must be removed");
+
+ let tabs = panel.sidebar._tabbox.querySelectorAll("tab");
+ let panels = panel.sidebar._tabbox.querySelectorAll("tabpanel");
+
+ is(tabs.length, 2, "There is the right number of tabs");
+ is(panels.length, 2, "There is the right number of panels");
+
+ testWidth(panel);
+ });
+
+ panel.sidebar.removeTab("tab3");
+ }
+
+ function testWidth(panel) {
+ let tabbox = panel.panelDoc.getElementById("sidebar");
+ tabbox.width = 420;
+ panel.sidebar.destroy().then(function () {
+ tabbox.width = 0;
+ panel.sidebar = new ToolSidebar(tabbox, panel, "testbug865688", true);
+ panel.sidebar.show();
+ is(panel.panelDoc.getElementById("sidebar").width, 420, "Width restored");
+
+ finishUp(panel);
+ });
+ }
+
+ function finishUp(panel) {
+ panel.sidebar.destroy();
+ panel.toolbox.destroy().then(function () {
+ gDevTools.unregisterTool(toolDefinition.id);
+
+ gBrowser.removeCurrentTab();
+
+ executeSoon(function () {
+ finish();
+ });
+ });
+ }
+}
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar_events.js b/devtools/client/framework/test/browser_toolbox_sidebar_events.js
new file mode 100644
index 000000000..9137aaebe
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_events.js
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ const Cu = Components.utils;
+ const { ToolSidebar } = require("devtools/client/framework/sidebar");
+
+ const toolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
+ "<hbox flex='1'><description flex='1'>foo</description><splitter class='devtools-side-splitter'/>" +
+ "<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'><tabs/><tabpanels flex='1'/></tabbox>" +
+ "</hbox>" +
+ "</window>";
+
+ const tab1URL = "data:text/html;charset=utf8,<title>1</title><p>1</p>";
+
+ let collectedEvents = [];
+
+ let toolDefinition = {
+ id: "testTool1072208",
+ visibilityswitch: "devtools.testTool1072208.enabled",
+ url: toolURL,
+ label: "Test tool",
+ isTargetSupported: () => true,
+ build: function (iframeWindow, toolbox) {
+ let deferred = defer();
+ executeSoon(() => {
+ deferred.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: function () {},
+ panelDoc: iframeWindow.document,
+ });
+ });
+ return deferred.promise;
+ },
+ };
+
+ gDevTools.registerTool(toolDefinition);
+
+ addTab("about:blank").then(function (aTab) {
+ let target = TargetFactory.forTab(aTab);
+ gDevTools.showToolbox(target, toolDefinition.id).then(function (toolbox) {
+ let panel = toolbox.getPanel(toolDefinition.id);
+ ok(true, "Tool open");
+
+ panel.once("sidebar-created", function (event, id) {
+ collectedEvents.push(event);
+ });
+
+ panel.once("sidebar-destroyed", function (event, id) {
+ collectedEvents.push(event);
+ });
+
+ let tabbox = panel.panelDoc.getElementById("sidebar");
+ panel.sidebar = new ToolSidebar(tabbox, panel, "testbug1072208", true);
+
+ panel.sidebar.once("show", function (event, id) {
+ collectedEvents.push(event);
+ });
+
+ panel.sidebar.once("hide", function (event, id) {
+ collectedEvents.push(event);
+ });
+
+ panel.sidebar.once("tab1-selected", () => finishUp(panel));
+ panel.sidebar.addTab("tab1", tab1URL, {selected: true});
+ panel.sidebar.show();
+ }).then(null, console.error);
+ });
+
+ function finishUp(panel) {
+ panel.sidebar.hide();
+ panel.sidebar.destroy();
+
+ let events = collectedEvents.join(":");
+ is(events, "sidebar-created:show:hide:sidebar-destroyed",
+ "Found the right amount of collected events.");
+
+ panel.toolbox.destroy().then(function () {
+ gDevTools.unregisterTool(toolDefinition.id);
+ gBrowser.removeCurrentTab();
+
+ executeSoon(function () {
+ finish();
+ });
+ });
+ }
+}
+
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar_existing_tabs.js b/devtools/client/framework/test/browser_toolbox_sidebar_existing_tabs.js
new file mode 100644
index 000000000..339687e10
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_existing_tabs.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the sidebar widget auto-registers existing tabs.
+
+const {ToolSidebar} = require("devtools/client/framework/sidebar");
+
+const testToolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" +
+ "<hbox flex='1'><description flex='1'>test tool</description>" +
+ "<splitter class='devtools-side-splitter'/>" +
+ "<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'>" +
+ "<tabs><tab id='tab1' label='tab 1'></tab><tab id='tab2' label='tab 2'></tab></tabs>" +
+ "<tabpanels flex='1'><tabpanel id='tabpanel1'>tab 1</tabpanel><tabpanel id='tabpanel2'>tab 2</tabpanel></tabpanels>" +
+ "</tabbox></hbox></window>";
+
+const testToolDefinition = {
+ id: "testTool",
+ url: testToolURL,
+ label: "Test Tool",
+ isTargetSupported: () => true,
+ build: (iframeWindow, toolbox) => {
+ return promise.resolve({
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: () => {},
+ panelDoc: iframeWindow.document,
+ });
+ }
+};
+
+add_task(function* () {
+ let tab = yield addTab("about:blank");
+
+ let target = TargetFactory.forTab(tab);
+
+ gDevTools.registerTool(testToolDefinition);
+ let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+
+ let toolPanel = toolbox.getPanel(testToolDefinition.id);
+ let tabbox = toolPanel.panelDoc.getElementById("sidebar");
+
+ info("Creating the sidebar widget");
+ let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1101569");
+
+ info("Checking that existing tabs have been registered");
+ ok(sidebar.getTab("tab1"), "Existing tab 1 was found");
+ ok(sidebar.getTab("tab2"), "Existing tab 2 was found");
+ ok(sidebar.getTabPanel("tabpanel1"), "Existing tabpanel 1 was found");
+ ok(sidebar.getTabPanel("tabpanel2"), "Existing tabpanel 2 was found");
+
+ info("Checking that the sidebar API works with existing tabs");
+
+ sidebar.select("tab2");
+ is(tabbox.selectedTab, tabbox.querySelector("#tab2"),
+ "Existing tabs can be selected");
+
+ sidebar.select("tab1");
+ is(tabbox.selectedTab, tabbox.querySelector("#tab1"),
+ "Existing tabs can be selected");
+
+ is(sidebar.getCurrentTabID(), "tab1", "getCurrentTabID returns the expected id");
+
+ info("Removing a tab");
+ sidebar.removeTab("tab2", "tabpanel2");
+ ok(!sidebar.getTab("tab2"), "Tab 2 was removed correctly");
+ ok(!sidebar.getTabPanel("tabpanel2"), "Tabpanel 2 was removed correctly");
+
+ sidebar.destroy();
+ yield toolbox.destroy();
+ gDevTools.unregisterTool(testToolDefinition.id);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js b/devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js
new file mode 100644
index 000000000..5f6914a2f
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_overflow_menu.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the sidebar widget correctly displays the "all tabs..." button
+// when the tabs overflow.
+
+const {ToolSidebar} = require("devtools/client/framework/sidebar");
+
+const testToolDefinition = {
+ id: "testTool",
+ url: CHROME_URL_ROOT + "browser_toolbox_sidebar_tool.xul",
+ label: "Test Tool",
+ isTargetSupported: () => true,
+ build: (iframeWindow, toolbox) => {
+ return {
+ target: toolbox.target,
+ toolbox: toolbox,
+ isReady: true,
+ destroy: () => {},
+ panelDoc: iframeWindow.document,
+ };
+ }
+};
+
+add_task(function* () {
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+
+ gDevTools.registerTool(testToolDefinition);
+ let toolbox = yield gDevTools.showToolbox(target, testToolDefinition.id);
+
+ let toolPanel = toolbox.getPanel(testToolDefinition.id);
+ let tabbox = toolPanel.panelDoc.getElementById("sidebar");
+
+ info("Creating the sidebar widget");
+ let sidebar = new ToolSidebar(tabbox, toolPanel, "bug1101569", {
+ showAllTabsMenu: true
+ });
+
+ let allTabsMenu = toolPanel.panelDoc.querySelector(".devtools-sidebar-alltabs");
+ ok(allTabsMenu, "The all-tabs menu is available");
+ is(allTabsMenu.getAttribute("hidden"), "true", "The menu is hidden for now");
+
+ info("Adding 10 tabs to the sidebar widget");
+ for (let nb = 0; nb < 10; nb++) {
+ let url = `data:text/html;charset=utf8,<title>tab ${nb}</title><p>Test tab ${nb}</p>`;
+ sidebar.addTab("tab" + nb, url, {selected: nb === 0});
+ }
+
+ info("Fake an overflow event so that the all-tabs menu is visible");
+ sidebar._onTabBoxOverflow();
+ ok(!allTabsMenu.hasAttribute("hidden"), "The all-tabs menu is now shown");
+
+ info("Select each tab, one by one");
+ for (let nb = 0; nb < 10; nb++) {
+ let id = "tab" + nb;
+
+ info("Found tab item nb " + nb);
+ let item = allTabsMenu.querySelector("#sidebar-alltabs-item-" + id);
+
+ info("Click on the tab");
+ EventUtils.sendMouseEvent({type: "click"}, item, toolPanel.panelDoc.defaultView);
+
+ is(tabbox.selectedTab.id, "sidebar-tab-" + id,
+ "The selected tab is now nb " + nb);
+ }
+
+ info("Fake an underflow event so that the all-tabs menu gets hidden");
+ sidebar._onTabBoxUnderflow();
+ is(allTabsMenu.getAttribute("hidden"), "true", "The all-tabs menu is hidden");
+
+ yield sidebar.destroy();
+ yield toolbox.destroy();
+ gDevTools.unregisterTool(testToolDefinition.id);
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_sidebar_tool.xul b/devtools/client/framework/test/browser_toolbox_sidebar_tool.xul
new file mode 100644
index 000000000..2ce495158
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_sidebar_tool.xul
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
+ <box flex="1" class="devtools-responsive-container theme-body">
+ <vbox flex="1" class="devtools-main-content" id="content">test</vbox>
+ <splitter class="devtools-side-splitter"/>
+ <tabbox flex="1" id="sidebar" class="devtools-sidebar-tabs">
+ <tabs/>
+ <tabpanels flex="1"/>
+ </tabbox>
+ </box>
+</window>
diff --git a/devtools/client/framework/test/browser_toolbox_split_console.js b/devtools/client/framework/test/browser_toolbox_split_console.js
new file mode 100644
index 000000000..8e1fecd15
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_split_console.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that these toolbox split console APIs work:
+// * toolbox.useKeyWithSplitConsole()
+// * toolbox.isSplitConsoleFocused
+
+let gToolbox = null;
+let panelWin = null;
+
+const URL = "data:text/html;charset=utf8,test split console key delegation";
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+add_task(function* () {
+ let tab = yield addTab(URL);
+ let target = TargetFactory.forTab(tab);
+ gToolbox = yield gDevTools.showToolbox(target, "jsdebugger");
+ panelWin = gToolbox.getPanel("jsdebugger").panelWin;
+
+ yield gToolbox.openSplitConsole();
+ yield testIsSplitConsoleFocused();
+ yield testUseKeyWithSplitConsole();
+ yield testUseKeyWithSplitConsoleWrongTool();
+
+ yield cleanup();
+});
+
+function* testIsSplitConsoleFocused() {
+ yield gToolbox.openSplitConsole();
+ // The newly opened split console should have focus
+ ok(gToolbox.isSplitConsoleFocused(), "Split console is focused");
+ panelWin.focus();
+ ok(!gToolbox.isSplitConsoleFocused(), "Split console is no longer focused");
+}
+
+// A key bound to the selected tool should trigger it's command
+function* testUseKeyWithSplitConsole() {
+ let commandCalled = false;
+
+ info("useKeyWithSplitConsole on debugger while debugger is focused");
+ gToolbox.useKeyWithSplitConsole("F3", () => {
+ commandCalled = true;
+ }, "jsdebugger");
+
+ info("synthesizeKey with the console focused");
+ let consoleInput = gToolbox.getPanel("webconsole").hud.jsterm.inputNode;
+ consoleInput.focus();
+ synthesizeKeyShortcut("F3", panelWin);
+
+ ok(commandCalled, "Shortcut key should trigger the command");
+}
+
+// A key bound to a *different* tool should not trigger it's command
+function* testUseKeyWithSplitConsoleWrongTool() {
+ let commandCalled = false;
+
+ info("useKeyWithSplitConsole on inspector while debugger is focused");
+ gToolbox.useKeyWithSplitConsole("F4", () => {
+ commandCalled = true;
+ }, "inspector");
+
+ info("synthesizeKey with the console focused");
+ let consoleInput = gToolbox.getPanel("webconsole").hud.jsterm.inputNode;
+ consoleInput.focus();
+ synthesizeKeyShortcut("F4", panelWin);
+
+ ok(!commandCalled, "Shortcut key shouldn't trigger the command");
+}
+
+function* cleanup() {
+ // We don't want the open split console to confuse other tests..
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+ yield gToolbox.destroy();
+ gBrowser.removeCurrentTab();
+ gToolbox = panelWin = null;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
new file mode 100644
index 000000000..b9401f768
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
@@ -0,0 +1,68 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+add_task(function* () {
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+
+ let toolIDs = gDevTools.getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ let toolbox = yield gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM);
+ let nextShortcut = L10N.getStr("toolbox.nextTool.key");
+ let prevShortcut = L10N.getStr("toolbox.previousTool.key");
+
+ // Iterate over all tools, starting from options to netmonitor, in normal
+ // order.
+ for (let i = 1; i < toolIDs.length; i++) {
+ yield testShortcuts(toolbox, i, nextShortcut, toolIDs);
+ }
+
+ // Iterate again, in the same order, starting from netmonitor (so next one is
+ // 0: options).
+ for (let i = 0; i < toolIDs.length; i++) {
+ yield testShortcuts(toolbox, i, nextShortcut, toolIDs);
+ }
+
+ // Iterate over all tools in reverse order, starting from netmonitor to
+ // options.
+ for (let i = toolIDs.length - 2; i >= 0; i--) {
+ yield testShortcuts(toolbox, i, prevShortcut, toolIDs);
+ }
+
+ // Iterate again, in reverse order again, starting from options (so next one
+ // is length-1: netmonitor).
+ for (let i = toolIDs.length - 1; i >= 0; i--) {
+ yield testShortcuts(toolbox, i, prevShortcut, toolIDs);
+ }
+
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function* testShortcuts(toolbox, index, shortcut, toolIDs) {
+ info("Testing shortcut to switch to tool " + index + ":" + toolIDs[index] +
+ " using shortcut " + shortcut);
+
+ let onToolSelected = toolbox.once("select");
+ synthesizeKeyShortcut(shortcut);
+ let id = yield onToolSelected;
+
+ info("toolbox-select event from " + id);
+
+ is(toolIDs.indexOf(id), index,
+ "Correct tool is selected on pressing the shortcut for " + id);
+}
diff --git a/devtools/client/framework/test/browser_toolbox_target.js b/devtools/client/framework/test/browser_toolbox_target.js
new file mode 100644
index 000000000..68639c501
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_target.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test about:devtools-toolbox?target which allows opening a toolbox in an
+// iframe while defining which document to debug by setting a `target`
+// attribute refering to the document to debug.
+
+add_task(function *() {
+ // iframe loads the document to debug
+ let iframe = document.createElement("browser");
+ iframe.setAttribute("type", "content");
+ document.documentElement.appendChild(iframe);
+
+ let onLoad = once(iframe, "load", true);
+ iframe.setAttribute("src", "data:text/html,document to debug");
+ yield onLoad;
+ is(iframe.contentWindow.document.body.innerHTML, "document to debug");
+
+ // toolbox loads the toolbox document
+ let toolboxIframe = document.createElement("iframe");
+ document.documentElement.appendChild(toolboxIframe);
+
+ // Important step to define which target to debug
+ toolboxIframe.target = iframe;
+
+ let onToolboxReady = gDevTools.once("toolbox-ready");
+
+ onLoad = once(toolboxIframe, "load", true);
+ toolboxIframe.setAttribute("src", "about:devtools-toolbox?target");
+ yield onLoad;
+
+ // Also wait for toolbox-ready, as toolbox document load isn't enough, there
+ // is plenty of asynchronous steps during toolbox load
+ info("Waiting for toolbox-ready");
+ let toolbox = yield onToolboxReady;
+
+ let onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
+ let onTabActorDetached = once(toolbox.target.client, "tabDetached");
+
+ info("Removing the iframes");
+ toolboxIframe.remove();
+
+ // And wait for toolbox-destroyed as toolbox unload is also full of
+ // asynchronous operation that outlast unload event
+ info("Waiting for toolbox-destroyed");
+ yield onToolboxDestroyed;
+ info("Toolbox destroyed");
+
+ // Also wait for tabDetached. Toolbox destroys the Target which calls
+ // TabActor.detach(). But Target doesn't wait for detach's end to resolve.
+ // Whereas it is quite important as it is a significant part of toolbox
+ // cleanup. If we do not wait for it and starts removing debugged document,
+ // the actor is still considered as being attached and continues processing
+ // events.
+ yield onTabActorDetached;
+
+ iframe.remove();
+});
diff --git a/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js
new file mode 100644
index 000000000..2e5f3210e
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "data:text/html;charset=utf8,test for textbox context menu";
+
+add_task(function* () {
+ let toolbox = yield openNewTabAndToolbox(URL, "inspector");
+ let textboxContextMenu = toolbox.textBoxContextMenuPopup;
+
+ emptyClipboard();
+
+ // Make sure the focus is predictable.
+ let inspector = toolbox.getPanel("inspector");
+ let onFocus = once(inspector.searchBox, "focus");
+ inspector.searchBox.focus();
+ yield onFocus;
+
+ ok(textboxContextMenu, "The textbox context menu is loaded in the toolbox");
+
+ let cmdUndo = textboxContextMenu.querySelector("[command=cmd_undo]");
+ let cmdDelete = textboxContextMenu.querySelector("[command=cmd_delete]");
+ let cmdSelectAll = textboxContextMenu.querySelector("[command=cmd_selectAll]");
+ let cmdCut = textboxContextMenu.querySelector("[command=cmd_cut]");
+ let cmdCopy = textboxContextMenu.querySelector("[command=cmd_copy]");
+ let cmdPaste = textboxContextMenu.querySelector("[command=cmd_paste]");
+
+ info("Opening context menu");
+
+ let onContextMenuPopup = once(textboxContextMenu, "popupshowing");
+ textboxContextMenu.openPopupAtScreen(0, 0, true);
+ yield onContextMenuPopup;
+
+ is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
+ is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
+ is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
+
+ // Cut/Copy items are enabled in context menu even if there
+ // is no selection. See also Bug 1303033
+ is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
+ is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
+
+ if (isWindows()) {
+ // emptyClipboard only works on Windows (666254), assert paste only for this OS.
+ is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
+ }
+
+ yield cleanup(toolbox);
+});
+
+function* cleanup(toolbox) {
+ yield toolbox.destroy();
+ gBrowser.removeCurrentTab();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_theme_registration.js b/devtools/client/framework/test/browser_toolbox_theme_registration.js
new file mode 100644
index 000000000..7794d457c
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_theme_registration.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from shared-head.js */
+"use strict";
+
+// Test for dynamically registering and unregistering themes
+const CHROME_URL = "chrome://mochitests/content/browser/devtools/client/framework/test/";
+
+var toolbox;
+
+add_task(function* themeRegistration() {
+ let tab = yield addTab("data:text/html,test");
+ let target = TargetFactory.forTab(tab);
+ toolbox = yield gDevTools.showToolbox(target, "options");
+
+ let themeId = yield new Promise(resolve => {
+ gDevTools.once("theme-registered", (e, registeredThemeId) => {
+ resolve(registeredThemeId);
+ });
+
+ gDevTools.registerTheme({
+ id: "test-theme",
+ label: "Test theme",
+ stylesheets: [CHROME_URL + "doc_theme.css"],
+ classList: ["theme-test"],
+ });
+ });
+
+ is(themeId, "test-theme", "theme-registered event handler sent theme id");
+
+ ok(gDevTools.getThemeDefinitionMap().has(themeId), "theme added to map");
+});
+
+add_task(function* themeInOptionsPanel() {
+ let panelWin = toolbox.getCurrentPanel().panelWin;
+ let doc = panelWin.frameElement.contentDocument;
+ let themeBox = doc.getElementById("devtools-theme-box");
+ let testThemeOption = themeBox.querySelector(
+ "input[type=radio][value=test-theme]");
+
+ ok(testThemeOption, "new theme exists in the Options panel");
+
+ let lightThemeOption = themeBox.querySelector(
+ "input[type=radio][value=light]");
+
+ let color = panelWin.getComputedStyle(themeBox).color;
+ isnot(color, "rgb(255, 0, 0)", "style unapplied");
+
+ let onThemeSwithComplete = once(panelWin, "theme-switch-complete");
+
+ // Select test theme.
+ testThemeOption.click();
+
+ info("Waiting for theme to finish loading");
+ yield onThemeSwithComplete;
+
+ color = panelWin.getComputedStyle(themeBox).color;
+ is(color, "rgb(255, 0, 0)", "style applied");
+
+ onThemeSwithComplete = once(panelWin, "theme-switch-complete");
+
+ // Select light theme
+ lightThemeOption.click();
+
+ info("Waiting for theme to finish loading");
+ yield onThemeSwithComplete;
+
+ color = panelWin.getComputedStyle(themeBox).color;
+ isnot(color, "rgb(255, 0, 0)", "style unapplied");
+
+ onThemeSwithComplete = once(panelWin, "theme-switch-complete");
+ // Select test theme again.
+ testThemeOption.click();
+ yield onThemeSwithComplete;
+});
+
+add_task(function* themeUnregistration() {
+ let panelWin = toolbox.getCurrentPanel().panelWin;
+ let onUnRegisteredTheme = once(gDevTools, "theme-unregistered");
+ let onThemeSwitchComplete = once(panelWin, "theme-switch-complete");
+ gDevTools.unregisterTheme("test-theme");
+ yield onUnRegisteredTheme;
+ yield onThemeSwitchComplete;
+
+ ok(!gDevTools.getThemeDefinitionMap().has("test-theme"),
+ "theme removed from map");
+
+ let doc = panelWin.frameElement.contentDocument;
+ let themeBox = doc.getElementById("devtools-theme-box");
+
+ // The default light theme must be selected now.
+ is(themeBox.querySelector("#devtools-theme-box [value=light]").checked, true,
+ "light theme must be selected");
+});
+
+add_task(function* cleanup() {
+ yield toolbox.destroy();
+ toolbox = null;
+});
diff --git a/devtools/client/framework/test/browser_toolbox_toggle.js b/devtools/client/framework/test/browser_toolbox_toggle.js
new file mode 100644
index 000000000..d5b6d0e96
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toggle.js
@@ -0,0 +1,108 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the toolbox with ACCEL+SHIFT+I / ACCEL+ALT+I and F12 in docked
+// and detached (window) modes.
+
+const URL = "data:text/html;charset=utf-8,Toggling devtools using shortcuts";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+add_task(function* () {
+ // Make sure this test starts with the selectedTool pref cleared. Previous
+ // tests select various tools, and that sets this pref.
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+
+ // Test with ACCEL+SHIFT+I / ACCEL+ALT+I (MacOSX) ; modifiers should match :
+ // - toolbox-key-toggle in devtools/client/framework/toolbox-window.xul
+ // - key_devToolboxMenuItem in browser/base/content/browser.xul
+ info("Test toggle using CTRL+SHIFT+I/CMD+ALT+I");
+ yield testToggle("I", {
+ accelKey: true,
+ shiftKey: !navigator.userAgent.match(/Mac/),
+ altKey: navigator.userAgent.match(/Mac/)
+ });
+
+ // Test with F12 ; no modifiers
+ info("Test toggle using F12");
+ yield testToggle("VK_F12", {});
+});
+
+function* testToggle(key, modifiers) {
+ let tab = yield addTab(URL + " ; key : '" + key + "'");
+ yield gDevTools.showToolbox(TargetFactory.forTab(tab));
+
+ yield testToggleDockedToolbox(tab, key, modifiers);
+ yield testToggleDetachedToolbox(tab, key, modifiers);
+
+ yield cleanup();
+}
+
+function* testToggleDockedToolbox(tab, key, modifiers) {
+ let toolbox = getToolboxForTab(tab);
+
+ isnot(toolbox.hostType, Toolbox.HostType.WINDOW,
+ "Toolbox is docked in the main window");
+
+ info("verify docked toolbox is destroyed when using toggle key");
+ let onToolboxDestroyed = once(gDevTools, "toolbox-destroyed");
+ EventUtils.synthesizeKey(key, modifiers);
+ yield onToolboxDestroyed;
+ ok(true, "Docked toolbox is destroyed when using a toggle key");
+
+ info("verify new toolbox is created when using toggle key");
+ let onToolboxReady = once(gDevTools, "toolbox-ready");
+ EventUtils.synthesizeKey(key, modifiers);
+ yield onToolboxReady;
+ ok(true, "Toolbox is created by using when toggle key");
+}
+
+function* testToggleDetachedToolbox(tab, key, modifiers) {
+ let toolbox = getToolboxForTab(tab);
+
+ info("change the toolbox hostType to WINDOW");
+
+ yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+ is(toolbox.hostType, Toolbox.HostType.WINDOW,
+ "Toolbox opened on separate window");
+
+ info("Wait for focus on the toolbox window");
+ yield new Promise(res => waitForFocus(res, toolbox.win));
+
+ info("Focus main window to put the toolbox window in the background");
+
+ let onMainWindowFocus = once(window, "focus");
+ window.focus();
+ yield onMainWindowFocus;
+ ok(true, "Main window focused");
+
+ info("Verify windowed toolbox is focused instead of closed when using " +
+ "toggle key from the main window");
+ let toolboxWindow = toolbox.win.top;
+ let onToolboxWindowFocus = once(toolboxWindow, "focus", true);
+ EventUtils.synthesizeKey(key, modifiers);
+ yield onToolboxWindowFocus;
+ ok(true, "Toolbox focused and not destroyed");
+
+ info("Verify windowed toolbox is destroyed when using toggle key from its " +
+ "own window");
+
+ let onToolboxDestroyed = once(gDevTools, "toolbox-destroyed");
+ EventUtils.synthesizeKey(key, modifiers, toolboxWindow);
+ yield onToolboxDestroyed;
+ ok(true, "Toolbox destroyed");
+}
+
+function getToolboxForTab(tab) {
+ return gDevTools.getToolbox(TargetFactory.forTab(tab));
+}
+
+function* cleanup() {
+ Services.prefs.setCharPref("devtools.toolbox.host",
+ Toolbox.HostType.BOTTOM);
+ gBrowser.removeCurrentTab();
+}
diff --git a/devtools/client/framework/test/browser_toolbox_tool_ready.js b/devtools/client/framework/test/browser_toolbox_tool_ready.js
new file mode 100644
index 000000000..7d430e7c5
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tool_ready.js
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(5);
+
+/**
+ * Whitelisting this test.
+ * As part of bug 1077403, the leaking uncaught rejection should be fixed.
+ */
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is " +
+ "still waiting for a WebGL context to be created.");
+
+function performChecks(target) {
+ return Task.spawn(function* () {
+ let toolIds = gDevTools.getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ let toolbox;
+ for (let index = 0; index < toolIds.length; index++) {
+ let toolId = toolIds[index];
+
+ info("About to open " + index + "/" + toolId);
+ toolbox = yield gDevTools.showToolbox(target, toolId);
+ ok(toolbox, "toolbox exists for " + toolId);
+ is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId);
+
+ let panel = toolbox.getCurrentPanel();
+ ok(panel.isReady, toolId + " panel should be ready");
+ }
+
+ yield toolbox.destroy();
+ });
+}
+
+function test() {
+ Task.spawn(function* () {
+ toggleAllTools(true);
+ let tab = yield addTab("about:blank");
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+ yield performChecks(target);
+ gBrowser.removeCurrentTab();
+ toggleAllTools(false);
+ finish();
+ }, console.error);
+}
diff --git a/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js b/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js
new file mode 100644
index 000000000..03461e953
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js
@@ -0,0 +1,135 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Whitelisting this test.
+ * As part of bug 1077403, the leaking uncaught rejection should be fixed.
+ */
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is " +
+ "still waiting for a WebGL context to be created.");
+
+const { DebuggerServer } = require("devtools/server/main");
+const { DebuggerClient } = require("devtools/shared/client/main");
+
+// Bug 1277805: Too slow for debug runs
+requestLongerTimeout(2);
+
+/**
+ * Bug 979536: Ensure fronts are destroyed after toolbox close.
+ *
+ * The fronts need to be destroyed manually to unbind their onPacket handlers.
+ *
+ * When you initialize a front and call |this.manage|, it adds a client actor
+ * pool that the DebuggerClient uses to route packet replies to that actor.
+ *
+ * Most (all?) tools create a new front when they are opened. When the destroy
+ * step is skipped and the tool is reopened, a second front is created and also
+ * added to the client actor pool. When a packet reply is received, is ends up
+ * being routed to the first (now unwanted) front that is still in the client
+ * actor pool. Since this is not the same front that was used to make the
+ * request, an error occurs.
+ *
+ * This problem does not occur with the toolbox for a local tab because the
+ * toolbox target creates its own DebuggerClient for the local tab, and the
+ * client is destroyed when the toolbox is closed, which removes the client
+ * actor pools, and avoids this issue.
+ *
+ * In WebIDE, we do not destroy the DebuggerClient on toolbox close because it
+ * is still used for other purposes like managing apps, etc. that aren't part of
+ * a toolbox. Thus, the same client gets reused across multiple toolboxes,
+ * which leads to the tools failing if they don't destroy their fronts.
+ */
+
+function runTools(target) {
+ return Task.spawn(function* () {
+ let toolIds = gDevTools.getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+
+ let toolbox;
+ for (let index = 0; index < toolIds.length; index++) {
+ let toolId = toolIds[index];
+
+ info("About to open " + index + "/" + toolId);
+ toolbox = yield gDevTools.showToolbox(target, toolId, "window");
+ ok(toolbox, "toolbox exists for " + toolId);
+ is(toolbox.currentToolId, toolId, "currentToolId should be " + toolId);
+
+ let panel = toolbox.getCurrentPanel();
+ ok(panel.isReady, toolId + " panel should be ready");
+ }
+
+ yield toolbox.destroy();
+ });
+}
+
+function getClient() {
+ let deferred = defer();
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ let transport = DebuggerServer.connectPipe();
+ let client = new DebuggerClient(transport);
+
+ return client.connect().then(() => client);
+}
+
+function getTarget(client) {
+ let deferred = defer();
+
+ client.listTabs(tabList => {
+ let target = TargetFactory.forRemoteTab({
+ client: client,
+ form: tabList.tabs[tabList.selected],
+ chrome: false
+ });
+ deferred.resolve(target);
+ });
+
+ return deferred.promise;
+}
+
+function test() {
+ Task.spawn(function* () {
+ toggleAllTools(true);
+ yield addTab("about:blank");
+
+ let client = yield getClient();
+ let target = yield getTarget(client);
+ yield runTools(target);
+
+ // Actor fronts should be destroyed now that the toolbox has closed, but
+ // look for any that remain.
+ for (let pool of client.__pools) {
+ if (!pool.__poolMap) {
+ continue;
+ }
+ for (let actor of pool.__poolMap.keys()) {
+ // Bug 1056342: Profiler fails today because of framerate actor, but
+ // this appears more complex to rework, so leave it for that bug to
+ // resolve.
+ if (actor.includes("framerateActor")) {
+ todo(false, "Front for " + actor + " still held in pool!");
+ continue;
+ }
+ // gcliActor is for the commandline which is separate to the toolbox
+ if (actor.includes("gcliActor")) {
+ continue;
+ }
+ ok(false, "Front for " + actor + " still held in pool!");
+ }
+ }
+
+ gBrowser.removeCurrentTab();
+ DebuggerServer.destroy();
+ toggleAllTools(false);
+ finish();
+ }, console.error);
+}
diff --git a/devtools/client/framework/test/browser_toolbox_transport_events.js b/devtools/client/framework/test/browser_toolbox_transport_events.js
new file mode 100644
index 000000000..1e2b67ac4
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_transport_events.js
@@ -0,0 +1,108 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { on, off } = require("sdk/event/core");
+const { DebuggerClient } = require("devtools/shared/client/main");
+
+function test() {
+ gDevTools.on("toolbox-created", onToolboxCreated);
+ on(DebuggerClient, "connect", onDebuggerClientConnect);
+
+ addTab("about:blank").then(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, "webconsole").then(testResults);
+ });
+}
+
+function testResults(toolbox) {
+ testPackets(sent1, received1);
+ testPackets(sent2, received2);
+
+ cleanUp(toolbox);
+}
+
+function cleanUp(toolbox) {
+ gDevTools.off("toolbox-created", onToolboxCreated);
+ off(DebuggerClient, "connect", onDebuggerClientConnect);
+
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+ executeSoon(function () {
+ finish();
+ });
+ });
+}
+
+function testPackets(sent, received) {
+ ok(sent.length > 0, "There must be at least one sent packet");
+ ok(received.length > 0, "There must be at leaset one received packet");
+
+ if (!sent.length || received.length) {
+ return;
+ }
+
+ let sentPacket = sent[0];
+ let receivedPacket = received[0];
+
+ is(receivedPacket.from, "root",
+ "The first received packet is from the root");
+ is(receivedPacket.applicationType, "browser",
+ "The first received packet has browser type");
+ is(sentPacket.type, "listTabs",
+ "The first sent packet is for list of tabs");
+}
+
+// Listen to the transport object that is associated with the
+// default Toolbox debugger client
+var sent1 = [];
+var received1 = [];
+
+function send1(eventId, packet) {
+ sent1.push(packet);
+}
+
+function onPacket1(eventId, packet) {
+ received1.push(packet);
+}
+
+function onToolboxCreated(eventId, toolbox) {
+ toolbox.target.makeRemote();
+ let client = toolbox.target.client;
+ let transport = client._transport;
+
+ transport.on("send", send1);
+ transport.on("packet", onPacket1);
+
+ client.addOneTimeListener("closed", event => {
+ transport.off("send", send1);
+ transport.off("packet", onPacket1);
+ });
+}
+
+// Listen to all debugger client object protocols.
+var sent2 = [];
+var received2 = [];
+
+function send2(eventId, packet) {
+ sent2.push(packet);
+}
+
+function onPacket2(eventId, packet) {
+ received2.push(packet);
+}
+
+function onDebuggerClientConnect(client) {
+ let transport = client._transport;
+
+ transport.on("send", send2);
+ transport.on("packet", onPacket2);
+
+ client.addOneTimeListener("closed", event => {
+ transport.off("send", send2);
+ transport.off("packet", onPacket2);
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_view_source_01.js b/devtools/client/framework/test/browser_toolbox_view_source_01.js
new file mode 100644
index 000000000..5a9a6d9b0
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_view_source_01.js
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Toolbox#viewSourceInDebugger works when debugger is not
+ * yet opened.
+ */
+
+var URL = `${URL_ROOT}doc_viewsource.html`;
+var JS_URL = `${URL_ROOT}code_math.js`;
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function* viewSource() {
+ let toolbox = yield openNewTabAndToolbox(URL);
+
+ yield toolbox.viewSourceInDebugger(JS_URL, 2);
+
+ let debuggerPanel = toolbox.getPanel("jsdebugger");
+ ok(debuggerPanel, "The debugger panel was opened.");
+ is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
+
+ let { DebuggerView } = debuggerPanel.panelWin;
+ let Sources = DebuggerView.Sources;
+
+ is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+ "The correct source is shown in the debugger.");
+ is(DebuggerView.editor.getCursor().line + 1, 2,
+ "The correct line is highlighted in the debugger's source editor.");
+
+ yield closeToolboxAndTab(toolbox);
+ finish();
+}
+
+function test() {
+ Task.spawn(viewSource).then(finish, (aError) => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_view_source_02.js b/devtools/client/framework/test/browser_toolbox_view_source_02.js
new file mode 100644
index 000000000..c18e885cf
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_view_source_02.js
@@ -0,0 +1,54 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Toolbox#viewSourceInDebugger works when debugger is already loaded.
+ */
+
+var URL = `${URL_ROOT}doc_viewsource.html`;
+var JS_URL = `${URL_ROOT}code_math.js`;
+
+// Force the old debugger UI since it's directly used (see Bug 1301705)
+Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+registerCleanupFunction(function* () {
+ Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
+});
+
+function* viewSource() {
+ let toolbox = yield openNewTabAndToolbox(URL);
+ let { panelWin: debuggerWin } = yield toolbox.selectTool("jsdebugger");
+ let debuggerEvents = debuggerWin.EVENTS;
+ let { DebuggerView } = debuggerWin;
+ let Sources = DebuggerView.Sources;
+
+ yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
+ ok("A source was shown in the debugger.");
+
+ is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+ "The correct source is initially shown in the debugger.");
+ is(DebuggerView.editor.getCursor().line, 0,
+ "The correct line is initially highlighted in the debugger's source editor.");
+
+ yield toolbox.viewSourceInDebugger(JS_URL, 2);
+
+ let debuggerPanel = toolbox.getPanel("jsdebugger");
+ ok(debuggerPanel, "The debugger panel was opened.");
+ is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
+
+ is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+ "The correct source is shown in the debugger.");
+ is(DebuggerView.editor.getCursor().line + 1, 2,
+ "The correct line is highlighted in the debugger's source editor.");
+
+ yield closeToolboxAndTab(toolbox);
+ finish();
+}
+
+function test() {
+ Task.spawn(viewSource).then(finish, (aError) => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_view_source_03.js b/devtools/client/framework/test/browser_toolbox_view_source_03.js
new file mode 100644
index 000000000..2d2cda76f
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_view_source_03.js
@@ -0,0 +1,40 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Toolbox#viewSourceInStyleEditor works when style editor is not
+ * yet opened.
+ */
+
+var URL = `${URL_ROOT}doc_viewsource.html`;
+var CSS_URL = `${URL_ROOT}doc_theme.css`;
+
+function* viewSource() {
+ let toolbox = yield openNewTabAndToolbox(URL);
+
+ let fileFound = yield toolbox.viewSourceInStyleEditor(CSS_URL, 2);
+ ok(fileFound, "viewSourceInStyleEditor should resolve to true if source found.");
+
+ let stylePanel = toolbox.getPanel("styleeditor");
+ ok(stylePanel, "The style editor panel was opened.");
+ is(toolbox.currentToolId, "styleeditor", "The style editor panel was selected.");
+
+ let { UI } = stylePanel;
+
+ is(UI.selectedEditor.styleSheet.href, CSS_URL,
+ "The correct source is shown in the style editor.");
+ is(UI.selectedEditor.sourceEditor.getCursor().line + 1, 2,
+ "The correct line is highlighted in the style editor's source editor.");
+
+ yield closeToolboxAndTab(toolbox);
+ finish();
+}
+
+function test() {
+ Task.spawn(viewSource).then(finish, (aError) => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_view_source_04.js b/devtools/client/framework/test/browser_toolbox_view_source_04.js
new file mode 100644
index 000000000..47d86fc11
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_view_source_04.js
@@ -0,0 +1,39 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Toolbox#viewSourceInScratchpad works.
+ */
+
+var URL = `${URL_ROOT}doc_viewsource.html`;
+
+function* viewSource() {
+ let toolbox = yield openNewTabAndToolbox(URL);
+ let win = yield openScratchpadWindow();
+ let { Scratchpad: scratchpad } = win;
+
+ // Brahm's Cello Sonata No.1, Op.38 now in the scratchpad
+ scratchpad.setText("E G B C B\nA B A G A B\nG E");
+ let scratchpadURL = scratchpad.uniqueName;
+
+ // Now select another tool for focus
+ yield toolbox.selectTool("webconsole");
+
+ yield toolbox.viewSourceInScratchpad(scratchpadURL, 2);
+
+ is(scratchpad.editor.getCursor().line, 2,
+ "The correct line is highlighted in scratchpad's editor.");
+
+ win.close();
+ yield closeToolboxAndTab(toolbox);
+ finish();
+}
+
+function test() {
+ Task.spawn(viewSource).then(finish, (aError) => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_reload_target.js b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
new file mode 100644
index 000000000..9f3339728
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(10);
+
+const TEST_URL = "data:text/html;charset=utf-8," +
+ "<html><head><title>Test reload</title></head>" +
+ "<body><h1>Testing reload from devtools</h1></body></html>";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+var target, toolbox, description, reloadsSent, toolIDs;
+
+function test() {
+ addTab(TEST_URL).then(() => {
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ target.makeRemote().then(() => {
+ toolIDs = gDevTools.getToolDefinitionArray()
+ .filter(def => def.isTargetSupported(target))
+ .map(def => def.id);
+ gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM)
+ .then(startReloadTest);
+ });
+ });
+}
+
+function startReloadTest(aToolbox) {
+ getFrameScript(); // causes frame-script-utils to be loaded into the child.
+ toolbox = aToolbox;
+
+ reloadsSent = 0;
+ let reloads = 0;
+ let reloadCounter = (msg) => {
+ reloads++;
+ info("Detected reload #" + reloads);
+ is(reloads, reloadsSent, "Reloaded from devtools window once and only for " + description + "");
+ };
+ gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", reloadCounter);
+
+ testAllTheTools("docked", () => {
+ let origHostType = toolbox.hostType;
+ toolbox.switchHost(Toolbox.HostType.WINDOW).then(() => {
+ toolbox.win.focus();
+ testAllTheTools("undocked", () => {
+ toolbox.switchHost(origHostType).then(() => {
+ gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", reloadCounter);
+ // If we finish too early, the inspector breaks promises:
+ toolbox.getPanel("inspector").once("new-root", finishUp);
+ });
+ });
+ });
+ }, toolIDs.length - 1 /* only test 1 tool in docked mode, to cut down test time */);
+}
+
+function testAllTheTools(docked, callback, toolNum = 0) {
+ if (toolNum >= toolIDs.length) {
+ return callback();
+ }
+ toolbox.selectTool(toolIDs[toolNum]).then(() => {
+ testReload("toolbox.reload.key", docked, toolIDs[toolNum], () => {
+ testReload("toolbox.reload2.key", docked, toolIDs[toolNum], () => {
+ testReload("toolbox.forceReload.key", docked, toolIDs[toolNum], () => {
+ testReload("toolbox.forceReload2.key", docked, toolIDs[toolNum], () => {
+ testAllTheTools(docked, callback, toolNum + 1);
+ });
+ });
+ });
+ });
+ });
+}
+
+function testReload(shortcut, docked, toolID, callback) {
+ let complete = () => {
+ gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", complete);
+ return callback();
+ };
+ gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", complete);
+
+ description = docked + " devtools with tool " + toolID + ", shortcut #" + shortcut;
+ info("Testing reload in " + description);
+ toolbox.win.focus();
+ synthesizeKeyShortcut(L10N.getStr(shortcut), toolbox.win);
+ reloadsSent++;
+}
+
+function finishUp() {
+ toolbox.destroy().then(() => {
+ gBrowser.removeCurrentTab();
+
+ target = toolbox = description = reloadsSent = toolIDs = null;
+
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_shortcuts.js b/devtools/client/framework/test/browser_toolbox_window_shortcuts.js
new file mode 100644
index 000000000..dde06dfea
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_shortcuts.js
@@ -0,0 +1,84 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+var toolbox, toolIDs, idIndex, modifiedPrefs = [];
+
+function test() {
+ addTab("about:blank").then(function () {
+ toolIDs = [];
+ for (let [id, definition] of gDevTools._tools) {
+ if (definition.key) {
+ toolIDs.push(id);
+
+ // Enable disabled tools
+ let pref = definition.visibilityswitch, prefValue;
+ try {
+ prefValue = Services.prefs.getBoolPref(pref);
+ } catch (e) {
+ continue;
+ }
+ if (!prefValue) {
+ modifiedPrefs.push(pref);
+ Services.prefs.setBoolPref(pref, true);
+ }
+ }
+ }
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ idIndex = 0;
+ gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.WINDOW)
+ .then(testShortcuts);
+ });
+}
+
+function testShortcuts(aToolbox, aIndex) {
+ if (aIndex === undefined) {
+ aIndex = 1;
+ } else if (aIndex == toolIDs.length) {
+ tidyUp();
+ return;
+ }
+
+ toolbox = aToolbox;
+ info("Toolbox fired a `ready` event");
+
+ toolbox.once("select", selectCB);
+
+ let key = gDevTools._tools.get(toolIDs[aIndex]).key;
+ let toolModifiers = gDevTools._tools.get(toolIDs[aIndex]).modifiers;
+ let modifiers = {
+ accelKey: toolModifiers.includes("accel"),
+ altKey: toolModifiers.includes("alt"),
+ shiftKey: toolModifiers.includes("shift"),
+ };
+ idIndex = aIndex;
+ info("Testing shortcut for tool " + aIndex + ":" + toolIDs[aIndex] +
+ " using key " + key);
+ EventUtils.synthesizeKey(key, modifiers, toolbox.win.parent);
+}
+
+function selectCB(event, id) {
+ info("toolbox-select event from " + id);
+
+ is(toolIDs.indexOf(id), idIndex,
+ "Correct tool is selected on pressing the shortcut for " + id);
+
+ testShortcuts(toolbox, idIndex + 1);
+}
+
+function tidyUp() {
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+
+ for (let pref of modifiedPrefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+ toolbox = toolIDs = idIndex = modifiedPrefs = Toolbox = null;
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_title_changes.js b/devtools/client/framework/test/browser_toolbox_window_title_changes.js
new file mode 100644
index 000000000..558c2094f
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_changes.js
@@ -0,0 +1,108 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(5);
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+function test() {
+ const URL_1 = "data:text/plain;charset=UTF-8,abcde";
+ const URL_2 = "data:text/plain;charset=UTF-8,12345";
+ const URL_3 = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
+
+ const TOOL_ID_1 = "webconsole";
+ const TOOL_ID_2 = "jsdebugger";
+
+ const NAME_1 = "";
+ const NAME_2 = "";
+ const NAME_3 = "Toolbox test for title update";
+
+ let toolbox;
+
+ addTab(URL_1).then(function () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ gDevTools.showToolbox(target, null, Toolbox.HostType.BOTTOM)
+ .then(function (aToolbox) { toolbox = aToolbox; })
+ .then(() => toolbox.selectTool(TOOL_ID_1))
+
+ // undock toolbox and check title
+ .then(() => {
+ // We have to first switch the host in order to spawn the new top level window
+ // on which we are going to listen from title change event
+ return toolbox.switchHost(Toolbox.HostType.WINDOW)
+ .then(() => waitForTitleChange(toolbox));
+ })
+ .then(checkTitle.bind(null, NAME_1, URL_1, "toolbox undocked"))
+
+ // switch to different tool and check title
+ .then(() => {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ toolbox.selectTool(TOOL_ID_2);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_1, URL_1, "tool changed"))
+
+ // navigate to different local url and check title
+ .then(function () {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ gBrowser.loadURI(URL_2);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_2, URL_2, "url changed"))
+
+ // navigate to a real url and check title
+ .then(() => {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ gBrowser.loadURI(URL_3);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_3, URL_3, "url changed"))
+
+ // destroy toolbox, create new one hosted in a window (with a
+ // different tool id), and check title
+ .then(function () {
+ // Give the tools a chance to handle the navigation event before
+ // destroying the toolbox.
+ executeSoon(function () {
+ toolbox.destroy()
+ .then(function () {
+ // After destroying the toolbox, a fresh target is required.
+ target = TargetFactory.forTab(gBrowser.selectedTab);
+ return gDevTools.showToolbox(target, null, Toolbox.HostType.WINDOW);
+ })
+ .then(function (aToolbox) { toolbox = aToolbox; })
+ .then(() => {
+ let onTitleChanged = waitForTitleChange(toolbox);
+ toolbox.selectTool(TOOL_ID_1);
+ return onTitleChanged;
+ })
+ .then(checkTitle.bind(null, NAME_3, URL_3,
+ "toolbox destroyed and recreated"))
+
+ // clean up
+ .then(() => toolbox.destroy())
+ .then(function () {
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+ Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
+ finish();
+ });
+ });
+ });
+ });
+}
+
+function checkTitle(name, url, context) {
+ let win = Services.wm.getMostRecentWindow("devtools:toolbox");
+ let expectedTitle;
+ if (name) {
+ expectedTitle = `Developer Tools - ${name} - ${url}`;
+ } else {
+ expectedTitle = `Developer Tools - ${url}`;
+ }
+ is(win.document.title, expectedTitle, context);
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html b/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html
new file mode 100644
index 000000000..8678469ee
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_changes_page.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Toolbox test for title update</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
new file mode 100644
index 000000000..1e3d66646
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
@@ -0,0 +1,94 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from shared-head.js */
+
+"use strict";
+
+/**
+ * Check that the detached devtools window title is not updated when switching
+ * the selected frame. Also check that frames command button has 'open'
+ * attribute set when the list of frames is opened.
+ */
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+const URL = URL_ROOT + "browser_toolbox_window_title_frame_select_page.html";
+const IFRAME_URL = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
+
+add_task(function* () {
+ Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
+
+ yield addTab(URL);
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ let toolbox = yield gDevTools.showToolbox(target, null,
+ Toolbox.HostType.BOTTOM);
+
+ let onTitleChanged = waitForTitleChange(toolbox);
+ yield toolbox.selectTool("inspector");
+ yield onTitleChanged;
+
+ yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+ // Wait for title change event *after* switch host, in order to listen
+ // for the event on the WINDOW host window, which only exists after switchHost
+ yield waitForTitleChange(toolbox);
+
+ is(getTitle(), `Developer Tools - Page title - ${URL}`,
+ "Devtools title correct after switching to detached window host");
+
+ // Wait for tick to avoid unexpected 'popuphidden' event, which
+ // blocks the frame popup menu opened below. See also bug 1276873
+ yield waitForTick();
+
+ // Open frame menu and wait till it's available on the screen.
+ // Also check 'open' attribute on the command button.
+ let btn = toolbox.doc.getElementById("command-button-frames");
+ ok(!btn.getAttribute("open"), "The open attribute must not be present");
+ let menu = toolbox.showFramesMenu({target: btn});
+ yield once(menu, "open");
+
+ is(btn.getAttribute("open"), "true", "The open attribute must be set");
+
+ // Verify that the frame list menu is populated
+ let frames = menu.items;
+ is(frames.length, 2, "We have both frames in the list");
+
+ let topFrameBtn = frames.filter(b => b.label == URL)[0];
+ let iframeBtn = frames.filter(b => b.label == IFRAME_URL)[0];
+ ok(topFrameBtn, "Got top level document in the list");
+ ok(iframeBtn, "Got iframe document in the list");
+
+ // Listen to will-navigate to check if the view is empty
+ let willNavigate = toolbox.target.once("will-navigate");
+
+ onTitleChanged = waitForTitleChange(toolbox);
+
+ // Only select the iframe after we are able to select an element from the top
+ // level document.
+ let newRoot = toolbox.getPanel("inspector").once("new-root");
+ info("Select the iframe");
+ iframeBtn.click();
+
+ yield willNavigate;
+ yield newRoot;
+ yield onTitleChanged;
+
+ info("Navigation to the iframe is done, the inspector should be back up");
+ is(getTitle(), `Developer Tools - Page title - ${URL}`,
+ "Devtools title was not updated after changing inspected frame");
+
+ info("Cleanup toolbox and test preferences.");
+ yield toolbox.destroy();
+ toolbox = null;
+ gBrowser.removeCurrentTab();
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+ Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
+ Services.prefs.clearUserPref("devtools.command-button-frames.enabled");
+ finish();
+});
+
+function getTitle() {
+ return Services.wm.getMostRecentWindow("devtools:toolbox").document.title;
+}
diff --git a/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html b/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html
new file mode 100644
index 000000000..1eda94a9c
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Page title</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <iframe src="browser_toolbox_window_title_changes_page.html"></iframe>
+ </head>
+ <body></body>
+</html>
diff --git a/devtools/client/framework/test/browser_toolbox_zoom.js b/devtools/client/framework/test/browser_toolbox_zoom.js
new file mode 100644
index 000000000..d078b4bc2
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_zoom.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var toolbox;
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+function test() {
+ addTab("about:blank").then(openToolbox);
+}
+
+function openToolbox() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target).then((aToolbox) => {
+ toolbox = aToolbox;
+ toolbox.selectTool("styleeditor").then(testZoom);
+ });
+}
+
+function testZoom() {
+ info("testing zoom keys");
+
+ testZoomLevel("In", 2, 1.2);
+ testZoomLevel("Out", 3, 0.9);
+ testZoomLevel("Reset", 1, 1);
+
+ tidyUp();
+}
+
+function testZoomLevel(type, times, expected) {
+ sendZoomKey("toolbox.zoom" + type + ".key", times);
+
+ let zoom = getCurrentZoom(toolbox);
+ is(zoom.toFixed(2), expected, "zoom level correct after zoom " + type);
+
+ let savedZoom = parseFloat(Services.prefs.getCharPref(
+ "devtools.toolbox.zoomValue"));
+ is(savedZoom.toFixed(2), expected,
+ "saved zoom level is correct after zoom " + type);
+}
+
+function sendZoomKey(shortcut, times) {
+ for (let i = 0; i < times; i++) {
+ synthesizeKeyShortcut(L10N.getStr(shortcut));
+ }
+}
+
+function getCurrentZoom() {
+ let windowUtils = toolbox.win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ return windowUtils.fullZoom;
+}
+
+function tidyUp() {
+ toolbox.destroy().then(function () {
+ gBrowser.removeCurrentTab();
+
+ toolbox = null;
+ finish();
+ });
+}
diff --git a/devtools/client/framework/test/browser_two_tabs.js b/devtools/client/framework/test/browser_two_tabs.js
new file mode 100644
index 000000000..08d5f2391
--- /dev/null
+++ b/devtools/client/framework/test/browser_two_tabs.js
@@ -0,0 +1,149 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check regression when opening two tabs
+ */
+
+var { DebuggerServer } = require("devtools/server/main");
+var { DebuggerClient } = require("devtools/shared/client/main");
+
+const TAB_URL_1 = "data:text/html;charset=utf-8,foo";
+const TAB_URL_2 = "data:text/html;charset=utf-8,bar";
+
+var gClient;
+var gTab1, gTab2;
+var gTabActor1, gTabActor2;
+
+function test() {
+ waitForExplicitFinish();
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+
+ openTabs();
+}
+
+function openTabs() {
+ // Open two tabs, select the second
+ addTab(TAB_URL_1).then(tab1 => {
+ gTab1 = tab1;
+ addTab(TAB_URL_2).then(tab2 => {
+ gTab2 = tab2;
+
+ connect();
+ });
+ });
+}
+
+function connect() {
+ // Connect to debugger server to fetch the two tab actors
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect()
+ .then(() => gClient.listTabs())
+ .then(response => {
+ // Fetch the tab actors for each tab
+ gTabActor1 = response.tabs.filter(a => a.url === TAB_URL_1)[0];
+ gTabActor2 = response.tabs.filter(a => a.url === TAB_URL_2)[0];
+
+ checkGetTab();
+ });
+}
+
+function checkGetTab() {
+ gClient.getTab({tab: gTab1})
+ .then(response => {
+ is(JSON.stringify(gTabActor1), JSON.stringify(response.tab),
+ "getTab returns the same tab grip for first tab");
+ })
+ .then(() => {
+ let filter = {};
+ // Filter either by tabId or outerWindowID,
+ // if we are running tests OOP or not.
+ if (gTab1.linkedBrowser.frameLoader.tabParent) {
+ filter.tabId = gTab1.linkedBrowser.frameLoader.tabParent.tabId;
+ } else {
+ let windowUtils = gTab1.linkedBrowser.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ filter.outerWindowID = windowUtils.outerWindowID;
+ }
+ return gClient.getTab(filter);
+ })
+ .then(response => {
+ is(JSON.stringify(gTabActor1), JSON.stringify(response.tab),
+ "getTab returns the same tab grip when filtering by tabId/outerWindowID");
+ })
+ .then(() => gClient.getTab({tab: gTab2}))
+ .then(response => {
+ is(JSON.stringify(gTabActor2), JSON.stringify(response.tab),
+ "getTab returns the same tab grip for second tab");
+ })
+ .then(checkGetTabFailures);
+}
+
+function checkGetTabFailures() {
+ gClient.getTab({ tabId: -999 })
+ .then(
+ response => ok(false, "getTab unexpectedly succeed with a wrong tabId"),
+ response => {
+ is(response.error, "noTab");
+ is(response.message, "Unable to find tab with tabId '-999'");
+ }
+ )
+ .then(() => gClient.getTab({ outerWindowID: -999 }))
+ .then(
+ response => ok(false, "getTab unexpectedly succeed with a wrong outerWindowID"),
+ response => {
+ is(response.error, "noTab");
+ is(response.message, "Unable to find tab with outerWindowID '-999'");
+ }
+ )
+ .then(checkSelectedTabActor);
+
+}
+
+function checkSelectedTabActor() {
+ // Send a naive request to the second tab actor
+ // to check if it works
+ gClient.request({ to: gTabActor2.consoleActor, type: "startListeners", listeners: [] }, aResponse => {
+ ok("startedListeners" in aResponse, "Actor from the selected tab should respond to the request.");
+
+ closeSecondTab();
+ });
+}
+
+function closeSecondTab() {
+ // Close the second tab, currently selected
+ let container = gBrowser.tabContainer;
+ container.addEventListener("TabClose", function onTabClose() {
+ container.removeEventListener("TabClose", onTabClose);
+
+ checkFirstTabActor();
+ });
+ gBrowser.removeTab(gTab2);
+}
+
+function checkFirstTabActor() {
+ // then send a request to the first tab actor
+ // to check if it still works
+ gClient.request({ to: gTabActor1.consoleActor, type: "startListeners", listeners: [] }, aResponse => {
+ ok("startedListeners" in aResponse, "Actor from the first tab should still respond.");
+
+ cleanup();
+ });
+}
+
+function cleanup() {
+ let container = gBrowser.tabContainer;
+ container.addEventListener("TabClose", function onTabClose() {
+ container.removeEventListener("TabClose", onTabClose);
+
+ gClient.close().then(finish);
+ });
+ gBrowser.removeTab(gTab1);
+}
diff --git a/devtools/client/framework/test/code_binary_search.coffee b/devtools/client/framework/test/code_binary_search.coffee
new file mode 100644
index 000000000..e3dacdaaa
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.coffee
@@ -0,0 +1,18 @@
+# Uses a binary search algorithm to locate a value in the specified array.
+window.binary_search = (items, value) ->
+
+ start = 0
+ stop = items.length - 1
+ pivot = Math.floor (start + stop) / 2
+
+ while items[pivot] isnt value and start < stop
+
+ # Adjust the search area.
+ stop = pivot - 1 if value < items[pivot]
+ start = pivot + 1 if value > items[pivot]
+
+ # Recalculate the pivot.
+ pivot = Math.floor (stop + start) / 2
+
+ # Make sure we've found the correct value.
+ if items[pivot] is value then pivot else -1 \ No newline at end of file
diff --git a/devtools/client/framework/test/code_binary_search.js b/devtools/client/framework/test/code_binary_search.js
new file mode 100644
index 000000000..c43848a60
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.js
@@ -0,0 +1,29 @@
+// Generated by CoffeeScript 1.6.1
+(function() {
+
+ window.binary_search = function(items, value) {
+ var pivot, start, stop;
+ start = 0;
+ stop = items.length - 1;
+ pivot = Math.floor((start + stop) / 2);
+ while (items[pivot] !== value && start < stop) {
+ if (value < items[pivot]) {
+ stop = pivot - 1;
+ }
+ if (value > items[pivot]) {
+ start = pivot + 1;
+ }
+ pivot = Math.floor((stop + start) / 2);
+ }
+ if (items[pivot] === value) {
+ return pivot;
+ } else {
+ return -1;
+ }
+ };
+
+}).call(this);
+
+/*
+//# sourceMappingURL=code_binary_search.map
+*/
diff --git a/devtools/client/framework/test/code_binary_search.map b/devtools/client/framework/test/code_binary_search.map
new file mode 100644
index 000000000..8d2251125
--- /dev/null
+++ b/devtools/client/framework/test/code_binary_search.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "code_binary_search.js",
+ "sourceRoot": "",
+ "sources": [
+ "code_binary_search.coffee"
+ ],
+ "names": [],
+ "mappings": ";AACA;CAAA;CAAA,CAAA,CAAuB,EAAA,CAAjB,GAAkB,IAAxB;CAEE,OAAA,UAAA;CAAA,EAAQ,CAAR,CAAA;CAAA,EACQ,CAAR,CAAa,CAAL;CADR,EAEQ,CAAR,CAAA;CAEA,EAA0C,CAAR,CAAtB,MAAN;CAGJ,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,CAAR,CAAQ,GAAR;QAAA;CACA,EAA6B,CAAR,CAAA,CAArB;CAAA,EAAQ,EAAR,GAAA;QADA;CAAA,EAIQ,CAAI,CAAZ,CAAA;CAXF,IAIA;CAUA,GAAA,CAAS;CAAT,YAA8B;MAA9B;AAA0C,CAAD,YAAA;MAhBpB;CAAvB,EAAuB;CAAvB"
+}
diff --git a/devtools/client/framework/test/code_math.js b/devtools/client/framework/test/code_math.js
new file mode 100644
index 000000000..9fe2a3541
--- /dev/null
+++ b/devtools/client/framework/test/code_math.js
@@ -0,0 +1,9 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function add(a, b, k) {
+ var result = a + b;
+ return k(result);
+}
diff --git a/devtools/client/framework/test/code_ugly.js b/devtools/client/framework/test/code_ugly.js
new file mode 100644
index 000000000..ccf8d5488
--- /dev/null
+++ b/devtools/client/framework/test/code_ugly.js
@@ -0,0 +1,3 @@
+function foo() { var a=1; var b=2; bar(a, b); }
+function bar(c, d) { return c - d; }
+foo();
diff --git a/devtools/client/framework/test/doc_empty-tab-01.html b/devtools/client/framework/test/doc_empty-tab-01.html
new file mode 100644
index 000000000..28398f776
--- /dev/null
+++ b/devtools/client/framework/test/doc_empty-tab-01.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Empty test page 1</title>
+ </head>
+
+ <body>
+ </body>
+
+</html>
diff --git a/devtools/client/framework/test/doc_theme.css b/devtools/client/framework/test/doc_theme.css
new file mode 100644
index 000000000..5ed6e866a
--- /dev/null
+++ b/devtools/client/framework/test/doc_theme.css
@@ -0,0 +1,3 @@
+.theme-test #devtools-theme-box {
+ color: red !important;
+}
diff --git a/devtools/client/framework/test/doc_viewsource.html b/devtools/client/framework/test/doc_viewsource.html
new file mode 100644
index 000000000..7094eb87e
--- /dev/null
+++ b/devtools/client/framework/test/doc_viewsource.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Toolbox test for View Source methods</title>
+ <!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <link charset="UTF-8" rel="stylesheet" href="doc_theme.css" />
+ <script src="code_math.js"></script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/devtools/client/framework/test/head.js b/devtools/client/framework/test/head.js
new file mode 100644
index 000000000..22433b237
--- /dev/null
+++ b/devtools/client/framework/test/head.js
@@ -0,0 +1,148 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from shared-head.js */
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
+
+function toggleAllTools(state) {
+ for (let [, tool] of gDevTools._tools) {
+ if (!tool.visibilityswitch) {
+ continue;
+ }
+ if (state) {
+ Services.prefs.setBoolPref(tool.visibilityswitch, true);
+ } else {
+ Services.prefs.clearUserPref(tool.visibilityswitch);
+ }
+ }
+}
+
+function getChromeActors(callback)
+{
+ let { DebuggerServer } = require("devtools/server/main");
+ let { DebuggerClient } = require("devtools/shared/client/main");
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+ client.connect()
+ .then(() => client.getProcess())
+ .then(response => {
+ callback(client, response.form);
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ DebuggerServer.destroy();
+ });
+}
+
+function getSourceActor(aSources, aURL) {
+ let item = aSources.getItemForAttachment(a => a.source.url === aURL);
+ return item && item.value;
+}
+
+/**
+ * Open a Scratchpad window.
+ *
+ * @return nsIDOMWindow
+ * The new window object that holds Scratchpad.
+ */
+function* openScratchpadWindow() {
+ let { promise: p, resolve } = defer();
+ let win = ScratchpadManager.openScratchpad();
+
+ yield once(win, "load");
+
+ win.Scratchpad.addObserver({
+ onReady: function () {
+ win.Scratchpad.removeObserver(this);
+ resolve(win);
+ }
+ });
+ return p;
+}
+
+/**
+ * Wait for a content -> chrome message on the message manager (the window
+ * messagemanager is used).
+ * @param {String} name The message name
+ * @return {Promise} A promise that resolves to the response data when the
+ * message has been received
+ */
+function waitForContentMessage(name) {
+ info("Expecting message " + name + " from content");
+
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ let def = defer();
+ mm.addMessageListener(name, function onMessage(msg) {
+ mm.removeMessageListener(name, onMessage);
+ def.resolve(msg.data);
+ });
+ return def.promise;
+}
+
+/**
+ * Send an async message to the frame script (chrome -> content) and wait for a
+ * response message with the same name (content -> chrome).
+ * @param {String} name The message name. Should be one of the messages defined
+ * in doc_frame_script.js
+ * @param {Object} data Optional data to send along
+ * @param {Object} objects Optional CPOW objects to send along
+ * @param {Boolean} expectResponse If set to false, don't wait for a response
+ * with the same name from the content script. Defaults to true.
+ * @return {Promise} Resolves to the response data if a response is expected,
+ * immediately resolves otherwise
+ */
+function executeInContent(name, data = {}, objects = {}, expectResponse = true) {
+ info("Sending message " + name + " to content");
+ let mm = gBrowser.selectedBrowser.messageManager;
+
+ mm.sendAsyncMessage(name, data, objects);
+ if (expectResponse) {
+ return waitForContentMessage(name);
+ } else {
+ return promise.resolve();
+ }
+}
+
+/**
+ * Synthesize a keypress from a <key> element, taking into account
+ * any modifiers.
+ * @param {Element} el the <key> element to synthesize
+ */
+function synthesizeKeyElement(el) {
+ let key = el.getAttribute("key") || el.getAttribute("keycode");
+ let mod = {};
+ el.getAttribute("modifiers").split(" ").forEach((m) => mod[m + "Key"] = true);
+ info(`Synthesizing: key=${key}, mod=${JSON.stringify(mod)}`);
+ EventUtils.synthesizeKey(key, mod, el.ownerDocument.defaultView);
+}
+
+/* Check the toolbox host type and prefs to make sure they match the
+ * expected values
+ * @param {Toolbox}
+ * @param {HostType} hostType
+ * One of {SIDE, BOTTOM, WINDOW} from Toolbox.HostType
+ * @param {HostType} Optional previousHostType
+ * The host that will be switched to when calling switchToPreviousHost
+ */
+function checkHostType(toolbox, hostType, previousHostType) {
+ is(toolbox.hostType, hostType, "host type is " + hostType);
+
+ let pref = Services.prefs.getCharPref("devtools.toolbox.host");
+ is(pref, hostType, "host pref is " + hostType);
+
+ if (previousHostType) {
+ is(Services.prefs.getCharPref("devtools.toolbox.previousHost"),
+ previousHostType, "The previous host is correct");
+ }
+}
diff --git a/devtools/client/framework/test/helper_disable_cache.js b/devtools/client/framework/test/helper_disable_cache.js
new file mode 100644
index 000000000..5e2feef8f
--- /dev/null
+++ b/devtools/client/framework/test/helper_disable_cache.js
@@ -0,0 +1,128 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Common code shared by browser_toolbox_options_disable_cache-*.js
+const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_cache.sjs";
+var tabs = [
+ {
+ title: "Tab 0",
+ desc: "Toggles cache on.",
+ startToolbox: true
+ },
+ {
+ title: "Tab 1",
+ desc: "Toolbox open before Tab 1 toggles cache.",
+ startToolbox: true
+ },
+ {
+ title: "Tab 2",
+ desc: "Opens toolbox after Tab 1 has toggled cache. Also closes and opens.",
+ startToolbox: false
+ },
+ {
+ title: "Tab 3",
+ desc: "No toolbox",
+ startToolbox: false
+ }];
+
+function* initTab(tabX, startToolbox) {
+ tabX.tab = yield addTab(TEST_URI);
+ tabX.target = TargetFactory.forTab(tabX.tab);
+
+ if (startToolbox) {
+ tabX.toolbox = yield gDevTools.showToolbox(tabX.target, "options");
+ }
+}
+
+function* checkCacheStateForAllTabs(states) {
+ for (let i = 0; i < tabs.length; i++) {
+ let tab = tabs[i];
+ yield checkCacheEnabled(tab, states[i]);
+ }
+}
+
+function* checkCacheEnabled(tabX, expected) {
+ gBrowser.selectedTab = tabX.tab;
+
+ yield reloadTab(tabX);
+
+ let oldGuid = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let h1 = doc.querySelector("h1");
+ return h1.textContent;
+ });
+
+ yield reloadTab(tabX);
+
+ let guid = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function () {
+ let doc = content.document;
+ let h1 = doc.querySelector("h1");
+ return h1.textContent;
+ });
+
+ if (expected) {
+ is(guid, oldGuid, tabX.title + " cache is enabled");
+ } else {
+ isnot(guid, oldGuid, tabX.title + " cache is not enabled");
+ }
+}
+
+function* setDisableCacheCheckboxChecked(tabX, state) {
+ gBrowser.selectedTab = tabX.tab;
+
+ let panel = tabX.toolbox.getCurrentPanel();
+ let cbx = panel.panelDoc.getElementById("devtools-disable-cache");
+
+ if (cbx.checked !== state) {
+ info("Setting disable cache checkbox to " + state + " for " + tabX.title);
+ cbx.click();
+
+ // We need to wait for all checkboxes to be updated and the docshells to
+ // apply the new cache settings.
+ yield waitForTick();
+ }
+}
+
+function reloadTab(tabX) {
+ let def = defer();
+ let browser = gBrowser.selectedBrowser;
+
+ BrowserTestUtils.browserLoaded(browser).then(function () {
+ info("Reloaded tab " + tabX.title);
+ def.resolve();
+ });
+
+ info("Reloading tab " + tabX.title);
+ let mm = getFrameScript();
+ mm.sendAsyncMessage("devtools:test:reload");
+
+ return def.promise;
+}
+
+function* destroyTab(tabX) {
+ let toolbox = gDevTools.getToolbox(tabX.target);
+
+ let onceDestroyed = promise.resolve();
+ if (toolbox) {
+ onceDestroyed = gDevTools.once("toolbox-destroyed");
+ }
+
+ info("Removing tab " + tabX.title);
+ gBrowser.removeTab(tabX.tab);
+ info("Removed tab " + tabX.title);
+
+ info("Waiting for toolbox-destroyed");
+ yield onceDestroyed;
+}
+
+function* finishUp() {
+ for (let tab of tabs) {
+ yield destroyTab(tab);
+ }
+
+ tabs = null;
+}
diff --git a/devtools/client/framework/test/serviceworker.js b/devtools/client/framework/test/serviceworker.js
new file mode 100644
index 000000000..ed3c1ec32
--- /dev/null
+++ b/devtools/client/framework/test/serviceworker.js
@@ -0,0 +1,6 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// empty service worker, always succeed!
diff --git a/devtools/client/framework/test/shared-head.js b/devtools/client/framework/test/shared-head.js
new file mode 100644
index 000000000..a89c6d752
--- /dev/null
+++ b/devtools/client/framework/test/shared-head.js
@@ -0,0 +1,596 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+// This shared-head.js file is used for multiple mochitest test directories in
+// devtools.
+// It contains various common helper functions.
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC}
+ = Components;
+
+function scopedCuImport(path) {
+ const scope = {};
+ Cu.import(path, scope);
+ return scope;
+}
+
+const {console} = scopedCuImport("resource://gre/modules/Console.jsm");
+const {ScratchpadManager} = scopedCuImport("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+const {loader, require} = scopedCuImport("resource://devtools/shared/Loader.jsm");
+
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {TargetFactory} = require("devtools/client/framework/target");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+let promise = require("promise");
+let defer = require("devtools/shared/defer");
+const Services = require("Services");
+const {Task} = require("devtools/shared/task");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+const CHROME_URL_ROOT = TEST_DIR + "/";
+const URL_ROOT = CHROME_URL_ROOT.replace("chrome://mochitests/content/",
+ "http://example.com/");
+const URL_ROOT_SSL = CHROME_URL_ROOT.replace("chrome://mochitests/content/",
+ "https://example.com/");
+
+// All test are asynchronous
+waitForExplicitFinish();
+
+var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+
+registerCleanupFunction(function () {
+ if (DevToolsUtils.assertionFailureCount !==
+ EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
+ ok(false,
+ "Should have had the expected number of DevToolsUtils.assert() failures."
+ + " Expected " + EXPECTED_DTU_ASSERT_FAILURE_COUNT
+ + ", got " + DevToolsUtils.assertionFailureCount);
+ }
+});
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+/**
+ * Watch console messages for failed propType definitions in React components.
+ */
+const ConsoleObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function (subject, topic, data) {
+ let message = subject.wrappedJSObject.arguments[0];
+
+ if (/Failed propType/.test(message)) {
+ ok(false, message);
+ }
+ }
+};
+
+Services.obs.addObserver(ConsoleObserver, "console-api-log-event", false);
+registerCleanupFunction(() => {
+ Services.obs.removeObserver(ConsoleObserver, "console-api-log-event");
+});
+
+var waitForTime = DevToolsUtils.waitForTime;
+
+function getFrameScript() {
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let frameURL = "chrome://devtools/content/shared/frame-script-utils.js";
+ mm.loadFrameScript(frameURL, false);
+ SimpleTest.registerCleanupFunction(() => {
+ mm = null;
+ });
+ return mm;
+}
+
+flags.testing = true;
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.dump.emit");
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.previousHost");
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+});
+
+registerCleanupFunction(function* cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ yield closeTabAndToolbox(gBrowser.selectedTab);
+ }
+});
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @param {Object} options Object with various optional fields:
+ * - {Boolean} background If true, open the tab in background
+ * - {ChromeWindow} window Firefox top level window we should use to open the tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+var addTab = Task.async(function* (url, options = { background: false, window: window }) {
+ info("Adding a new tab with URL: " + url);
+
+ let { background } = options;
+ let { gBrowser } = options.window ? options.window : window;
+
+ let tab = gBrowser.addTab(url);
+ if (!background) {
+ gBrowser.selectedTab = tab;
+ }
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("Tab added and finished loading");
+
+ return tab;
+});
+
+/**
+ * Remove the given tab.
+ * @param {Object} tab The tab to be removed.
+ * @return Promise<undefined> resolved when the tab is successfully removed.
+ */
+var removeTab = Task.async(function* (tab) {
+ info("Removing tab.");
+
+ let { gBrowser } = tab.ownerDocument.defaultView;
+ let onClose = once(gBrowser.tabContainer, "TabClose");
+ gBrowser.removeTab(tab);
+ yield onClose;
+
+ info("Tab removed and finished closing");
+});
+
+/**
+ * Refresh the given tab.
+ * @param {Object} tab The tab to be refreshed.
+ * @return Promise<undefined> resolved when the tab is successfully refreshed.
+ */
+var refreshTab = Task.async(function*(tab) {
+ info("Refreshing tab.");
+ const finished = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.reloadTab(gBrowser.selectedTab);
+ yield finished;
+ info("Tab finished refreshing.");
+});
+
+/**
+ * Simulate a key event from a <key> element.
+ * @param {DOMNode} key
+ */
+function synthesizeKeyFromKeyTag(key) {
+ is(key && key.tagName, "key", "Successfully retrieved the <key> node");
+
+ let modifiersAttr = key.getAttribute("modifiers");
+
+ let name = null;
+
+ if (key.getAttribute("keycode")) {
+ name = key.getAttribute("keycode");
+ } else if (key.getAttribute("key")) {
+ name = key.getAttribute("key");
+ }
+
+ isnot(name, null, "Successfully retrieved keycode/key");
+
+ let modifiers = {
+ shiftKey: !!modifiersAttr.match("shift"),
+ ctrlKey: !!modifiersAttr.match("control"),
+ altKey: !!modifiersAttr.match("alt"),
+ metaKey: !!modifiersAttr.match("meta"),
+ accelKey: !!modifiersAttr.match("accel")
+ };
+
+ info("Synthesizing key " + name + " " + JSON.stringify(modifiers));
+ EventUtils.synthesizeKey(name, modifiers);
+}
+
+/**
+ * Simulate a key event from an electron key shortcut string:
+ * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
+ *
+ * @param {String} key
+ * @param {DOMWindow} target
+ * Optional window where to fire the key event
+ */
+function synthesizeKeyShortcut(key, target) {
+ // parseElectronKey requires any window, just to access `KeyboardEvent`
+ let window = Services.appShell.hiddenDOMWindow;
+ let shortcut = KeyShortcuts.parseElectronKey(window, key);
+ let keyEvent = {
+ altKey: shortcut.alt,
+ ctrlKey: shortcut.ctrl,
+ metaKey: shortcut.meta,
+ shiftKey: shortcut.shift
+ };
+ if (shortcut.keyCode) {
+ keyEvent.keyCode = shortcut.keyCode;
+ }
+
+ info("Synthesizing key shortcut: " + key);
+ EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
+}
+
+/**
+ * Wait for eventName on target to be delivered a number of times.
+ *
+ * @param {Object} target
+ * An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Number} numTimes
+ * Number of deliveries to wait for.
+ * @param {Boolean} useCapture
+ * Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function waitForNEvents(target, eventName, numTimes, useCapture = false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ let deferred = defer();
+ let count = 0;
+
+ for (let [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"]
+ ]) {
+ if ((add in target) && (remove in target)) {
+ target[add](eventName, function onEvent(...aArgs) {
+ info("Got event: '" + eventName + "' on " + target + ".");
+ if (++count == numTimes) {
+ target[remove](eventName, onEvent, useCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }
+ }, useCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Wait for eventName on target.
+ *
+ * @param {Object} target
+ * An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture
+ * Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture = false) {
+ return waitForNEvents(target, eventName, 1, useCapture);
+}
+
+/**
+ * Some tests may need to import one or more of the test helper scripts.
+ * A test helper script is simply a js file that contains common test code that
+ * is either not common-enough to be in head.js, or that is located in a
+ * separate directory.
+ * The script will be loaded synchronously and in the test's scope.
+ * @param {String} filePath The file path, relative to the current directory.
+ * Examples:
+ * - "helper_attributes_test_runner.js"
+ * - "../../../commandline/test/helpers.js"
+ */
+function loadHelperScript(filePath) {
+ let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+ Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
+}
+
+/**
+ * Wait for a tick.
+ * @return {Promise}
+ */
+function waitForTick() {
+ let deferred = defer();
+ executeSoon(deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * This shouldn't be used in the tests, but is useful when writing new tests or
+ * debugging existing tests in order to introduce delays in the test steps
+ *
+ * @param {Number} ms
+ * The time to wait
+ * @return A promise that resolves when the time is passed
+ */
+function wait(ms) {
+ return new promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Open the toolbox in a given tab.
+ * @param {XULNode} tab The tab the toolbox should be opened in.
+ * @param {String} toolId Optional. The ID of the tool to be selected.
+ * @param {String} hostType Optional. The type of toolbox host to be used.
+ * @return {Promise} Resolves with the toolbox, when it has been opened.
+ */
+var openToolboxForTab = Task.async(function* (tab, toolId, hostType) {
+ info("Opening the toolbox");
+
+ let toolbox;
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+
+ // Check if the toolbox is already loaded.
+ toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ if (!toolId || (toolId && toolbox.getPanel(toolId))) {
+ info("Toolbox is already opened");
+ return toolbox;
+ }
+ }
+
+ // If not, load it now.
+ toolbox = yield gDevTools.showToolbox(target, toolId, hostType);
+
+ // Make sure that the toolbox frame is focused.
+ yield new Promise(resolve => waitForFocus(resolve, toolbox.win));
+
+ info("Toolbox opened and focused");
+
+ return toolbox;
+});
+
+/**
+ * Add a new tab and open the toolbox in it.
+ * @param {String} url The URL for the tab to be opened.
+ * @param {String} toolId Optional. The ID of the tool to be selected.
+ * @param {String} hostType Optional. The type of toolbox host to be used.
+ * @return {Promise} Resolves when the tab has been added, loaded and the
+ * toolbox has been opened. Resolves to the toolbox.
+ */
+var openNewTabAndToolbox = Task.async(function* (url, toolId, hostType) {
+ let tab = yield addTab(url);
+ return openToolboxForTab(tab, toolId, hostType);
+});
+
+/**
+ * Close a tab and if necessary, the toolbox that belongs to it
+ * @param {Tab} tab The tab to close.
+ * @return {Promise} Resolves when the toolbox and tab have been destroyed and
+ * closed.
+ */
+var closeTabAndToolbox = Task.async(function* (tab = gBrowser.selectedTab) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ if (target) {
+ yield gDevTools.closeToolbox(target);
+ }
+
+ yield removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Close a toolbox and the current tab.
+ * @param {Toolbox} toolbox The toolbox to close.
+ * @return {Promise} Resolves when the toolbox and tab have been destroyed and
+ * closed.
+ */
+var closeToolboxAndTab = Task.async(function* (toolbox) {
+ yield toolbox.destroy();
+ yield removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ * Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function () {
+ waitUntil(predicate, interval).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+let MM_INC_ID = 0;
+function evalInDebuggee(mm, script) {
+ return new Promise(function (resolve, reject) {
+ let id = MM_INC_ID++;
+ mm.sendAsyncMessage("devtools:test:eval", { script, id });
+ mm.addMessageListener("devtools:test:eval:response", handler);
+
+ function handler({ data }) {
+ if (id !== data.id) {
+ return;
+ }
+
+ info(`Successfully evaled in debuggee: ${script}`);
+ mm.removeMessageListener("devtools:test:eval:response", handler);
+ resolve(data.value);
+ }
+ });
+}
+
+/**
+ * Wait for a context menu popup to open.
+ *
+ * @param nsIDOMElement popup
+ * The XUL popup you expect to open.
+ * @param nsIDOMElement button
+ * The button/element that receives the contextmenu event. This is
+ * expected to open the popup.
+ * @param function onShown
+ * Function to invoke on popupshown event.
+ * @param function onHidden
+ * Function to invoke on popuphidden event.
+ * @return object
+ * A Promise object that is resolved after the popuphidden event
+ * callback is invoked.
+ */
+function waitForContextMenu(popup, button, onShown, onHidden) {
+ let deferred = defer();
+
+ function onPopupShown() {
+ info("onPopupShown");
+ popup.removeEventListener("popupshown", onPopupShown);
+
+ onShown && onShown();
+
+ // Use executeSoon() to get out of the popupshown event.
+ popup.addEventListener("popuphidden", onPopupHidden);
+ executeSoon(() => popup.hidePopup());
+ }
+ function onPopupHidden() {
+ info("onPopupHidden");
+ popup.removeEventListener("popuphidden", onPopupHidden);
+
+ onHidden && onHidden();
+
+ deferred.resolve(popup);
+ }
+
+ popup.addEventListener("popupshown", onPopupShown);
+
+ info("wait for the context menu to open");
+ button.scrollIntoView();
+ let eventDetails = {type: "contextmenu", button: 2};
+ EventUtils.synthesizeMouse(button, 5, 2, eventDetails,
+ button.ownerDocument.defaultView);
+ return deferred.promise;
+}
+
+/**
+ * Promise wrapper around SimpleTest.waitForClipboard
+ */
+function waitForClipboardPromise(setup, expected) {
+ return new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(expected, setup, resolve, reject);
+ });
+}
+
+/**
+ * Simple helper to push a temporary preference. Wrapper on SpecialPowers
+ * pushPrefEnv that returns a promise resolving when the preferences have been
+ * updated.
+ *
+ * @param {String} preferenceName
+ * The name of the preference to updated
+ * @param {} value
+ * The preference value, type can vary
+ * @return {Promise} resolves when the preferences have been updated
+ */
+function pushPref(preferenceName, value) {
+ return new Promise(resolve => {
+ let options = {"set": [[preferenceName, value]]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+}
+
+/**
+ * Lookup the provided dotted path ("prop1.subprop2.myProp") in the provided object.
+ *
+ * @param {Object} obj
+ * Object to expand.
+ * @param {String} path
+ * Dotted path to use to expand the object.
+ * @return {?} anything that is found at the provided path in the object.
+ */
+function lookupPath(obj, path) {
+ let segments = path.split(".");
+ return segments.reduce((prev, current) => prev[current], obj);
+}
+
+var closeToolbox = Task.async(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+});
+
+/**
+ * Load the Telemetry utils, then stub Telemetry.prototype.log and
+ * Telemetry.prototype.logKeyed in order to record everything that's logged in
+ * it.
+ * Store all recordings in Telemetry.telemetryInfo.
+ * @return {Telemetry}
+ */
+function loadTelemetryAndRecordLogs() {
+ info("Mock the Telemetry log function to record logged information");
+
+ let Telemetry = require("devtools/client/shared/telemetry");
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function (histogramId, value) {
+ if (!this.telemetryInfo) {
+ // Telemetry instance still in use after stopRecordingTelemetryLogs
+ return;
+ }
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+ this.telemetryInfo[histogramId].push(value);
+ }
+ };
+ Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
+ Telemetry.prototype.logKeyed = function (histogramId, key, value) {
+ this.log(`${histogramId}|${key}`, value);
+ };
+
+ return Telemetry;
+}
+
+/**
+ * Stop recording the Telemetry logs and put back the utils as it was before.
+ * @param {Telemetry} Required Telemetry
+ * Telemetry object that needs to be stopped.
+ */
+function stopRecordingTelemetryLogs(Telemetry) {
+ info("Stopping Telemetry");
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype.telemetryInfo;
+}
+
+/**
+ * Clean the logical clipboard content. This method only clears the OS clipboard on
+ * Windows (see Bug 666254).
+ */
+function emptyClipboard() {
+ let clipboard = Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(SpecialPowers.Ci.nsIClipboard);
+ clipboard.emptyClipboard(clipboard.kGlobalClipboard);
+}
+
+/**
+ * Check if the current operating system is Windows.
+ */
+function isWindows() {
+ return Services.appinfo.OS === "WINNT";
+}
+
+/**
+ * Wait for a given toolbox to get its title updated.
+ */
+function waitForTitleChange(toolbox) {
+ let deferred = defer();
+ toolbox.win.parent.addEventListener("message", function onmessage(event) {
+ if (event.data.name == "set-host-title") {
+ toolbox.win.parent.removeEventListener("message", onmessage);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
diff --git a/devtools/client/framework/test/shared-redux-head.js b/devtools/client/framework/test/shared-redux-head.js
new file mode 100644
index 000000000..c7c939152
--- /dev/null
+++ b/devtools/client/framework/test/shared-redux-head.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ./shared-head.js */
+// Currently this file expects "defer" to be imported into scope.
+
+// Common utility functions for working with Redux stores. The file is meant
+// to be safe to load in both mochitest and xpcshell environments.
+
+/**
+ * A logging function that can be used from xpcshell and browser mochitest
+ * environments.
+ */
+function commonLog(message) {
+ let log;
+ if (Services && Services.appinfo && Services.appinfo.name &&
+ Services.appinfo.name == "Firefox") {
+ log = info;
+ } else {
+ log = do_print;
+ }
+ log(message);
+}
+
+/**
+ * Wait until the store has reached a state that matches the predicate.
+ * @param Store store
+ * The Redux store being used.
+ * @param function predicate
+ * A function that returns true when the store has reached the expected
+ * state.
+ * @return Promise
+ * Resolved once the store reaches the expected state.
+ */
+function waitUntilState(store, predicate) {
+ let deferred = defer();
+ let unsubscribe = store.subscribe(check);
+
+ commonLog(`Waiting for state predicate "${predicate}"`);
+ function check() {
+ if (predicate(store.getState())) {
+ commonLog(`Found state predicate "${predicate}"`);
+ unsubscribe();
+ deferred.resolve();
+ }
+ }
+
+ // Fire the check immediately in case the action has already occurred
+ check();
+
+ return deferred.promise;
+}
+
+/**
+ * Wait until a particular action has been emitted by the store.
+ * @param Store store
+ * The Redux store being used.
+ * @param string actionType
+ * The expected action to wait for.
+ * @return Promise
+ * Resolved once the expected action is emitted by the store.
+ */
+function waitUntilAction(store, actionType) {
+ let deferred = defer();
+ let unsubscribe = store.subscribe(check);
+ let history = store.history;
+ let index = history.length;
+
+ commonLog(`Waiting for action "${actionType}"`);
+ function check() {
+ let action = history[index++];
+ if (action && action.type === actionType) {
+ commonLog(`Found action "${actionType}"`);
+ unsubscribe();
+ deferred.resolve(store.getState());
+ }
+ }
+
+ return deferred.promise;
+}
diff --git a/devtools/client/framework/toolbox-highlighter-utils.js b/devtools/client/framework/toolbox-highlighter-utils.js
new file mode 100644
index 000000000..e7f343857
--- /dev/null
+++ b/devtools/client/framework/toolbox-highlighter-utils.js
@@ -0,0 +1,324 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const promise = require("promise");
+const {Task} = require("devtools/shared/task");
+const flags = require("devtools/shared/flags");
+
+/**
+ * Client-side highlighter shared module.
+ * To be used by toolbox panels that need to highlight DOM elements.
+ *
+ * Highlighting and selecting elements is common enough that it needs to be at
+ * toolbox level, accessible by any panel that needs it.
+ * That's why the toolbox is the one that initializes the inspector and
+ * highlighter. It's also why the API returned by this module needs a reference
+ * to the toolbox which should be set once only.
+ */
+
+/**
+ * Get the highighterUtils instance for a given toolbox.
+ * This should be done once only by the toolbox itself and stored there so that
+ * panels can get it from there. That's because the API returned has a stateful
+ * scope that would be different for another instance returned by this function.
+ *
+ * @param {Toolbox} toolbox
+ * @return {Object} the highlighterUtils public API
+ */
+exports.getHighlighterUtils = function (toolbox) {
+ if (!toolbox || !toolbox.target) {
+ throw new Error("Missing or invalid toolbox passed to getHighlighterUtils");
+ return;
+ }
+
+ // Exported API properties will go here
+ let exported = {};
+
+ // The current toolbox target
+ let target = toolbox.target;
+
+ // Is the highlighter currently in pick mode
+ let isPicking = false;
+
+ // Is the box model already displayed, used to prevent dispatching
+ // unnecessary requests, especially during toolbox shutdown
+ let isNodeFrontHighlighted = false;
+
+ /**
+ * Release this utils, nullifying the references to the toolbox
+ */
+ exported.release = function () {
+ toolbox = target = null;
+ };
+
+ /**
+ * Does the target have the highlighter actor.
+ * The devtools must be backwards compatible with at least B2G 1.3 (28),
+ * which doesn't have the highlighter actor. This can be removed as soon as
+ * the minimal supported version becomes 1.4 (29)
+ */
+ let isRemoteHighlightable = exported.isRemoteHighlightable = function () {
+ return target.client.traits.highlightable;
+ };
+
+ /**
+ * Does the target support custom highlighters.
+ */
+ let supportsCustomHighlighters = exported.supportsCustomHighlighters = () => {
+ return !!target.client.traits.customHighlighters;
+ };
+
+ /**
+ * Make a function that initializes the inspector before it runs.
+ * Since the init of the inspector is asynchronous, the return value will be
+ * produced by Task.async and the argument should be a generator
+ * @param {Function*} generator A generator function
+ * @return {Function} A function
+ */
+ let isInspectorInitialized = false;
+ let requireInspector = generator => {
+ return Task.async(function* (...args) {
+ if (!isInspectorInitialized) {
+ yield toolbox.initInspector();
+ isInspectorInitialized = true;
+ }
+ return yield generator.apply(null, args);
+ });
+ };
+
+ /**
+ * Start/stop the element picker on the debuggee target.
+ * @param {Boolean} doFocus - Optionally focus the content area once the picker is
+ * activated.
+ * @return A promise that resolves when done
+ */
+ let togglePicker = exported.togglePicker = function (doFocus) {
+ if (isPicking) {
+ return cancelPicker();
+ } else {
+ return startPicker(doFocus);
+ }
+ };
+
+ /**
+ * Start the element picker on the debuggee target.
+ * This will request the inspector actor to start listening for mouse events
+ * on the target page to highlight the hovered/picked element.
+ * Depending on the server-side capabilities, this may fire events when nodes
+ * are hovered.
+ * @param {Boolean} doFocus - Optionally focus the content area once the picker is
+ * activated.
+ * @return A promise that resolves when the picker has started or immediately
+ * if it is already started
+ */
+ let startPicker = exported.startPicker = requireInspector(function* (doFocus = false) {
+ if (isPicking) {
+ return;
+ }
+ isPicking = true;
+
+ toolbox.pickerButtonChecked = true;
+ yield toolbox.selectTool("inspector");
+ toolbox.on("select", cancelPicker);
+
+ if (isRemoteHighlightable()) {
+ toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
+ toolbox.walker.on("picker-node-picked", onPickerNodePicked);
+ toolbox.walker.on("picker-node-previewed", onPickerNodePreviewed);
+ toolbox.walker.on("picker-node-canceled", onPickerNodeCanceled);
+
+ yield toolbox.highlighter.pick(doFocus);
+ toolbox.emit("picker-started");
+ } else {
+ // If the target doesn't have the highlighter actor, we can use the
+ // walker's pick method instead, knowing that it only responds when a node
+ // is picked (instead of emitting events)
+ toolbox.emit("picker-started");
+ let node = yield toolbox.walker.pick();
+ onPickerNodePicked({node: node});
+ }
+ });
+
+ /**
+ * Stop the element picker. Note that the picker is automatically stopped when
+ * an element is picked
+ * @return A promise that resolves when the picker has stopped or immediately
+ * if it is already stopped
+ */
+ let stopPicker = exported.stopPicker = requireInspector(function* () {
+ if (!isPicking) {
+ return;
+ }
+ isPicking = false;
+
+ toolbox.pickerButtonChecked = false;
+
+ if (isRemoteHighlightable()) {
+ yield toolbox.highlighter.cancelPick();
+ toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
+ toolbox.walker.off("picker-node-picked", onPickerNodePicked);
+ toolbox.walker.off("picker-node-previewed", onPickerNodePreviewed);
+ toolbox.walker.off("picker-node-canceled", onPickerNodeCanceled);
+ } else {
+ // If the target doesn't have the highlighter actor, use the walker's
+ // cancelPick method instead
+ yield toolbox.walker.cancelPick();
+ }
+
+ toolbox.off("select", cancelPicker);
+ toolbox.emit("picker-stopped");
+ });
+
+ /**
+ * Stop the picker, but also emit an event that the picker was canceled.
+ */
+ let cancelPicker = exported.cancelPicker = Task.async(function* () {
+ yield stopPicker();
+ toolbox.emit("picker-canceled");
+ });
+
+ /**
+ * When a node is hovered by the mouse when the highlighter is in picker mode
+ * @param {Object} data Information about the node being hovered
+ */
+ function onPickerNodeHovered(data) {
+ toolbox.emit("picker-node-hovered", data.node);
+ }
+
+ /**
+ * When a node has been picked while the highlighter is in picker mode
+ * @param {Object} data Information about the picked node
+ */
+ function onPickerNodePicked(data) {
+ toolbox.selection.setNodeFront(data.node, "picker-node-picked");
+ stopPicker();
+ }
+
+ /**
+ * When a node has been shift-clicked (previewed) while the highlighter is in
+ * picker mode
+ * @param {Object} data Information about the picked node
+ */
+ function onPickerNodePreviewed(data) {
+ toolbox.selection.setNodeFront(data.node, "picker-node-previewed");
+ }
+
+ /**
+ * When the picker is canceled, stop the picker, and make sure the toolbox
+ * gets the focus.
+ */
+ function onPickerNodeCanceled() {
+ cancelPicker();
+ toolbox.win.focus();
+ }
+
+ /**
+ * Show the box model highlighter on a node in the content page.
+ * The node needs to be a NodeFront, as defined by the inspector actor
+ * @see devtools/server/actors/inspector.js
+ * @param {NodeFront} nodeFront The node to highlight
+ * @param {Object} options
+ * @return A promise that resolves when the node has been highlighted
+ */
+ let highlightNodeFront = exported.highlightNodeFront = requireInspector(
+ function* (nodeFront, options = {}) {
+ if (!nodeFront) {
+ return;
+ }
+
+ isNodeFrontHighlighted = true;
+ if (isRemoteHighlightable()) {
+ yield toolbox.highlighter.showBoxModel(nodeFront, options);
+ } else {
+ // If the target doesn't have the highlighter actor, revert to the
+ // walker's highlight method, which draws a simple outline
+ yield toolbox.walker.highlight(nodeFront);
+ }
+
+ toolbox.emit("node-highlight", nodeFront, options.toSource());
+ });
+
+ /**
+ * This is a convenience method in case you don't have a nodeFront but a
+ * valueGrip. This is often the case with VariablesView properties.
+ * This method will simply translate the grip into a nodeFront and call
+ * highlightNodeFront, so it has the same signature.
+ * @see highlightNodeFront
+ */
+ let highlightDomValueGrip = exported.highlightDomValueGrip = requireInspector(
+ function* (valueGrip, options = {}) {
+ let nodeFront = yield gripToNodeFront(valueGrip);
+ if (nodeFront) {
+ yield highlightNodeFront(nodeFront, options);
+ } else {
+ throw new Error("The ValueGrip passed could not be translated to a NodeFront");
+ }
+ });
+
+ /**
+ * Translate a debugger value grip into a node front usable by the inspector
+ * @param {ValueGrip}
+ * @return a promise that resolves to the node front when done
+ */
+ let gripToNodeFront = exported.gripToNodeFront = requireInspector(
+ function* (grip) {
+ return yield toolbox.walker.getNodeActorFromObjectActor(grip.actor);
+ });
+
+ /**
+ * Hide the highlighter.
+ * @param {Boolean} forceHide Only really matters in test mode (when
+ * flags.testing is true). In test mode, hovering over several nodes
+ * in the markup view doesn't hide/show the highlighter to ease testing. The
+ * highlighter stays visible at all times, except when the mouse leaves the
+ * markup view, which is when this param is passed to true
+ * @return a promise that resolves when the highlighter is hidden
+ */
+ let unhighlight = exported.unhighlight = Task.async(
+ function* (forceHide = false) {
+ forceHide = forceHide || !flags.testing;
+
+ // Note that if isRemoteHighlightable is true, there's no need to hide the
+ // highlighter as the walker uses setTimeout to hide it after some time
+ if (isNodeFrontHighlighted && forceHide && toolbox.highlighter && isRemoteHighlightable()) {
+ isNodeFrontHighlighted = false;
+ yield toolbox.highlighter.hideBoxModel();
+ }
+
+ // unhighlight is called when destroying the toolbox, which means that by
+ // now, the toolbox reference might have been nullified already.
+ if (toolbox) {
+ toolbox.emit("node-unhighlight");
+ }
+ });
+
+ /**
+ * If the main, box-model, highlighter isn't enough, or if multiple
+ * highlighters are needed in parallel, this method can be used to return a
+ * new instance of a highlighter actor, given a type.
+ * The type of the highlighter passed must be known by the server.
+ * The highlighter actor returned will have the show(nodeFront) and hide()
+ * methods and needs to be released by the consumer when not needed anymore.
+ * @return a promise that resolves to the highlighter
+ */
+ let getHighlighterByType = exported.getHighlighterByType = requireInspector(
+ function* (typeName) {
+ let highlighter = null;
+
+ if (supportsCustomHighlighters()) {
+ highlighter = yield toolbox.inspector.getHighlighterByType(typeName);
+ }
+
+ return highlighter || promise.reject("The target doesn't support " +
+ `creating highlighters by types or ${typeName} is unknown`);
+
+ });
+
+ // Return the public API
+ return exported;
+};
diff --git a/devtools/client/framework/toolbox-host-manager.js b/devtools/client/framework/toolbox-host-manager.js
new file mode 100644
index 000000000..1638f3a9a
--- /dev/null
+++ b/devtools/client/framework/toolbox-host-manager.js
@@ -0,0 +1,244 @@
+const Services = require("Services");
+const {Ci} = require("chrome");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const {Task} = require("devtools/shared/task");
+
+loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
+loader.lazyRequireGetter(this, "Hosts", "devtools/client/framework/toolbox-hosts", true);
+
+/**
+ * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
+ *
+ * This component handles iframe creation within Firefox, in which we are loading
+ * the toolbox document. Then both the chrome and the toolbox document communicate
+ * via "message" events.
+ *
+ * Messages sent by the toolbox to the chrome:
+ * - switch-host:
+ * Order to display the toolbox in another host (side, bottom, window, or the
+ * previously used one)
+ * - toggle-minimize-mode:
+ * When using the bottom host, the toolbox can be miximized to only display
+ * the tool titles
+ * - maximize-host:
+ * When using the bottom host in minimized mode, revert back to regular mode
+ * in order to see tool titles and the tools
+ * - raise-host:
+ * Focus the tools
+ * - set-host-title:
+ * When using the window host, update the window title
+ *
+ * Messages sent by the chrome to the toolbox:
+ * - host-minimized:
+ * The bottom host is done minimizing (after animation end)
+ * - host-maximized:
+ * The bottom host is done switching back to regular mode (after animation
+ * end)
+ * - switched-host:
+ * The `switch-host` command sent by the toolbox is done
+ */
+
+const LAST_HOST = "devtools.toolbox.host";
+const PREVIOUS_HOST = "devtools.toolbox.previousHost";
+let ID_COUNTER = 1;
+
+function ToolboxHostManager(target, hostType, hostOptions) {
+ this.target = target;
+
+ this.frameId = ID_COUNTER++;
+
+ if (!hostType) {
+ hostType = Services.prefs.getCharPref(LAST_HOST);
+ }
+ this.onHostMinimized = this.onHostMinimized.bind(this);
+ this.onHostMaximized = this.onHostMaximized.bind(this);
+ this.host = this.createHost(hostType, hostOptions);
+ this.hostType = hostType;
+}
+
+ToolboxHostManager.prototype = {
+ create: Task.async(function* (toolId) {
+ yield this.host.create();
+
+ this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
+ this.host.frame.ownerDocument.defaultView.addEventListener("message", this);
+ // We have to listen on capture as no event fires on bubble
+ this.host.frame.addEventListener("unload", this, true);
+
+ let toolbox = new Toolbox(this.target, toolId, this.host.type, this.host.frame.contentWindow, this.frameId);
+
+ // Prevent reloading the toolbox when loading the tools in a tab (e.g. from about:debugging)
+ if (!this.host.frame.contentWindow.location.href.startsWith("about:devtools-toolbox")) {
+ this.host.frame.setAttribute("src", "about:devtools-toolbox");
+ }
+
+ return toolbox;
+ }),
+
+ handleEvent(event) {
+ switch(event.type) {
+ case "message":
+ this.onMessage(event);
+ break;
+ case "unload":
+ // On unload, host iframe already lost its contentWindow attribute, so
+ // we can only compare against locations. Here we filter two very
+ // different cases: preliminary about:blank document as well as iframes
+ // like tool iframes.
+ if (!event.target.location.href.startsWith("about:devtools-toolbox")) {
+ break;
+ }
+ // Don't destroy the host during unload event (esp., don't remove the
+ // iframe from DOM!). Otherwise the unload event for the toolbox
+ // document doesn't fire within the toolbox *document*! This is
+ // the unload event that fires on the toolbox *iframe*.
+ DevToolsUtils.executeSoon(() => {
+ this.destroy();
+ });
+ break;
+ }
+ },
+
+ onMessage(event) {
+ if (!event.data) {
+ return;
+ }
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ if (event.data.frameId != this.frameId) {
+ return;
+ }
+ switch (event.data.name) {
+ case "switch-host":
+ this.switchHost(event.data.hostType);
+ break;
+ case "maximize-host":
+ this.host.maximize();
+ break;
+ case "raise-host":
+ this.host.raise();
+ break;
+ case "toggle-minimize-mode":
+ this.host.toggleMinimizeMode(event.data.toolbarHeight);
+ break;
+ case "set-host-title":
+ this.host.setTitle(event.data.title);
+ break;
+ }
+ },
+
+ postMessage(data) {
+ let window = this.host.frame.contentWindow;
+ window.postMessage(data, "*");
+ },
+
+ destroy() {
+ this.destroyHost();
+ this.host = null;
+ this.hostType = null;
+ this.target = null;
+ },
+
+ /**
+ * Create a host object based on the given host type.
+ *
+ * Warning: bottom and sidebar hosts require that the toolbox target provides
+ * a reference to the attached tab. Not all Targets have a tab property -
+ * make sure you correctly mix and match hosts and targets.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ *
+ * @return {Host} host
+ * The created host object
+ */
+ createHost(hostType, options) {
+ if (!Hosts[hostType]) {
+ throw new Error("Unknown hostType: " + hostType);
+ }
+
+ let newHost = new Hosts[hostType](this.target.tab, options);
+ // Update the label and icon when the state changes.
+ newHost.on("minimized", this.onHostMinimized);
+ newHost.on("maximized", this.onHostMaximized);
+ return newHost;
+ },
+
+ onHostMinimized() {
+ this.postMessage({
+ name: "host-minimized"
+ });
+ },
+
+ onHostMaximized() {
+ this.postMessage({
+ name: "host-maximized"
+ });
+ },
+
+ switchHost: Task.async(function* (hostType) {
+ if (hostType == "previous") {
+ // Switch to the last used host for the toolbox UI.
+ // This is determined by the devtools.toolbox.previousHost pref.
+ hostType = Services.prefs.getCharPref(PREVIOUS_HOST);
+
+ // Handle the case where the previous host happens to match the current
+ // host. If so, switch to bottom if it's not already used, and side if not.
+ if (hostType === this.hostType) {
+ if (hostType === Toolbox.HostType.BOTTOM) {
+ hostType = Toolbox.HostType.SIDE;
+ } else {
+ hostType = Toolbox.HostType.BOTTOM;
+ }
+ }
+ }
+ let iframe = this.host.frame;
+ let newHost = this.createHost(hostType);
+ let newIframe = yield newHost.create();
+ // change toolbox document's parent to the new host
+ newIframe.swapFrameLoaders(iframe);
+
+ this.destroyHost();
+
+ if (this.hostType != Toolbox.HostType.CUSTOM) {
+ Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
+ }
+
+ this.host = newHost;
+ this.hostType = hostType;
+ this.host.setTitle(this.host.frame.contentWindow.document.title);
+ this.host.frame.ownerDocument.defaultView.addEventListener("message", this);
+ this.host.frame.addEventListener("unload", this, true);
+
+ if (hostType != Toolbox.HostType.CUSTOM) {
+ Services.prefs.setCharPref(LAST_HOST, hostType);
+ }
+
+ // Tell the toolbox the host changed
+ this.postMessage({
+ name: "switched-host",
+ hostType
+ });
+ }),
+
+ /**
+ * Destroy the current host, and remove event listeners from its frame.
+ *
+ * @return {promise} to be resolved when the host is destroyed.
+ */
+ destroyHost() {
+ // When Firefox toplevel is closed, the frame may already be detached and
+ // the top level document gone
+ if (this.host.frame.ownerDocument.defaultView) {
+ this.host.frame.ownerDocument.defaultView.removeEventListener("message", this);
+ }
+ this.host.frame.removeEventListener("unload", this, true);
+
+ this.host.off("minimized", this.onHostMinimized);
+ this.host.off("maximized", this.onHostMaximized);
+ return this.host.destroy();
+ }
+};
+exports.ToolboxHostManager = ToolboxHostManager;
diff --git a/devtools/client/framework/toolbox-hosts.js b/devtools/client/framework/toolbox-hosts.js
new file mode 100644
index 000000000..ea774549a
--- /dev/null
+++ b/devtools/client/framework/toolbox-hosts.js
@@ -0,0 +1,425 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const {DOMHelpers} = require("resource://devtools/client/shared/DOMHelpers.jsm");
+
+loader.lazyRequireGetter(this, "system", "devtools/shared/system");
+
+/* A host should always allow this much space for the page to be displayed.
+ * There is also a min-height on the browser, but we still don't want to set
+ * frame.height to be larger than that, since it can cause problems with
+ * resizing the toolbox and panel layout. */
+const MIN_PAGE_SIZE = 25;
+
+/**
+ * A toolbox host represents an object that contains a toolbox (e.g. the
+ * sidebar or a separate window). Any host object should implement the
+ * following functions:
+ *
+ * create() - create the UI and emit a 'ready' event when the UI is ready to use
+ * destroy() - destroy the host's UI
+ */
+
+exports.Hosts = {
+ "bottom": BottomHost,
+ "side": SidebarHost,
+ "window": WindowHost,
+ "custom": CustomHost
+};
+
+/**
+ * Host object for the dock on the bottom of the browser
+ */
+function BottomHost(hostTab) {
+ this.hostTab = hostTab;
+
+ EventEmitter.decorate(this);
+}
+
+BottomHost.prototype = {
+ type: "bottom",
+
+ heightPref: "devtools.toolbox.footer.height",
+
+ /**
+ * Create a box at the bottom of the host tab.
+ */
+ create: function () {
+ let deferred = defer();
+
+ let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ let ownerDocument = gBrowser.ownerDocument;
+ this._nbox = gBrowser.getNotificationBox(this.hostTab.linkedBrowser);
+
+ this._splitter = ownerDocument.createElement("splitter");
+ this._splitter.setAttribute("class", "devtools-horizontal-splitter");
+ // Avoid resizing notification containers
+ this._splitter.setAttribute("resizebefore", "flex");
+
+ this.frame = ownerDocument.createElement("iframe");
+ this.frame.className = "devtools-toolbox-bottom-iframe";
+ this.frame.height = Math.min(
+ Services.prefs.getIntPref(this.heightPref),
+ this._nbox.clientHeight - MIN_PAGE_SIZE
+ );
+
+ this._nbox.appendChild(this._splitter);
+ this._nbox.appendChild(this.frame);
+
+ let frameLoad = () => {
+ this.emit("ready", this.frame);
+ deferred.resolve(this.frame);
+ };
+
+ this.frame.tooltip = "aHTMLTooltip";
+
+ // we have to load something so we can switch documents if we have to
+ this.frame.setAttribute("src", "about:blank");
+
+ let domHelper = new DOMHelpers(this.frame.contentWindow);
+ domHelper.onceDOMReady(frameLoad);
+
+ focusTab(this.hostTab);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function () {
+ focusTab(this.hostTab);
+ },
+
+ /**
+ * Minimize this host so that only the toolbox tabbar remains visible.
+ * @param {Number} height The height to minimize to. Defaults to 0, which
+ * means that the toolbox won't be visible at all once minimized.
+ */
+ minimize: function (height = 0) {
+ if (this.isMinimized) {
+ return;
+ }
+ this.isMinimized = true;
+
+ let onTransitionEnd = event => {
+ if (event.propertyName !== "margin-bottom") {
+ // Ignore transitionend on unrelated properties.
+ return;
+ }
+
+ this.frame.removeEventListener("transitionend", onTransitionEnd);
+ this.emit("minimized");
+ };
+ this.frame.addEventListener("transitionend", onTransitionEnd);
+ this.frame.style.marginBottom = -this.frame.height + height + "px";
+ this._splitter.classList.add("disabled");
+ },
+
+ /**
+ * If the host was minimized before, maximize it again (the host will be
+ * maximized to the height it previously had).
+ */
+ maximize: function () {
+ if (!this.isMinimized) {
+ return;
+ }
+ this.isMinimized = false;
+
+ let onTransitionEnd = event => {
+ if (event.propertyName !== "margin-bottom") {
+ // Ignore transitionend on unrelated properties.
+ return;
+ }
+
+ this.frame.removeEventListener("transitionend", onTransitionEnd);
+ this.emit("maximized");
+ };
+ this.frame.addEventListener("transitionend", onTransitionEnd);
+ this.frame.style.marginBottom = "0";
+ this._splitter.classList.remove("disabled");
+ },
+
+ /**
+ * Toggle the minimize mode.
+ * @param {Number} minHeight The height to minimize to.
+ */
+ toggleMinimizeMode: function (minHeight) {
+ this.isMinimized ? this.maximize() : this.minimize(minHeight);
+ },
+
+ /**
+ * Set the toolbox title.
+ * Nothing to do for this host type.
+ */
+ setTitle: function () {},
+
+ /**
+ * Destroy the bottom dock.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ Services.prefs.setIntPref(this.heightPref, this.frame.height);
+ this._nbox.removeChild(this._splitter);
+ this._nbox.removeChild(this.frame);
+ this.frame = null;
+ this._nbox = null;
+ this._splitter = null;
+ }
+
+ return promise.resolve(null);
+ }
+};
+
+/**
+ * Host object for the in-browser sidebar
+ */
+function SidebarHost(hostTab) {
+ this.hostTab = hostTab;
+
+ EventEmitter.decorate(this);
+}
+
+SidebarHost.prototype = {
+ type: "side",
+
+ widthPref: "devtools.toolbox.sidebar.width",
+
+ /**
+ * Create a box in the sidebar of the host tab.
+ */
+ create: function () {
+ let deferred = defer();
+
+ let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
+ let ownerDocument = gBrowser.ownerDocument;
+ this._sidebar = gBrowser.getSidebarContainer(this.hostTab.linkedBrowser);
+
+ this._splitter = ownerDocument.createElement("splitter");
+ this._splitter.setAttribute("class", "devtools-side-splitter");
+
+ this.frame = ownerDocument.createElement("iframe");
+ this.frame.className = "devtools-toolbox-side-iframe";
+
+ this.frame.width = Math.min(
+ Services.prefs.getIntPref(this.widthPref),
+ this._sidebar.clientWidth - MIN_PAGE_SIZE
+ );
+
+ this._sidebar.appendChild(this._splitter);
+ this._sidebar.appendChild(this.frame);
+
+ let frameLoad = () => {
+ this.emit("ready", this.frame);
+ deferred.resolve(this.frame);
+ };
+
+ this.frame.tooltip = "aHTMLTooltip";
+ this.frame.setAttribute("src", "about:blank");
+
+ let domHelper = new DOMHelpers(this.frame.contentWindow);
+ domHelper.onceDOMReady(frameLoad);
+
+ focusTab(this.hostTab);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function () {
+ focusTab(this.hostTab);
+ },
+
+ /**
+ * Set the toolbox title.
+ * Nothing to do for this host type.
+ */
+ setTitle: function () {},
+
+ /**
+ * Destroy the sidebar.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ Services.prefs.setIntPref(this.widthPref, this.frame.width);
+ this._sidebar.removeChild(this._splitter);
+ this._sidebar.removeChild(this.frame);
+ }
+
+ return promise.resolve(null);
+ }
+};
+
+/**
+ * Host object for the toolbox in a separate window
+ */
+function WindowHost() {
+ this._boundUnload = this._boundUnload.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+WindowHost.prototype = {
+ type: "window",
+
+ WINDOW_URL: "chrome://devtools/content/framework/toolbox-window.xul",
+
+ /**
+ * Create a new xul window to contain the toolbox.
+ */
+ create: function () {
+ let deferred = defer();
+
+ let flags = "chrome,centerscreen,resizable,dialog=no";
+ let win = Services.ww.openWindow(null, this.WINDOW_URL, "_blank",
+ flags, null);
+
+ let frameLoad = () => {
+ win.removeEventListener("load", frameLoad, true);
+ win.focus();
+
+ let key;
+ if (system.constants.platform === "macosx") {
+ key = win.document.getElementById("toolbox-key-toggle-osx");
+ } else {
+ key = win.document.getElementById("toolbox-key-toggle");
+ }
+ key.removeAttribute("disabled");
+
+ this.frame = win.document.getElementById("toolbox-iframe");
+ this.emit("ready", this.frame);
+
+ deferred.resolve(this.frame);
+ };
+
+ win.addEventListener("load", frameLoad, true);
+ win.addEventListener("unload", this._boundUnload);
+
+ this._window = win;
+
+ return deferred.promise;
+ },
+
+ /**
+ * Catch the user closing the window.
+ */
+ _boundUnload: function (event) {
+ if (event.target.location != this.WINDOW_URL) {
+ return;
+ }
+ this._window.removeEventListener("unload", this._boundUnload);
+
+ this.emit("window-closed");
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function () {
+ this._window.focus();
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function (title) {
+ this._window.document.title = title;
+ },
+
+ /**
+ * Destroy the window.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+
+ this._window.removeEventListener("unload", this._boundUnload);
+ this._window.close();
+ }
+
+ return promise.resolve(null);
+ }
+};
+
+/**
+ * Host object for the toolbox in its own tab
+ */
+function CustomHost(hostTab, options) {
+ this.frame = options.customIframe;
+ this.uid = options.uid;
+ EventEmitter.decorate(this);
+}
+
+CustomHost.prototype = {
+ type: "custom",
+
+ _sendMessageToTopWindow: function (msg, data) {
+ // It's up to the custom frame owner (parent window) to honor
+ // "close" or "raise" instructions.
+ let topWindow = this.frame.ownerDocument.defaultView;
+ if (!topWindow) {
+ return;
+ }
+ let json = {name: "toolbox-" + msg, uid: this.uid};
+ if (data) {
+ json.data = data;
+ }
+ topWindow.postMessage(JSON.stringify(json), "*");
+ },
+
+ /**
+ * Create a new xul window to contain the toolbox.
+ */
+ create: function () {
+ return promise.resolve(this.frame);
+ },
+
+ /**
+ * Raise the host.
+ */
+ raise: function () {
+ this._sendMessageToTopWindow("raise");
+ },
+
+ /**
+ * Set the toolbox title.
+ */
+ setTitle: function (title) {
+ this._sendMessageToTopWindow("title", { value: title });
+ },
+
+ /**
+ * Destroy the window.
+ */
+ destroy: function () {
+ if (!this._destroyed) {
+ this._destroyed = true;
+ this._sendMessageToTopWindow("close");
+ }
+ return promise.resolve(null);
+ }
+};
+
+/**
+ * Switch to the given tab in a browser and focus the browser window
+ */
+function focusTab(tab) {
+ let browserWindow = tab.ownerDocument.defaultView;
+ browserWindow.focus();
+ browserWindow.gBrowser.selectedTab = tab;
+}
diff --git a/devtools/client/framework/toolbox-init.js b/devtools/client/framework/toolbox-init.js
new file mode 100644
index 000000000..cb041c22d
--- /dev/null
+++ b/devtools/client/framework/toolbox-init.js
@@ -0,0 +1,73 @@
+/* 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";
+
+// URL constructor doesn't support about: scheme
+let href = window.location.href.replace("about:", "http://");
+let url = new window.URL(href);
+
+// Only use this method to attach the toolbox if some query parameters are given
+if (url.search.length > 1) {
+ const Cu = Components.utils;
+ const Ci = Components.interfaces;
+ const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ const { gDevTools } = require("devtools/client/framework/devtools");
+ const { targetFromURL } = require("devtools/client/framework/target-from-url");
+ const { Toolbox } = require("devtools/client/framework/toolbox");
+ const { TargetFactory } = require("devtools/client/framework/target");
+ const { DebuggerServer } = require("devtools/server/main");
+ const { DebuggerClient } = require("devtools/shared/client/main");
+ const { Task } = require("devtools/shared/task");
+
+ // `host` is the frame element loading the toolbox.
+ let host = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .containerElement;
+
+ // Specify the default tool to open
+ let tool = url.searchParams.get("tool");
+
+ Task.spawn(function* () {
+ let target;
+ if (url.searchParams.has("target")) {
+ // Attach toolbox to a given browser iframe (<xul:browser> or <html:iframe
+ // mozbrowser>) whose reference is set on the host iframe.
+
+ // `iframe` is the targeted document to debug
+ let iframe = host.wrappedJSObject ? host.wrappedJSObject.target
+ : host.target;
+ // Need to use a xray and query some interfaces to have
+ // attributes and behavior expected by devtools codebase
+ iframe = XPCNativeWrapper(iframe);
+ iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
+
+ if (iframe) {
+ // Fake a xul:tab object as we don't have one.
+ // linkedBrowser is the only one attribute being queried by client.getTab
+ let tab = { linkedBrowser: iframe };
+
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ let client = new DebuggerClient(DebuggerServer.connectPipe());
+
+ yield client.connect();
+ // Creates a target for a given browser iframe.
+ let response = yield client.getTab({ tab });
+ let form = response.tab;
+ target = yield TargetFactory.forRemoteTab({client, form, chrome: false});
+ } else {
+ alert("Unable to find the targetted iframe to debug");
+ }
+ } else {
+ target = yield targetFromURL(url);
+ }
+ let options = { customIframe: host };
+ yield gDevTools.showToolbox(target, tool, Toolbox.HostType.CUSTOM, options);
+ }).catch(error => {
+ console.error("Exception while loading the toolbox", error);
+ });
+}
diff --git a/devtools/client/framework/toolbox-options.js b/devtools/client/framework/toolbox-options.js
new file mode 100644
index 000000000..6362d98dd
--- /dev/null
+++ b/devtools/client/framework/toolbox-options.js
@@ -0,0 +1,431 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+const defer = require("devtools/shared/defer");
+const {Task} = require("devtools/shared/task");
+const {gDevTools} = require("devtools/client/framework/devtools");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+exports.OptionsPanel = OptionsPanel;
+
+function GetPref(name) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.getCharPref(name);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.getIntPref(name);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.getBoolPref(name);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function SetPref(name, value) {
+ let type = Services.prefs.getPrefType(name);
+ switch (type) {
+ case Services.prefs.PREF_STRING:
+ return Services.prefs.setCharPref(name, value);
+ case Services.prefs.PREF_INT:
+ return Services.prefs.setIntPref(name, value);
+ case Services.prefs.PREF_BOOL:
+ return Services.prefs.setBoolPref(name, value);
+ default:
+ throw new Error("Unknown type");
+ }
+}
+
+function InfallibleGetBoolPref(key) {
+ try {
+ return Services.prefs.getBoolPref(key);
+ } catch (ex) {
+ return true;
+ }
+}
+
+/**
+ * Represents the Options Panel in the Toolbox.
+ */
+function OptionsPanel(iframeWindow, toolbox) {
+ this.panelDoc = iframeWindow.document;
+ 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._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);
+}
+
+OptionsPanel.prototype = {
+
+ get target() {
+ return this.toolbox.target;
+ },
+
+ open: Task.async(function* () {
+ // For local debugging we need to make the target remote.
+ if (!this.target.isRemote) {
+ yield this.target.makeRemote();
+ }
+
+ this.setupToolsList();
+ this.setupToolbarButtonsList();
+ this.setupThemeList();
+ yield this.populatePreferences();
+ this.isReady = true;
+ this.emit("ready");
+ return this;
+ }),
+
+ _addListeners: function () {
+ Services.prefs.addObserver("devtools.cache.disabled", this._prefChanged, false);
+ Services.prefs.addObserver("devtools.theme", this._prefChanged, false);
+ gDevTools.on("theme-registered", this._themeRegistered);
+ gDevTools.on("theme-unregistered", this._themeUnregistered);
+ },
+
+ _removeListeners: function () {
+ Services.prefs.removeObserver("devtools.cache.disabled", this._prefChanged);
+ Services.prefs.removeObserver("devtools.theme", this._prefChanged);
+ gDevTools.off("theme-registered", this._themeRegistered);
+ gDevTools.off("theme-unregistered", this._themeUnregistered);
+ },
+
+ _prefChanged: function (subject, topic, prefName) {
+ if (prefName === "devtools.cache.disabled") {
+ let cacheDisabled = data.newValue;
+ let cbx = this.panelDoc.getElementById("devtools-disable-cache");
+
+ cbx.checked = cacheDisabled;
+ } else if (prefName === "devtools.theme") {
+ this.updateCurrentTheme();
+ }
+ },
+
+ _themeRegistered: function (event, themeId) {
+ this.setupThemeList();
+ },
+
+ _themeUnregistered: function (event, theme) {
+ let themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ let themeInput = themeBox.querySelector(`[value=${theme.id}]`);
+
+ if (themeInput) {
+ themeInput.parentNode.remove();
+ }
+ },
+
+ setupToolbarButtonsList: function () {
+ let enabledToolbarButtonsBox = this.panelDoc.getElementById(
+ "enabled-toolbox-buttons-box");
+
+ let toggleableButtons = this.toolbox.toolboxButtons;
+ let setToolboxButtonsVisibility =
+ this.toolbox.setToolboxButtonsVisibility.bind(this.toolbox);
+
+ let onCheckboxClick = (checkbox) => {
+ let toolDefinition = toggleableButtons.filter(
+ toggleableButton => toggleableButton.id === checkbox.id)[0];
+ Services.prefs.setBoolPref(
+ toolDefinition.visibilityswitch, checkbox.checked);
+ setToolboxButtonsVisibility();
+ };
+
+ let createCommandCheckbox = tool => {
+ let checkboxLabel = this.panelDoc.createElement("label");
+ let checkboxSpanLabel = this.panelDoc.createElement("span");
+ checkboxSpanLabel.textContent = tool.label;
+ let checkboxInput = this.panelDoc.createElement("input");
+ checkboxInput.setAttribute("type", "checkbox");
+ checkboxInput.setAttribute("id", tool.id);
+ if (InfallibleGetBoolPref(tool.visibilityswitch)) {
+ checkboxInput.setAttribute("checked", true);
+ }
+ checkboxInput.addEventListener("change",
+ onCheckboxClick.bind(this, checkboxInput));
+
+ checkboxLabel.appendChild(checkboxInput);
+ checkboxLabel.appendChild(checkboxSpanLabel);
+ return checkboxLabel;
+ };
+
+ for (let tool of toggleableButtons) {
+ if (!tool.isTargetSupported(this.toolbox.target)) {
+ continue;
+ }
+
+ enabledToolbarButtonsBox.appendChild(createCommandCheckbox(tool));
+ }
+ },
+
+ setupToolsList: function () {
+ let defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
+ let additionalToolsBox = this.panelDoc.getElementById(
+ "additional-tools-box");
+ let toolsNotSupportedLabel = this.panelDoc.getElementById(
+ "tools-not-supported-label");
+ let atleastOneToolNotSupported = false;
+
+ let onCheckboxClick = function (id) {
+ let toolDefinition = gDevTools._tools.get(id);
+ // Set the kill switch pref boolean to true
+ Services.prefs.setBoolPref(toolDefinition.visibilityswitch, this.checked);
+ if (this.checked) {
+ gDevTools.emit("tool-registered", id);
+ } else {
+ gDevTools.emit("tool-unregistered", toolDefinition);
+ }
+ };
+
+ 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)) {
+ checkboxSpanLabel.textContent = tool.label;
+ } else {
+ atleastOneToolNotSupported = true;
+ checkboxSpanLabel.textContent =
+ L10N.getFormatStr("options.toolNotSupportedMarker", tool.label);
+ 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));
+
+ checkboxLabel.appendChild(checkboxInput);
+ checkboxLabel.appendChild(checkboxSpanLabel);
+ return checkboxLabel;
+ };
+
+ // 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.
+ let atleastOneAddon = false;
+ for (let tool of gDevTools.getAdditionalTools()) {
+ atleastOneAddon = true;
+ additionalToolsBox.appendChild(createToolCheckbox(tool));
+ }
+
+ if (!atleastOneAddon) {
+ additionalToolsBox.style.display = "none";
+ }
+
+ if (!atleastOneToolNotSupported) {
+ toolsNotSupportedLabel.style.display = "none";
+ }
+
+ this.panelWin.focus();
+ },
+
+ setupThemeList: function () {
+ let themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ let themeLabels = themeBox.querySelectorAll("label");
+ for (let label of themeLabels) {
+ label.remove();
+ }
+
+ let createThemeOption = theme => {
+ let inputLabel = this.panelDoc.createElement("label");
+ let inputRadio = this.panelDoc.createElement("input");
+ inputRadio.setAttribute("type", "radio");
+ inputRadio.setAttribute("value", theme.id);
+ inputRadio.setAttribute("name", "devtools-theme-item");
+ inputRadio.addEventListener("change", function (e) {
+ setPrefAndEmit(themeBox.getAttribute("data-pref"),
+ e.target.value);
+ });
+
+ let inputSpanLabel = this.panelDoc.createElement("span");
+ inputSpanLabel.textContent = theme.label;
+ inputLabel.appendChild(inputRadio);
+ inputLabel.appendChild(inputSpanLabel);
+
+ return inputLabel;
+ };
+
+ // Populating the default theme list
+ let themes = gDevTools.getThemeDefinitionArray();
+ for (let theme of themes) {
+ themeBox.appendChild(createThemeOption(theme));
+ }
+
+ this.updateCurrentTheme();
+ },
+
+ populatePreferences: function () {
+ let prefCheckboxes = this.panelDoc.querySelectorAll(
+ "input[type=checkbox][data-pref]");
+ for (let prefCheckbox of prefCheckboxes) {
+ if (GetPref(prefCheckbox.getAttribute("data-pref"))) {
+ prefCheckbox.setAttribute("checked", true);
+ }
+ prefCheckbox.addEventListener("change", function (e) {
+ let checkbox = e.target;
+ setPrefAndEmit(checkbox.getAttribute("data-pref"), checkbox.checked);
+ });
+ }
+ // Themes radio inputs are handled in setupThemeList
+ let prefRadiogroups = this.panelDoc.querySelectorAll(
+ ".radiogroup[data-pref]:not(#devtools-theme-box)");
+ for (let radioGroup of prefRadiogroups) {
+ let selectedValue = GetPref(radioGroup.getAttribute("data-pref"));
+
+ for (let radioInput of radioGroup.querySelectorAll("input[type=radio]")) {
+ if (radioInput.getAttribute("value") == selectedValue) {
+ radioInput.setAttribute("checked", true);
+ }
+
+ radioInput.addEventListener("change", function (e) {
+ setPrefAndEmit(radioGroup.getAttribute("data-pref"),
+ e.target.value);
+ });
+ }
+ }
+ let prefSelects = this.panelDoc.querySelectorAll("select[data-pref]");
+ for (let prefSelect of prefSelects) {
+ let pref = GetPref(prefSelect.getAttribute("data-pref"));
+ let options = [...prefSelect.options];
+ options.some(function (option) {
+ let value = option.value;
+ // non strict check to allow int values.
+ if (value == pref) {
+ prefSelect.selectedIndex = options.indexOf(option);
+ return true;
+ }
+ });
+
+ prefSelect.addEventListener("change", function (e) {
+ let select = e.target;
+ setPrefAndEmit(select.getAttribute("data-pref"),
+ select.options[select.selectedIndex].value);
+ });
+ }
+
+ if (this.target.activeTab) {
+ return this.target.client.attachTab(this.target.activeTab._actor)
+ .then(([response, client]) => {
+ this._origJavascriptEnabled = !response.javascriptEnabled;
+ this.disableJSNode.checked = this._origJavascriptEnabled;
+ this.disableJSNode.addEventListener("click",
+ this._disableJSClicked, false);
+ });
+ }
+ this.disableJSNode.hidden = true;
+ },
+
+ updateCurrentTheme: function () {
+ let currentTheme = GetPref("devtools.theme");
+ let themeBox = this.panelDoc.getElementById("devtools-theme-box");
+ let themeRadioInput = themeBox.querySelector(`[value=${currentTheme}]`);
+
+ if (themeRadioInput) {
+ themeRadioInput.checked = true;
+ } else {
+ // If the current theme does not exist anymore, switch to light theme
+ let lightThemeInputRadio = themeBox.querySelector("[value=light]");
+ lightThemeInputRadio.checked = true;
+ }
+ },
+
+ /**
+ * Disables JavaScript for the currently loaded tab. We force a page refresh
+ * here because setting docShell.allowJavascript to true fails to block JS
+ * execution from event listeners added using addEventListener(), AJAX calls
+ * and timers. The page refresh prevents these things from being added in the
+ * first place.
+ *
+ * @param {Event} event
+ * The event sent by checking / unchecking the disable JS checkbox.
+ */
+ _disableJSClicked: function (event) {
+ let checked = event.target.checked;
+
+ let options = {
+ "javascriptEnabled": !checked
+ };
+
+ this.target.activeTab.reconfigure(options);
+ },
+
+ destroy: function () {
+ if (this.destroyPromise) {
+ return this.destroyPromise;
+ }
+
+ let deferred = defer();
+ this.destroyPromise = deferred.promise;
+
+ this._removeListeners();
+
+ if (this.target.activeTab) {
+ this.disableJSNode.removeEventListener("click", this._disableJSClicked);
+ // FF41+ automatically cleans up state in actor on disconnect
+ if (!this.target.activeTab.traits.noTabReconfigureOnClose) {
+ let options = {
+ "javascriptEnabled": this._origJavascriptEnabled,
+ "performReload": false
+ };
+ this.target.activeTab.reconfigure(options, deferred.resolve);
+ } else {
+ deferred.resolve();
+ }
+ } else {
+ deferred.resolve();
+ }
+
+ this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null;
+
+ return this.destroyPromise;
+ }
+};
+
+/* Set a pref and emit the pref-changed event if needed. */
+function setPrefAndEmit(prefName, newValue) {
+ let data = {
+ pref: prefName,
+ newValue: newValue
+ };
+ data.oldValue = GetPref(data.pref);
+ SetPref(data.pref, data.newValue);
+
+ if (data.newValue != data.oldValue) {
+ gDevTools.emit("pref-changed", data);
+ }
+}
diff --git a/devtools/client/framework/toolbox-options.xhtml b/devtools/client/framework/toolbox-options.xhtml
new file mode 100644
index 000000000..372a588ab
--- /dev/null
+++ b/devtools/client/framework/toolbox-options.xhtml
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE html [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>Toolbox option</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="stylesheet" href="chrome://devtools/content/framework/options-panel.css" type="text/css"/>
+ <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
+ </head>
+ <body role="application" class="theme-body">
+ <form id="options-panel">
+ <div id="tools-box" class="options-vertical-pane">
+ <fieldset id="default-tools-box" class="options-groupbox">
+ <legend>&options.selectDefaultTools.label2;</legend>
+ </fieldset>
+
+ <fieldset id="additional-tools-box" class="options-groupbox">
+ <legend>&options.selectAdditionalTools.label;</legend>
+ </fieldset>
+
+ <fieldset id="enabled-toolbox-buttons-box" class="options-groupbox">
+ <legend>&options.selectEnabledToolboxButtons.label;</legend>
+ <span id="tools-not-supported-label"
+ class="options-citation-label theme-comment">
+ &options.toolNotSupported.label;</span>
+ </fieldset>
+ </div>
+
+ <div class="options-vertical-pane">
+ <fieldset id="devtools-theme-box"
+ class="options-groupbox
+ horizontal-options-groupbox
+ radiogroup"
+ data-pref="devtools.theme">
+ <legend>&options.selectDevToolsTheme.label2;</legend>
+ </fieldset>
+
+ <fieldset id="commonprefs-options" class="options-groupbox">
+ <legend>&options.commonPrefs.label;</legend>
+ <label title="&options.enablePersistentLogs.tooltip;">
+ <input type="checkbox" data-pref="devtools.webconsole.persistlog" />
+ <span>&options.enablePersistentLogs.label;</span>
+ </label>
+ </fieldset>
+
+ <fieldset id="inspector-options" class="options-groupbox">
+ <legend>&options.context.inspector;</legend>
+ <label title="&options.showUserAgentStyles.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.inspector.showUserAgentStyles"/>
+ <span>&options.showUserAgentStyles.label;</span>
+ </label>
+ <label title="&options.collapseAttrs.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.markup.collapseAttributes"/>
+ <span>&options.collapseAttrs.label;</span>
+ </label>
+ <label>
+ <span>&options.defaultColorUnit.label;</span>
+ <select id="defaultColorUnitMenuList"
+ data-pref="devtools.defaultColorUnit">
+ <option value="authored">&options.defaultColorUnit.authored;</option>
+ <option value="hex">&options.defaultColorUnit.hex;</option>
+ <option value="hsl">&options.defaultColorUnit.hsl;</option>
+ <option value="rgb">&options.defaultColorUnit.rgb;</option>
+ <option value="name">&options.defaultColorUnit.name;</option>
+ </select>
+ </label>
+ </fieldset>
+
+ <fieldset id="webconsole-options" class="options-groupbox">
+ <legend>&options.webconsole.label;</legend>
+ <label title="&options.timestampMessages.tooltip;">
+ <input type="checkbox"
+ id="webconsole-timestamp-messages"
+ data-pref="devtools.webconsole.timestampMessages"/>
+ <span>&options.timestampMessages.label;</span>
+ </label>
+ </fieldset>
+
+ <fieldset id="debugger-options" class="options-groupbox">
+ <legend>&options.debugger.label;</legend>
+ <label title="&options.sourceMaps.tooltip;">
+ <input type="checkbox"
+ id="debugger-sourcemaps"
+ data-pref="devtools.debugger.client-source-maps-enabled"/>
+ <span>&options.sourceMaps.label;</span>
+ </label>
+ </fieldset>
+
+ <fieldset id="styleeditor-options" class="options-groupbox">
+ <legend>&options.styleeditor.label;</legend>
+ <label title="&options.stylesheetSourceMaps.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.styleeditor.source-maps-enabled"/>
+ <span>&options.stylesheetSourceMaps.label;</span>
+ </label>
+ <label title="&options.stylesheetAutocompletion.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.styleeditor.autocompletion-enabled"/>
+ <span>&options.stylesheetAutocompletion.label;</span>
+ </label>
+ </fieldset>
+ </div>
+
+ <div class="options-vertical-pane">
+ <fieldset id="sourceeditor-options" class="options-groupbox">
+ <legend>&options.sourceeditor.label;</legend>
+ <label title="&options.sourceeditor.detectindentation.tooltip;">
+ <input type="checkbox"
+ id="devtools-sourceeditor-detectindentation"
+ data-pref="devtools.editor.detectindentation"/>
+ <span>&options.sourceeditor.detectindentation.label;</span>
+ </label>
+ <label title="&options.sourceeditor.autoclosebrackets.tooltip;">
+ <input type="checkbox"
+ id="devtools-sourceeditor-autoclosebrackets"
+ data-pref="devtools.editor.autoclosebrackets"/>
+ <span>&options.sourceeditor.autoclosebrackets.label;</span>
+ </label>
+ <label title="&options.sourceeditor.expandtab.tooltip;">
+ <input type="checkbox"
+ id="devtools-sourceeditor-expandtab"
+ data-pref="devtools.editor.expandtab"/>
+ <span>&options.sourceeditor.expandtab.label;</span>
+ </label>
+ <label>
+ <span>&options.sourceeditor.tabsize.label;</span>
+ <select id="devtools-sourceeditor-tabsize-select"
+ data-pref="devtools.editor.tabsize">
+ <option label="2">2</option>
+ <option label="4">4</option>
+ <option label="8">8</option>
+ </select>
+ </label>
+ <label>
+ <span>&options.sourceeditor.keybinding.label;</span>
+ <select id="devtools-sourceeditor-keybinding-select"
+ data-pref="devtools.editor.keymap">
+ <option value="default">&options.sourceeditor.keybinding.default.label;</option>
+ <option value="vim">Vim</option>
+ <option value="emacs">Emacs</option>
+ <option value="sublime">Sublime Text</option>
+ </select>
+ </label>
+ </fieldset>
+
+ <fieldset id="context-options" class="options-groupbox">
+ <legend>&options.context.advancedSettings;</legend>
+ <label title="&options.showPlatformData.tooltip;">
+ <input type="checkbox"
+ id="devtools-show-gecko-data"
+ data-pref="devtools.performance.ui.show-platform-data"/>
+ <span>&options.showPlatformData.label;</span>
+ </label>
+ <label title="&options.disableHTTPCache.tooltip;">
+ <input type="checkbox"
+ id="devtools-disable-cache"
+ data-pref="devtools.cache.disabled"/>
+ <span>&options.disableHTTPCache.label;</span>
+ </label>
+ <label title="&options.disableJavaScript.tooltip;">
+ <input type="checkbox"
+ id="devtools-disable-javascript"/>
+ <span>&options.disableJavaScript.label;</span>
+ </label>
+ <label title="&options.enableServiceWorkersHTTP.tooltip;">
+ <input type="checkbox"
+ id="devtools-enable-serviceWorkersTesting"
+ data-pref="devtools.serviceWorkers.testing.enabled"/>
+ <span>&options.enableServiceWorkersHTTP.label;</span>
+ </label>
+ <label title="&options.enableChrome.tooltip3;">
+ <input type="checkbox"
+ data-pref="devtools.chrome.enabled"/>
+ <span>&options.enableChrome.label5;</span>
+ </label>
+ <label title="&options.enableRemote.tooltip2;">
+ <input type="checkbox"
+ data-pref="devtools.debugger.remote-enabled"/>
+ <span>&options.enableRemote.label3;</span>
+ </label>
+ <label title="&options.enableWorkers.tooltip;">
+ <input type="checkbox"
+ data-pref="devtools.debugger.workers"/>
+ <span>&options.enableWorkers.label;</span>
+ </label>
+ <span class="options-citation-label theme-comment"
+ >&options.context.triggersPageRefresh;</span>
+ </fieldset>
+ </div>
+
+ </form>
+ </body>
+</html>
diff --git a/devtools/client/framework/toolbox-process-window.js b/devtools/client/framework/toolbox-process-window.js
new file mode 100644
index 000000000..8ead718b3
--- /dev/null
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -0,0 +1,230 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+// Require this module to setup core modules
+loader.require("devtools/client/framework/devtools-browser");
+
+var { gDevTools } = require("devtools/client/framework/devtools");
+var { TargetFactory } = require("devtools/client/framework/target");
+var { Toolbox } = require("devtools/client/framework/toolbox");
+var Services = require("Services");
+var { DebuggerClient } = require("devtools/shared/client/main");
+var { PrefsHelper } = require("devtools/client/shared/prefs");
+var { Task } = require("devtools/shared/task");
+
+/**
+ * Shortcuts for accessing various debugger preferences.
+ */
+var Prefs = new PrefsHelper("devtools.debugger", {
+ chromeDebuggingHost: ["Char", "chrome-debugging-host"],
+ chromeDebuggingPort: ["Int", "chrome-debugging-port"],
+ chromeDebuggingWebSocket: ["Bool", "chrome-debugging-websocket"],
+});
+
+var gToolbox, gClient;
+
+var connect = Task.async(function*() {
+ window.removeEventListener("load", connect);
+ // Initiate the connection
+ let transport = yield DebuggerClient.socketConnect({
+ host: Prefs.chromeDebuggingHost,
+ port: Prefs.chromeDebuggingPort,
+ webSocket: Prefs.chromeDebuggingWebSocket,
+ });
+ gClient = new DebuggerClient(transport);
+ yield gClient.connect();
+ let addonID = getParameterByName("addonID");
+
+ if (addonID) {
+ let { addons } = yield gClient.listAddons();
+ let addonActor = addons.filter(addon => addon.id === addonID).pop();
+ openToolbox({
+ form: addonActor,
+ chrome: true,
+ isTabActor: addonActor.isWebExtension ? true : false
+ });
+ } else {
+ let response = yield gClient.getProcess();
+ openToolbox({
+ form: response.form,
+ chrome: true
+ });
+ }
+});
+
+// Certain options should be toggled since we can assume chrome debugging here
+function setPrefDefaults() {
+ Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
+ Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true);
+ Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
+ Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
+ Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
+ Services.prefs.setBoolPref("devtools.scratchpad.enabled", true);
+ // Bug 1225160 - Using source maps with browser debugging can lead to a crash
+ Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
+ Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
+ Services.prefs.setBoolPref("devtools.debugger.client-source-maps-enabled", true);
+}
+
+window.addEventListener("load", function() {
+ let cmdClose = document.getElementById("toolbox-cmd-close");
+ cmdClose.addEventListener("command", onCloseCommand);
+ setPrefDefaults();
+ connect().catch(e => {
+ let errorMessageContainer = document.getElementById("error-message-container");
+ let errorMessage = document.getElementById("error-message");
+ errorMessage.value = e.message || e;
+ errorMessageContainer.hidden = false;
+ console.error(e);
+ });
+});
+
+function onCloseCommand(event) {
+ window.close();
+}
+
+function openToolbox({ form, chrome, isTabActor }) {
+ let options = {
+ form: form,
+ client: gClient,
+ chrome: chrome,
+ isTabActor: isTabActor
+ };
+ TargetFactory.forRemoteTab(options).then(target => {
+ let frame = document.getElementById("toolbox-iframe");
+ let selectedTool = "jsdebugger";
+
+ try {
+ // Remember the last panel that was used inside of this profile.
+ selectedTool = Services.prefs.getCharPref("devtools.toolbox.selectedTool");
+ } catch(e) {}
+
+ try {
+ // But if we are testing, then it should always open the debugger panel.
+ selectedTool = Services.prefs.getCharPref("devtools.browsertoolbox.panel");
+ } catch(e) {}
+
+ let options = { customIframe: frame };
+ gDevTools.showToolbox(target,
+ selectedTool,
+ Toolbox.HostType.CUSTOM,
+ options)
+ .then(onNewToolbox);
+ });
+}
+
+function onNewToolbox(toolbox) {
+ gToolbox = toolbox;
+ bindToolboxHandlers();
+ raise();
+ let env = Components.classes["@mozilla.org/process/environment;1"].getService(Components.interfaces.nsIEnvironment);
+ let testScript = env.get("MOZ_TOOLBOX_TEST_SCRIPT");
+ if (testScript) {
+ // Only allow executing random chrome scripts when a special
+ // test-only pref is set
+ let prefName = "devtools.browser-toolbox.allow-unsafe-script";
+ if (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_BOOL &&
+ Services.prefs.getBoolPref(prefName) === true) {
+ evaluateTestScript(testScript, toolbox);
+ }
+ }
+}
+
+function evaluateTestScript(script, toolbox) {
+ let sandbox = Cu.Sandbox(window);
+ sandbox.window = window;
+ sandbox.toolbox = toolbox;
+ Cu.evalInSandbox(script, sandbox);
+}
+
+function bindToolboxHandlers() {
+ gToolbox.once("destroyed", quitApp);
+ window.addEventListener("unload", onUnload);
+
+#ifdef XP_MACOSX
+ // Badge the dock icon to differentiate this process from the main application process.
+ updateBadgeText(false);
+
+ // Once the debugger panel opens listen for thread pause / resume.
+ gToolbox.getPanelWhenReady("jsdebugger").then(panel => {
+ setupThreadListeners(panel);
+ });
+#endif
+}
+
+function setupThreadListeners(panel) {
+ updateBadgeText(panel._controller.activeThread.state == "paused");
+
+ let onPaused = updateBadgeText.bind(null, true);
+ let onResumed = updateBadgeText.bind(null, false);
+ panel.target.on("thread-paused", onPaused);
+ panel.target.on("thread-resumed", onResumed);
+
+ panel.once("destroyed", () => {
+ panel.off("thread-paused", onPaused);
+ panel.off("thread-resumed", onResumed);
+ });
+}
+
+function updateBadgeText(paused) {
+ let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"].getService(Ci.nsIMacDockSupport);
+ dockSupport.badgeText = paused ? "▐▐ " : " ▶";
+}
+
+function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ window.removeEventListener("message", onMessage);
+ let cmdClose = document.getElementById("toolbox-cmd-close");
+ cmdClose.removeEventListener("command", onCloseCommand);
+ gToolbox.destroy();
+}
+
+function onMessage(event) {
+ try {
+ let json = JSON.parse(event.data);
+ switch (json.name) {
+ case "toolbox-raise":
+ raise();
+ break;
+ case "toolbox-title":
+ setTitle(json.data.value);
+ break;
+ }
+ } catch(e) { console.error(e); }
+}
+
+window.addEventListener("message", onMessage);
+
+function raise() {
+ window.focus();
+}
+
+function setTitle(title) {
+ document.title = title;
+}
+
+function quitApp() {
+ let quit = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(quit, "quit-application-requested", null);
+
+ let shouldProceed = !quit.data;
+ if (shouldProceed) {
+ Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
+ }
+}
+
+function getParameterByName (name) {
+ name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
+ let regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
+ let results = regex.exec(window.location.search);
+ return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
+}
diff --git a/devtools/client/framework/toolbox-process-window.xul b/devtools/client/framework/toolbox-process-window.xul
new file mode 100644
index 000000000..d2f8a741b
--- /dev/null
+++ b/devtools/client/framework/toolbox-process-window.xul
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="devtools-toolbox-window"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ windowtype="devtools:toolbox"
+ width="900" height="600"
+ persist="screenX screenY width height sizemode">
+
+ <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="text/javascript" src="toolbox-process-window.js"/>
+ <script type="text/javascript" src="chrome://global/content/viewSourceUtils.js"/>
+ <script type="text/javascript" src="chrome://browser/content/utilityOverlay.js"/>
+
+ <commandset id="toolbox-commandset">
+ <command id="toolbox-cmd-close"/>
+ </commandset>
+
+ <keyset id="toolbox-keyset">
+ <key id="toolbox-key-close"
+ key="&closeCmd.key;"
+ command="toolbox-cmd-close"
+ modifiers="accel"/>
+ </keyset>
+
+ <!-- This will be used by the Web Console to hold any popups it may create,
+ for example when viewing network request details. -->
+ <popupset id="mainPopupSet"></popupset>
+
+ <vbox id="error-message-container" hidden="true" flex="1">
+ <box>&browserToolboxErrorMessage;</box>
+ <textbox multiline="true" id="error-message" flex="1"></textbox>
+ </vbox>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <iframe id="toolbox-iframe" flex="1" tooltip="aHTMLTooltip"></iframe>
+</window>
diff --git a/devtools/client/framework/toolbox-window.xul b/devtools/client/framework/toolbox-window.xul
new file mode 100644
index 000000000..cd14a3597
--- /dev/null
+++ b/devtools/client/framework/toolbox-window.xul
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+ %toolboxDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="devtools-toolbox-window"
+ macanimationtype="document"
+ fullscreenbutton="true"
+ windowtype="devtools:toolbox"
+ width="900" height="320"
+ persist="screenX screenY width height sizemode">
+
+ <commandset id="toolbox-commandset">
+ <command id="toolbox-cmd-close" oncommand="window.close();"/>
+ </commandset>
+
+ <keyset id="toolbox-keyset">
+ <key id="toolbox-key-close"
+ key="&closeCmd.key;"
+ command="toolbox-cmd-close"
+ modifiers="accel"/>
+ <key id="toolbox-key-toggle"
+ key="&toggleToolbox.key;"
+ command="toolbox-cmd-close"
+ modifiers="accel,shift"
+ disabled="true"/>
+ <key id="toolbox-key-toggle-osx"
+ key="&toggleToolbox.key;"
+ command="toolbox-cmd-close"
+ modifiers="accel,alt"
+ disabled="true"/>
+ <key id="toolbox-key-toggle-F12"
+ keycode="&toggleToolboxF12.keycode;"
+ keytext="&toggleToolboxF12.keytext;"
+ command="toolbox-cmd-close"/>
+ </keyset>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <iframe id="toolbox-iframe" flex="1" forceOwnRefreshDriver="" tooltip="aHTMLTooltip"></iframe>
+</window>
diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js
new file mode 100644
index 000000000..82d5d2915
--- /dev/null
+++ b/devtools/client/framework/toolbox.js
@@ -0,0 +1,2417 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const MAX_ORDINAL = 99;
+const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
+const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
+const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
+const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
+const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
+const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const { SourceMapService } = require("./source-map-service");
+
+var {Ci, Cu} = require("chrome");
+var promise = require("promise");
+var defer = require("devtools/shared/defer");
+var Services = require("Services");
+var {Task} = require("devtools/shared/task");
+var {gDevTools} = require("devtools/client/framework/devtools");
+var EventEmitter = require("devtools/shared/event-emitter");
+var Telemetry = require("devtools/client/shared/telemetry");
+var HUDService = require("devtools/client/webconsole/hudservice");
+var viewSource = require("devtools/client/shared/view-source");
+var { attachThread, detachThread } = require("./attach-thread");
+var Menu = require("devtools/client/framework/menu");
+var MenuItem = require("devtools/client/framework/menu-item");
+var { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+
+const { BrowserLoader } =
+ Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
+
+loader.lazyRequireGetter(this, "CommandUtils",
+ "devtools/client/shared/developer-toolbar", true);
+loader.lazyRequireGetter(this, "getHighlighterUtils",
+ "devtools/client/framework/toolbox-highlighter-utils", true);
+loader.lazyRequireGetter(this, "Selection",
+ "devtools/client/framework/selection", true);
+loader.lazyRequireGetter(this, "InspectorFront",
+ "devtools/shared/fronts/inspector", true);
+loader.lazyRequireGetter(this, "flags",
+ "devtools/shared/flags");
+loader.lazyRequireGetter(this, "showDoorhanger",
+ "devtools/client/shared/doorhanger", true);
+loader.lazyRequireGetter(this, "createPerformanceFront",
+ "devtools/shared/fronts/performance", true);
+loader.lazyRequireGetter(this, "system",
+ "devtools/shared/system");
+loader.lazyRequireGetter(this, "getPreferenceFront",
+ "devtools/shared/fronts/preference", true);
+loader.lazyRequireGetter(this, "KeyShortcuts",
+ "devtools/client/shared/key-shortcuts", true);
+loader.lazyRequireGetter(this, "ZoomKeys",
+ "devtools/client/shared/zoom-keys");
+loader.lazyRequireGetter(this, "settleAll",
+ "devtools/shared/ThreadSafeDevToolsUtils", true);
+loader.lazyRequireGetter(this, "ToolboxButtons",
+ "devtools/client/definitions", true);
+
+loader.lazyGetter(this, "registerHarOverlay", () => {
+ return require("devtools/client/netmonitor/har/toolbox-overlay").register;
+});
+
+/**
+ * A "Toolbox" is the component that holds all the tools for one specific
+ * target. Visually, it's a document that includes the tools tabs and all
+ * the iframes where the tool panels will be living in.
+ *
+ * @param {object} target
+ * The object the toolbox is debugging.
+ * @param {string} selectedTool
+ * Tool to select initially
+ * @param {Toolbox.HostType} hostType
+ * Type of host that will host the toolbox (e.g. sidebar, window)
+ * @param {DOMWindow} contentWindow
+ * The window object of the toolbox document
+ * @param {string} frameId
+ * 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;
+
+ this._toolPanels = new Map();
+ this._telemetry = new Telemetry();
+ if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) {
+ this._sourceMapService = new SourceMapService(this._target);
+ }
+
+ this._initInspector = null;
+ this._inspector = null;
+
+ // Map of frames (id => frame-info) and currently selected frame id.
+ this.frameMap = new Map();
+ this.selectedFrameId = null;
+
+ this._toolRegistered = this._toolRegistered.bind(this);
+ this._toolUnregistered = this._toolUnregistered.bind(this);
+ this._refreshHostTitle = this._refreshHostTitle.bind(this);
+ this._toggleAutohide = this._toggleAutohide.bind(this);
+ this.showFramesMenu = this.showFramesMenu.bind(this);
+ this._updateFrames = this._updateFrames.bind(this);
+ this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
+ this.destroy = this.destroy.bind(this);
+ this.highlighterUtils = getHighlighterUtils(this);
+ this._highlighterReady = this._highlighterReady.bind(this);
+ this._highlighterHidden = this._highlighterHidden.bind(this);
+ this._prefChanged = this._prefChanged.bind(this);
+ this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+ this._onBrowserMessage = this._onBrowserMessage.bind(this);
+ this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
+ this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
+ this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
+ this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
+ this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
+ this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
+ this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
+ this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
+ this._onTabbarFocus = this._onTabbarFocus.bind(this);
+ this._onTabbarArrowKeypress = this._onTabbarArrowKeypress.bind(this);
+ this._onPickerClick = this._onPickerClick.bind(this);
+ this._onPickerKeypress = this._onPickerKeypress.bind(this);
+ this._onPickerStarted = this._onPickerStarted.bind(this);
+ this._onPickerStopped = this._onPickerStopped.bind(this);
+
+ this._target.on("close", this.destroy);
+
+ if (!selectedTool) {
+ selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
+ }
+ this._defaultToolId = selectedTool;
+
+ this._hostType = hostType;
+
+ EventEmitter.decorate(this);
+
+ this._target.on("navigate", this._refreshHostTitle);
+ this._target.on("frame-update", this._updateFrames);
+
+ this.on("host-changed", this._refreshHostTitle);
+ this.on("select", this._refreshHostTitle);
+
+ this.on("ready", this._showDevEditionPromo);
+
+ gDevTools.on("tool-registered", this._toolRegistered);
+ gDevTools.on("tool-unregistered", this._toolUnregistered);
+
+ this.on("picker-started", this._onPickerStarted);
+ this.on("picker-stopped", this._onPickerStopped);
+}
+exports.Toolbox = Toolbox;
+
+/**
+ * The toolbox can be 'hosted' either embedded in a browser window
+ * or in a separate window.
+ */
+Toolbox.HostType = {
+ BOTTOM: "bottom",
+ SIDE: "side",
+ WINDOW: "window",
+ CUSTOM: "custom"
+};
+
+Toolbox.prototype = {
+ _URL: "about:devtools-toolbox",
+
+ _prefs: {
+ LAST_TOOL: "devtools.toolbox.selectedTool",
+ SIDE_ENABLED: "devtools.toolbox.sideEnabled",
+ },
+
+ currentToolId: null,
+ lastUsedToolId: null,
+
+ /**
+ * Returns a *copy* of the _toolPanels collection.
+ *
+ * @return {Map} panels
+ * All the running panels in the toolbox
+ */
+ getToolPanels: function () {
+ return new Map(this._toolPanels);
+ },
+
+ /**
+ * Access the panel for a given tool
+ */
+ getPanel: function (id) {
+ return this._toolPanels.get(id);
+ },
+
+ /**
+ * Get the panel instance for a given tool once it is ready.
+ * If the tool is already opened, the promise will resolve immediately,
+ * otherwise it will wait until the tool has been opened before resolving.
+ *
+ * Note that this does not open the tool, use selectTool if you'd
+ * like to select the tool right away.
+ *
+ * @param {String} id
+ * The id of the panel, for example "jsdebugger".
+ * @returns Promise
+ * A promise that resolves once the panel is ready.
+ */
+ getPanelWhenReady: function (id) {
+ let deferred = defer();
+ let panel = this.getPanel(id);
+ if (panel) {
+ deferred.resolve(panel);
+ } else {
+ this.on(id + "-ready", (e, initializedPanel) => {
+ deferred.resolve(initializedPanel);
+ });
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * This is a shortcut for getPanel(currentToolId) because it is much more
+ * likely that we're going to want to get the panel that we've just made
+ * visible
+ */
+ getCurrentPanel: function () {
+ return this._toolPanels.get(this.currentToolId);
+ },
+
+ /**
+ * Get/alter the target of a Toolbox so we're debugging something different.
+ * See Target.jsm for more details.
+ * TODO: Do we allow |toolbox.target = null;| ?
+ */
+ get target() {
+ return this._target;
+ },
+
+ get threadClient() {
+ return this._threadClient;
+ },
+
+ /**
+ * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate
+ * tab. See HostType for more details.
+ */
+ get hostType() {
+ return this._hostType;
+ },
+
+ /**
+ * Shortcut to the window containing the toolbox UI
+ */
+ get win() {
+ return this._win;
+ },
+
+ /**
+ * Shortcut to the document containing the toolbox UI
+ */
+ get doc() {
+ return this.win.document;
+ },
+
+ /**
+ * Get the toolbox highlighter front. Note that it may not always have been
+ * initialized first. Use `initInspector()` if needed.
+ * Consider using highlighterUtils instead, it exposes the highlighter API in
+ * a useful way for the toolbox panels
+ */
+ get highlighter() {
+ return this._highlighter;
+ },
+
+ /**
+ * Get the toolbox's performance front. Note that it may not always have been
+ * initialized first. Use `initPerformance()` if needed.
+ */
+ get performance() {
+ return this._performance;
+ },
+
+ /**
+ * Get the toolbox's inspector front. Note that it may not always have been
+ * initialized first. Use `initInspector()` if needed.
+ */
+ get inspector() {
+ return this._inspector;
+ },
+
+ /**
+ * Get the toolbox's walker front. Note that it may not always have been
+ * initialized first. Use `initInspector()` if needed.
+ */
+ get walker() {
+ return this._walker;
+ },
+
+ /**
+ * Get the toolbox's node selection. Note that it may not always have been
+ * initialized first. Use `initInspector()` if needed.
+ */
+ get selection() {
+ return this._selection;
+ },
+
+ /**
+ * Get the toggled state of the split console
+ */
+ get splitConsole() {
+ return this._splitConsole;
+ },
+
+ /**
+ * Get the focused state of the split console
+ */
+ isSplitConsoleFocused: function () {
+ if (!this._splitConsole) {
+ return false;
+ }
+ let focusedWin = Services.focus.focusedWindow;
+ return focusedWin && focusedWin ===
+ this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow;
+ },
+
+ /**
+ * Open the toolbox
+ */
+ open: function () {
+ return Task.spawn(function* () {
+ this.browserRequire = BrowserLoader({
+ window: this.doc.defaultView,
+ useOnlyShared: true
+ }).require;
+
+ if (this.win.location.href.startsWith(this._URL)) {
+ // Update the URL so that onceDOMReady watch for the right url.
+ this._URL = this.win.location.href;
+ }
+
+ let domReady = defer();
+ let domHelper = new DOMHelpers(this.win);
+ domHelper.onceDOMReady(() => {
+ domReady.resolve();
+ }, this._URL);
+
+ // Optimization: fire up a few other things before waiting on
+ // the iframe being ready (makes startup faster)
+
+ // Load the toolbox-level actor fronts and utilities now
+ yield this._target.makeRemote();
+
+ // Attach the thread
+ this._threadClient = yield attachThread(this);
+ yield domReady.promise;
+
+ this.isReady = true;
+ let framesPromise = this._listFrames();
+
+ this.closeButton = this.doc.getElementById("toolbox-close");
+ this.closeButton.addEventListener("click", this.destroy, true);
+
+ gDevTools.on("pref-changed", this._prefChanged);
+
+ let framesMenu = this.doc.getElementById("command-button-frames");
+ framesMenu.addEventListener("click", this.showFramesMenu, false);
+
+ let noautohideMenu = this.doc.getElementById("command-button-noautohide");
+ noautohideMenu.addEventListener("click", this._toggleAutohide, true);
+
+ this.textBoxContextMenuPopup =
+ this.doc.getElementById("toolbox-textbox-context-popup");
+ this.textBoxContextMenuPopup.addEventListener("popupshowing",
+ this._updateTextBoxMenuItems, true);
+
+ this.shortcuts = new KeyShortcuts({
+ window: this.doc.defaultView
+ });
+ this._buildDockButtons();
+ this._buildOptions();
+ this._buildTabs();
+ this._applyCacheSettings();
+ this._applyServiceWorkersTestingSettings();
+ this._addKeysToWindow();
+ this._addReloadKeys();
+ this._addHostListeners();
+ this._registerOverlays();
+ if (!this._hostOptions || this._hostOptions.zoom === true) {
+ ZoomKeys.register(this.win);
+ }
+
+ this.tabbar = this.doc.querySelector(".devtools-tabbar");
+ this.tabbar.addEventListener("focus", this._onTabbarFocus, true);
+ this.tabbar.addEventListener("click", this._onTabbarFocus, true);
+ this.tabbar.addEventListener("keypress", this._onTabbarArrowKeypress);
+
+ this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
+ this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
+ this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
+
+ let buttonsPromise = this._buildButtons();
+
+ this._pingTelemetry();
+
+ // The isTargetSupported check needs to happen after the target is
+ // remoted, otherwise we could have done it in the toolbox constructor
+ // (bug 1072764).
+ let toolDef = gDevTools.getToolDefinition(this._defaultToolId);
+ if (!toolDef || !toolDef.isTargetSupported(this._target)) {
+ this._defaultToolId = "webconsole";
+ }
+
+ yield this.selectTool(this._defaultToolId);
+
+ // Wait until the original tool is selected so that the split
+ // console input will receive focus.
+ let splitConsolePromise = promise.resolve();
+ if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
+ splitConsolePromise = this.openSplitConsole();
+ }
+
+ yield promise.all([
+ splitConsolePromise,
+ buttonsPromise,
+ framesPromise
+ ]);
+
+ // Lazily connect to the profiler here and don't wait for it to complete,
+ // used to intercept console.profile calls before the performance tools are open.
+ let performanceFrontConnection = this.initPerformance();
+
+ // If in testing environment, wait for performance connection to finish,
+ // so we don't have to explicitly wait for this in tests; ideally, all tests
+ // will handle this on their own, but each have their own tear down function.
+ if (flags.testing) {
+ yield performanceFrontConnection;
+ }
+
+ this.emit("ready");
+ }.bind(this)).then(null, console.error.bind(console));
+ },
+
+ /**
+ * loading React modules when needed (to avoid performance penalties
+ * during Firefox start up time).
+ */
+ get React() {
+ return this.browserRequire("devtools/client/shared/vendor/react");
+ },
+
+ get ReactDOM() {
+ return this.browserRequire("devtools/client/shared/vendor/react-dom");
+ },
+
+ get ReactRedux() {
+ return this.browserRequire("devtools/client/shared/vendor/react-redux");
+ },
+
+ // Return HostType id for telemetry
+ _getTelemetryHostId: function () {
+ switch (this.hostType) {
+ case Toolbox.HostType.BOTTOM: return 0;
+ case Toolbox.HostType.SIDE: return 1;
+ case Toolbox.HostType.WINDOW: return 2;
+ case Toolbox.HostType.CUSTOM: return 3;
+ default: return 9;
+ }
+ },
+
+ _pingTelemetry: function () {
+ this._telemetry.toolOpened("toolbox");
+
+ this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
+ this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
+ Services.appinfo.is64Bit ? 1 : 0);
+ this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
+ system.getScreenDimensions());
+ this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
+ },
+
+ /**
+ * Because our panels are lazy loaded this is a good place to watch for
+ * "pref-changed" events.
+ * @param {String} event
+ * The event type, "pref-changed".
+ * @param {Object} data
+ * {
+ * newValue: The new value
+ * oldValue: The old value
+ * pref: The name of the preference that has changed
+ * }
+ */
+ _prefChanged: function (event, data) {
+ switch (data.pref) {
+ case "devtools.cache.disabled":
+ this._applyCacheSettings();
+ break;
+ case "devtools.serviceWorkers.testing.enabled":
+ this._applyServiceWorkersTestingSettings();
+ break;
+ }
+ },
+
+ _buildOptions: function () {
+ let selectOptions = (name, event) => {
+ // Flip back to the last used panel if we are already
+ // on the options panel.
+ if (this.currentToolId === "options" &&
+ gDevTools.getToolDefinition(this.lastUsedToolId)) {
+ this.selectTool(this.lastUsedToolId);
+ } else {
+ this.selectTool("options");
+ }
+ // Prevent the opening of bookmarks window on toolbox.options.key
+ event.preventDefault();
+ };
+ this.shortcuts.on(L10N.getStr("toolbox.options.key"), selectOptions);
+ this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
+ },
+
+ _splitConsoleOnKeypress: function (e) {
+ if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
+ this.toggleSplitConsole();
+ // If the debugger is paused, don't let the ESC key stop any pending
+ // navigation.
+ if (this._threadClient.state == "paused") {
+ e.preventDefault();
+ }
+ }
+ },
+
+ /**
+ * Add a shortcut key that should work when a split console
+ * has focus to the toolbox.
+ *
+ * @param {String} key
+ * The electron key shortcut.
+ * @param {Function} handler
+ * The callback that should be called when the provided key shortcut is pressed.
+ * @param {String} whichTool
+ * The tool the key belongs to. The corresponding handler will only be triggered
+ * if this tool is active.
+ */
+ useKeyWithSplitConsole: function (key, handler, whichTool) {
+ this.shortcuts.on(key, (name, event) => {
+ if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
+ handler();
+ event.preventDefault();
+ }
+ });
+ },
+
+ _addReloadKeys: function () {
+ [
+ ["reload", false],
+ ["reload2", false],
+ ["forceReload", true],
+ ["forceReload2", true]
+ ].forEach(([id, force]) => {
+ let key = L10N.getStr("toolbox." + id + ".key");
+ this.shortcuts.on(key, (name, event) => {
+ this.reloadTarget(force);
+
+ // Prevent Firefox shortcuts from reloading the page
+ event.preventDefault();
+ });
+ });
+ },
+
+ _addHostListeners: function () {
+ this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"),
+ (name, event) => {
+ this.selectNextTool();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"),
+ (name, event) => {
+ this.selectPreviousTool();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.minimize.key"),
+ (name, event) => {
+ this._toggleMinimizeMode();
+ event.preventDefault();
+ });
+ this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"),
+ (name, event) => {
+ this.switchToPreviousHost();
+ event.preventDefault();
+ });
+
+ this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false);
+ this.doc.addEventListener("focus", this._onFocus, true);
+ this.win.addEventListener("unload", this.destroy);
+ this.win.addEventListener("message", this._onBrowserMessage, true);
+ },
+
+ _removeHostListeners: function () {
+ // The host iframe's contentDocument may already be gone.
+ if (this.doc) {
+ this.doc.removeEventListener("keypress", this._splitConsoleOnKeypress, false);
+ this.doc.removeEventListener("focus", this._onFocus, true);
+ this.win.removeEventListener("unload", this.destroy);
+ this.win.removeEventListener("message", this._onBrowserMessage, true);
+ }
+ },
+
+ // Called whenever the chrome send a message
+ _onBrowserMessage: function (event) {
+ if (!event.data) {
+ return;
+ }
+ switch (event.data.name) {
+ case "switched-host":
+ this._onSwitchedHost(event.data);
+ break;
+ case "host-minimized":
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ this._onBottomHostMinimized();
+ }
+ break;
+ case "host-maximized":
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ this._onBottomHostMaximized();
+ }
+ break;
+ }
+ },
+
+ _registerOverlays: function () {
+ registerHarOverlay(this);
+ },
+
+ _saveSplitConsoleHeight: function () {
+ Services.prefs.setIntPref(SPLITCONSOLE_HEIGHT_PREF,
+ this.webconsolePanel.height);
+ },
+
+ /**
+ * Make sure that the console is showing up properly based on all the
+ * possible conditions.
+ * 1) If the console tab is selected, then regardless of split state
+ * it should take up the full height of the deck, and we should
+ * hide the deck and splitter.
+ * 2) If the console tab is not selected and it is split, then we should
+ * show the splitter, deck, and console.
+ * 3) If the console tab is not selected and it is *not* split,
+ * then we should hide the console and splitter, and show the deck
+ * at full height.
+ */
+ _refreshConsoleDisplay: function () {
+ let deck = this.doc.getElementById("toolbox-deck");
+ let webconsolePanel = this.webconsolePanel;
+ let splitter = this.doc.getElementById("toolbox-console-splitter");
+ let openedConsolePanel = this.currentToolId === "webconsole";
+
+ if (openedConsolePanel) {
+ deck.setAttribute("collapsed", "true");
+ splitter.setAttribute("hidden", "true");
+ webconsolePanel.removeAttribute("collapsed");
+ } else {
+ deck.removeAttribute("collapsed");
+ if (this.splitConsole) {
+ webconsolePanel.removeAttribute("collapsed");
+ splitter.removeAttribute("hidden");
+ } else {
+ webconsolePanel.setAttribute("collapsed", "true");
+ splitter.setAttribute("hidden", "true");
+ }
+ }
+ },
+
+ /**
+ * Adds the keys and commands to the Toolbox Window in window mode.
+ */
+ _addKeysToWindow: function () {
+ if (this.hostType != Toolbox.HostType.WINDOW) {
+ return;
+ }
+
+ let doc = this.win.parent.document;
+
+ for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) {
+ // Prevent multiple entries for the same tool.
+ if (!toolDefinition.key || doc.getElementById("key_" + id)) {
+ continue;
+ }
+
+ let toolId = id;
+ let key = doc.createElement("key");
+
+ key.id = "key_" + toolId;
+
+ if (toolDefinition.key.startsWith("VK_")) {
+ key.setAttribute("keycode", toolDefinition.key);
+ } else {
+ key.setAttribute("key", toolDefinition.key);
+ }
+
+ key.setAttribute("modifiers", toolDefinition.modifiers);
+ // needed. See bug 371900
+ key.setAttribute("oncommand", "void(0);");
+ key.addEventListener("command", () => {
+ this.selectTool(toolId).then(() => this.fireCustomKey(toolId));
+ }, true);
+ doc.getElementById("toolbox-keyset").appendChild(key);
+ }
+
+ // Add key for toggling the browser console from the detached window
+ if (!doc.getElementById("key_browserconsole")) {
+ let key = doc.createElement("key");
+ key.id = "key_browserconsole";
+
+ key.setAttribute("key", L10N.getStr("browserConsoleCmd.commandkey"));
+ key.setAttribute("modifiers", "accel,shift");
+ // needed. See bug 371900
+ key.setAttribute("oncommand", "void(0)");
+ key.addEventListener("command", () => {
+ HUDService.toggleBrowserConsole();
+ }, true);
+ doc.getElementById("toolbox-keyset").appendChild(key);
+ }
+ },
+
+ /**
+ * Handle any custom key events. Returns true if there was a custom key
+ * binding run.
+ * @param {string} toolId Which tool to run the command on (skip if not
+ * current)
+ */
+ fireCustomKey: function (toolId) {
+ let toolDefinition = gDevTools.getToolDefinition(toolId);
+
+ if (toolDefinition.onkey &&
+ ((this.currentToolId === toolId) ||
+ (toolId == "webconsole" && this.splitConsole))) {
+ toolDefinition.onkey(this.getCurrentPanel(), this);
+ }
+ },
+
+ /**
+ * Build the notification box as soon as needed.
+ */
+ get notificationBox() {
+ if (!this._notificationBox) {
+ let { NotificationBox, PriorityLevels } =
+ this.browserRequire(
+ "devtools/client/shared/components/notification-box");
+
+ NotificationBox = this.React.createFactory(NotificationBox);
+
+ // Render NotificationBox and assign priority levels to it.
+ let box = this.doc.getElementById("toolbox-notificationbox");
+ this._notificationBox = Object.assign(
+ this.ReactDOM.render(NotificationBox({}), box),
+ PriorityLevels);
+ }
+ return this._notificationBox;
+ },
+
+ /**
+ * Build the buttons for changing hosts. Called every time
+ * the host changes.
+ */
+ _buildDockButtons: function () {
+ let dockBox = this.doc.getElementById("toolbox-dock-buttons");
+
+ while (dockBox.firstChild) {
+ dockBox.removeChild(dockBox.firstChild);
+ }
+
+ if (!this._target.isLocalTab) {
+ return;
+ }
+
+ // Bottom-type host can be minimized, add a button for this.
+ if (this.hostType == Toolbox.HostType.BOTTOM) {
+ let minimizeBtn = this.doc.createElementNS(HTML_NS, "button");
+ minimizeBtn.id = "toolbox-dock-bottom-minimize";
+ minimizeBtn.className = "devtools-button";
+ /* Bug 1177463 - The minimize button is currently hidden until we agree on
+ the UI for it, and until bug 1173849 is fixed too. */
+ minimizeBtn.setAttribute("hidden", "true");
+
+ minimizeBtn.addEventListener("click", this._toggleMinimizeMode);
+ dockBox.appendChild(minimizeBtn);
+ // Show the button in its maximized state.
+ this._onBottomHostMaximized();
+
+ // Maximize again when a tool gets selected.
+ this.on("before-select", this._onToolSelectWhileMinimized);
+ // Maximize and stop listening before the host type changes.
+ this.once("host-will-change", this._onBottomHostWillChange);
+ }
+
+ if (this.hostType == Toolbox.HostType.WINDOW) {
+ this.closeButton.setAttribute("hidden", "true");
+ } else {
+ this.closeButton.removeAttribute("hidden");
+ }
+
+ let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
+
+ for (let type in Toolbox.HostType) {
+ let position = Toolbox.HostType[type];
+ if (position == this.hostType ||
+ position == Toolbox.HostType.CUSTOM ||
+ (!sideEnabled && position == Toolbox.HostType.SIDE)) {
+ continue;
+ }
+
+ let button = this.doc.createElementNS(HTML_NS, "button");
+ button.id = "toolbox-dock-" + position;
+ button.className = "toolbox-dock-button devtools-button";
+ button.setAttribute("title", L10N.getStr("toolboxDockButtons." +
+ position + ".tooltip"));
+ button.addEventListener("click", this.switchHost.bind(this, position));
+
+ dockBox.appendChild(button);
+ }
+ },
+
+ _getMinimizeButtonShortcutTooltip: function () {
+ let str = L10N.getStr("toolbox.minimize.key");
+ let key = KeyShortcuts.parseElectronKey(this.win, str);
+ return "(" + KeyShortcuts.stringify(key) + ")";
+ },
+
+ _onBottomHostMinimized: function () {
+ let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
+ btn.className = "minimized";
+
+ btn.setAttribute("title",
+ L10N.getStr("toolboxDockButtons.bottom.maximize") + " " +
+ this._getMinimizeButtonShortcutTooltip());
+ },
+
+ _onBottomHostMaximized: function () {
+ let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
+ btn.className = "maximized";
+
+ btn.setAttribute("title",
+ L10N.getStr("toolboxDockButtons.bottom.minimize") + " " +
+ this._getMinimizeButtonShortcutTooltip());
+ },
+
+ _onToolSelectWhileMinimized: function () {
+ this.postMessage({
+ name: "maximize-host"
+ });
+ },
+
+ postMessage: function (msg) {
+ // We sometime try to send messages in middle of destroy(), where the
+ // toolbox iframe may already be detached and no longer have a parent.
+ if (this.win.parent) {
+ // Toolbox document is still chrome and disallow identifying message
+ // origin via event.source as it is null. So use a custom id.
+ msg.frameId = this.frameId;
+ this.win.parent.postMessage(msg, "*");
+ }
+ },
+
+ _onBottomHostWillChange: function () {
+ this.postMessage({
+ name: "maximize-host"
+ });
+
+ this.off("before-select", this._onToolSelectWhileMinimized);
+ },
+
+ _toggleMinimizeMode: function () {
+ if (this.hostType !== Toolbox.HostType.BOTTOM) {
+ return;
+ }
+
+ // Calculate the height to which the host should be minimized so the
+ // tabbar is still visible.
+ let toolbarHeight = this.tabbar.getBoxQuads({box: "content"})[0].bounds
+ .height;
+ this.postMessage({
+ name: "toggle-minimize-mode",
+ toolbarHeight
+ });
+ },
+
+ /**
+ * Add tabs to the toolbox UI for registered tools
+ */
+ _buildTabs: function () {
+ for (let definition of gDevTools.getToolDefinitionArray()) {
+ this._buildTabForTool(definition);
+ }
+ },
+
+ /**
+ * Get all dev tools tab bar focusable elements. These are visible elements
+ * such as buttons or elements with tabindex.
+ */
+ get tabbarFocusableElms() {
+ return [...this.tabbar.querySelectorAll(
+ "[tabindex]:not([hidden]), button:not([hidden])")];
+ },
+
+ /**
+ * Reset tabindex attributes across all focusable elements inside the tabbar.
+ * Only have one element with tabindex=0 at a time to make sure that tabbing
+ * results in navigating away from the tabbar container.
+ * @param {FocusEvent} event
+ */
+ _onTabbarFocus: function (event) {
+ this.tabbarFocusableElms.forEach(elm =>
+ elm.setAttribute("tabindex", event.target === elm ? "0" : "-1"));
+ },
+
+ /**
+ * On left/right arrow press, attempt to move the focus inside the tabbar to
+ * the previous/next focusable element.
+ * @param {KeyboardEvent} event
+ */
+ _onTabbarArrowKeypress: function (event) {
+ let { key, target, ctrlKey, shiftKey, altKey, metaKey } = event;
+
+ // If any of the modifier keys are pressed do not attempt navigation as it
+ // might conflict with global shortcuts (Bug 1327972).
+ if (ctrlKey || shiftKey || altKey || metaKey) {
+ return;
+ }
+
+ let focusableElms = this.tabbarFocusableElms;
+ let curIndex = focusableElms.indexOf(target);
+
+ if (curIndex === -1) {
+ console.warn(target + " is not found among Developer Tools tab bar " +
+ "focusable elements. It needs to either be a button or have " +
+ "tabindex. If it is intended to be hidden, 'hidden' attribute must " +
+ "be used.");
+ return;
+ }
+
+ let newTarget;
+
+ if (key === "ArrowLeft") {
+ // Do nothing if already at the beginning.
+ if (curIndex === 0) {
+ return;
+ }
+ newTarget = focusableElms[curIndex - 1];
+ } else if (key === "ArrowRight") {
+ // Do nothing if already at the end.
+ if (curIndex === focusableElms.length - 1) {
+ return;
+ }
+ newTarget = focusableElms[curIndex + 1];
+ } else {
+ return;
+ }
+
+ focusableElms.forEach(elm =>
+ elm.setAttribute("tabindex", newTarget === elm ? "0" : "-1"));
+ newTarget.focus();
+
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
+ */
+ _buildButtons: function () {
+ if (this.target.getTrait("highlightable")) {
+ this._buildPickerButton();
+ }
+
+ this.setToolboxButtonsVisibility();
+
+ // Old servers don't have a GCLI Actor, so just return
+ if (!this.target.hasActor("gcli")) {
+ return promise.resolve();
+ }
+ // Disable gcli in browser toolbox until there is usages of it
+ if (this.target.chrome) {
+ return promise.resolve();
+ }
+
+ const options = {
+ environment: CommandUtils.createEnvironment(this, "_target")
+ };
+ return CommandUtils.createRequisition(this.target, options).then(requisition => {
+ this._requisition = requisition;
+
+ const spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
+ return CommandUtils.createButtons(spec, this.target, this.doc, requisition)
+ .then(buttons => {
+ let container = this.doc.getElementById("toolbox-buttons");
+ buttons.forEach(button => {
+ if (button) {
+ container.appendChild(button);
+ }
+ });
+ this.setToolboxButtonsVisibility();
+ });
+ });
+ },
+
+ /**
+ * Adding the element picker button is done here unlike the other buttons
+ * since we want it to work for remote targets too
+ */
+ _buildPickerButton: function () {
+ this._pickerButton = this.doc.createElementNS(HTML_NS, "button");
+ this._pickerButton.id = "command-button-pick";
+ this._pickerButton.className =
+ "command-button command-button-invertable devtools-button";
+ this._pickerButton.setAttribute("title", L10N.getStr("pickButton.tooltip"));
+
+ let container = this.doc.querySelector("#toolbox-picker-container");
+ container.appendChild(this._pickerButton);
+
+ this._pickerButton.addEventListener("click", this._onPickerClick, false);
+ },
+
+ /**
+ * Toggle the picker, but also decide whether or not the highlighter should
+ * focus the window. This is only desirable when the toolbox is mounted to the
+ * window. When devtools is free floating, then the target window should not
+ * pop in front of the viewer when the picker is clicked.
+ */
+ _onPickerClick: function () {
+ let focus = this.hostType === Toolbox.HostType.BOTTOM ||
+ this.hostType === Toolbox.HostType.SIDE;
+ this.highlighterUtils.togglePicker(focus);
+ },
+
+ /**
+ * If the picker is activated, then allow the Escape key to deactivate the
+ * functionality instead of the default behavior of toggling the console.
+ */
+ _onPickerKeypress: function (event) {
+ if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) {
+ this.highlighterUtils.cancelPicker();
+ // Stop the console from toggling.
+ event.stopImmediatePropagation();
+ }
+ },
+
+ _onPickerStarted: function () {
+ this.doc.addEventListener("keypress", this._onPickerKeypress, true);
+ },
+
+ _onPickerStopped: function () {
+ this.doc.removeEventListener("keypress", this._onPickerKeypress, true);
+ },
+
+ /**
+ * Apply the current cache setting from devtools.cache.disabled to this
+ * toolbox's tab.
+ */
+ _applyCacheSettings: function () {
+ let pref = "devtools.cache.disabled";
+ let cacheDisabled = Services.prefs.getBoolPref(pref);
+
+ if (this.target.activeTab) {
+ this.target.activeTab.reconfigure({"cacheDisabled": cacheDisabled});
+ }
+ },
+
+ /**
+ * Apply the current service workers testing setting from
+ * devtools.serviceWorkers.testing.enabled to this toolbox's tab.
+ */
+ _applyServiceWorkersTestingSettings: function () {
+ let pref = "devtools.serviceWorkers.testing.enabled";
+ let serviceWorkersTestingEnabled =
+ Services.prefs.getBoolPref(pref) || false;
+
+ if (this.target.activeTab) {
+ this.target.activeTab.reconfigure({
+ "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
+ });
+ }
+ },
+
+ /**
+ * Setter for the checked state of the picker button in the toolbar
+ * @param {Boolean} isChecked
+ */
+ set pickerButtonChecked(isChecked) {
+ if (isChecked) {
+ this._pickerButton.setAttribute("checked", "true");
+ } else {
+ this._pickerButton.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Return all toolbox buttons (command buttons, plus any others that were
+ * added manually).
+ */
+ get toolboxButtons() {
+ return ToolboxButtons.map(options => {
+ let button = this.doc.getElementById(options.id);
+ // Some buttons may not exist inside of Browser Toolbox
+ if (!button) {
+ return false;
+ }
+
+ return {
+ id: options.id,
+ button: button,
+ label: button.getAttribute("title"),
+ visibilityswitch: "devtools." + options.id + ".enabled",
+ isTargetSupported: options.isTargetSupported
+ ? options.isTargetSupported
+ : target => target.isLocalTab,
+ };
+ }).filter(button=>button);
+ },
+
+ /**
+ * Ensure the visibility of each toolbox button matches the
+ * preference value. Simply hide buttons that are preffed off.
+ */
+ setToolboxButtonsVisibility: function () {
+ this.toolboxButtons.forEach(buttonSpec => {
+ let { visibilityswitch, button, isTargetSupported } = buttonSpec;
+ let on = true;
+ try {
+ on = Services.prefs.getBoolPref(visibilityswitch);
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ on = on && isTargetSupported(this.target);
+
+ if (button) {
+ if (on) {
+ button.removeAttribute("hidden");
+ } else {
+ button.setAttribute("hidden", "true");
+ }
+ }
+ });
+
+ this._updateNoautohideButton();
+ },
+
+ /**
+ * Build a tab for one tool definition and add to the toolbox
+ *
+ * @param {string} toolDefinition
+ * Tool definition of the tool to build a tab for.
+ */
+ _buildTabForTool: function (toolDefinition) {
+ if (!toolDefinition.isTargetSupported(this._target)) {
+ return;
+ }
+
+ let tabs = this.doc.getElementById("toolbox-tabs");
+ let deck = this.doc.getElementById("toolbox-deck");
+
+ let id = toolDefinition.id;
+
+ if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
+ toolDefinition.ordinal = MAX_ORDINAL;
+ }
+
+ let radio = this.doc.createElement("radio");
+ // The radio element is not being used in the conventional way, thus
+ // the devtools-tab class replaces the radio XBL binding with its base
+ // binding (the control-item binding).
+ radio.className = "devtools-tab";
+ radio.id = "toolbox-tab-" + id;
+ radio.setAttribute("toolid", id);
+ radio.setAttribute("tabindex", "0");
+ radio.setAttribute("ordinal", toolDefinition.ordinal);
+ radio.setAttribute("tooltiptext", toolDefinition.tooltip);
+ if (toolDefinition.invertIconForLightTheme) {
+ radio.setAttribute("icon-invertable", "light-theme");
+ } else if (toolDefinition.invertIconForDarkTheme) {
+ radio.setAttribute("icon-invertable", "dark-theme");
+ }
+
+ radio.addEventListener("command", this.selectTool.bind(this, id));
+
+ // spacer lets us center the image and label, while allowing cropping
+ let spacer = this.doc.createElement("spacer");
+ spacer.setAttribute("flex", "1");
+ radio.appendChild(spacer);
+
+ if (toolDefinition.icon) {
+ let image = this.doc.createElement("image");
+ image.className = "default-icon";
+ image.setAttribute("src",
+ toolDefinition.icon || toolDefinition.highlightedicon);
+ radio.appendChild(image);
+ // Adding the highlighted icon image
+ image = this.doc.createElement("image");
+ image.className = "highlighted-icon";
+ image.setAttribute("src",
+ toolDefinition.highlightedicon || toolDefinition.icon);
+ radio.appendChild(image);
+ }
+
+ if (toolDefinition.label && !toolDefinition.iconOnly) {
+ let label = this.doc.createElement("label");
+ label.setAttribute("value", toolDefinition.label);
+ label.setAttribute("crop", "end");
+ label.setAttribute("flex", "1");
+ radio.appendChild(label);
+ }
+
+ if (!toolDefinition.bgTheme) {
+ toolDefinition.bgTheme = "theme-toolbar";
+ }
+ let vbox = this.doc.createElement("vbox");
+ vbox.className = "toolbox-panel " + toolDefinition.bgTheme;
+
+ // There is already a container for the webconsole frame.
+ if (!this.doc.getElementById("toolbox-panel-" + id)) {
+ vbox.id = "toolbox-panel-" + id;
+ }
+
+ if (id === "options") {
+ // Options panel is special. It doesn't belong in the same container as
+ // the other tabs.
+ radio.setAttribute("role", "button");
+ let optionTabContainer = this.doc.getElementById("toolbox-option-container");
+ optionTabContainer.appendChild(radio);
+ deck.appendChild(vbox);
+ } else {
+ radio.setAttribute("role", "tab");
+
+ // If there is no tab yet, or the ordinal to be added is the largest one.
+ if (tabs.childNodes.length == 0 ||
+ tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
+ tabs.appendChild(radio);
+ deck.appendChild(vbox);
+ } else {
+ // else, iterate over all the tabs to get the correct location.
+ Array.some(tabs.childNodes, (node, i) => {
+ if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
+ tabs.insertBefore(radio, node);
+ deck.insertBefore(vbox, deck.childNodes[i]);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ this._addKeysToWindow();
+ },
+
+ /**
+ * Ensure the tool with the given id is loaded.
+ *
+ * @param {string} id
+ * The id of the tool to load.
+ */
+ loadTool: function (id) {
+ if (id === "inspector" && !this._inspector) {
+ return this.initInspector().then(() => {
+ return this.loadTool(id);
+ });
+ }
+
+ let deferred = defer();
+ let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
+
+ if (iframe) {
+ let panel = this._toolPanels.get(id);
+ if (panel) {
+ deferred.resolve(panel);
+ } else {
+ this.once(id + "-ready", initializedPanel => {
+ deferred.resolve(initializedPanel);
+ });
+ }
+ return deferred.promise;
+ }
+
+ let definition = gDevTools.getToolDefinition(id);
+ if (!definition) {
+ deferred.reject(new Error("no such tool id " + id));
+ return deferred.promise;
+ }
+
+ iframe = this.doc.createElement("iframe");
+ iframe.className = "toolbox-panel-iframe";
+ iframe.id = "toolbox-panel-iframe-" + id;
+ iframe.setAttribute("flex", 1);
+ iframe.setAttribute("forceOwnRefreshDriver", "");
+ iframe.tooltip = "aHTMLTooltip";
+ iframe.style.visibility = "hidden";
+
+ gDevTools.emit(id + "-init", this, iframe);
+ this.emit(id + "-init", iframe);
+
+ // If no parent yet, append the frame into default location.
+ if (!iframe.parentNode) {
+ let vbox = this.doc.getElementById("toolbox-panel-" + id);
+ vbox.appendChild(iframe);
+ }
+
+ let onLoad = () => {
+ // Prevent flicker while loading by waiting to make visible until now.
+ iframe.style.visibility = "visible";
+
+ // Try to set the dir attribute as early as possible.
+ this.setIframeDocumentDir(iframe);
+
+ // The build method should return a panel instance, so events can
+ // be fired with the panel as an argument. However, in order to keep
+ // backward compatibility with existing extensions do a check
+ // for a promise return value.
+ let built = definition.build(iframe.contentWindow, this);
+
+ if (!(typeof built.then == "function")) {
+ let panel = built;
+ iframe.panel = panel;
+
+ // The panel instance is expected to fire (and listen to) various
+ // framework events, so make sure it's properly decorated with
+ // appropriate API (on, off, once, emit).
+ // In this case we decorate panel instances directly returned by
+ // the tool definition 'build' method.
+ if (typeof panel.emit == "undefined") {
+ EventEmitter.decorate(panel);
+ }
+
+ gDevTools.emit(id + "-build", this, panel);
+ this.emit(id + "-build", panel);
+
+ // The panel can implement an 'open' method for asynchronous
+ // initialization sequence.
+ if (typeof panel.open == "function") {
+ built = panel.open();
+ } else {
+ let buildDeferred = defer();
+ buildDeferred.resolve(panel);
+ built = buildDeferred.promise;
+ }
+ }
+
+ // Wait till the panel is fully ready and fire 'ready' events.
+ promise.resolve(built).then((panel) => {
+ this._toolPanels.set(id, panel);
+
+ // Make sure to decorate panel object with event API also in case
+ // where the tool definition 'build' method returns only a promise
+ // and the actual panel instance is available as soon as the
+ // promise is resolved.
+ if (typeof panel.emit == "undefined") {
+ EventEmitter.decorate(panel);
+ }
+
+ gDevTools.emit(id + "-ready", this, panel);
+ this.emit(id + "-ready", panel);
+
+ deferred.resolve(panel);
+ }, console.error);
+ };
+
+ iframe.setAttribute("src", definition.url);
+ if (definition.panelLabel) {
+ iframe.setAttribute("aria-label", definition.panelLabel);
+ }
+
+ // Depending on the host, iframe.contentWindow is not always
+ // defined at this moment. If it is not defined, we use an
+ // event listener on the iframe DOM node. If it's defined,
+ // we use the chromeEventHandler. We can't use a listener
+ // on the DOM node every time because this won't work
+ // if the (xul chrome) iframe is loaded in a content docshell.
+ if (iframe.contentWindow) {
+ let domHelper = new DOMHelpers(iframe.contentWindow);
+ domHelper.onceDOMReady(onLoad);
+ } else {
+ let callback = () => {
+ iframe.removeEventListener("DOMContentLoaded", callback);
+ onLoad();
+ };
+
+ iframe.addEventListener("DOMContentLoaded", callback);
+ }
+
+ return deferred.promise;
+ },
+
+ /**
+ * Set the dir attribute on the content document element of the provided iframe.
+ *
+ * @param {IFrameElement} iframe
+ */
+ setIframeDocumentDir: function (iframe) {
+ let docEl = iframe.contentWindow && iframe.contentWindow.document.documentElement;
+ if (!docEl || docEl.namespaceURI !== HTML_NS) {
+ // Bail out if the content window or document is not ready or if the document is not
+ // HTML.
+ return;
+ }
+
+ if (docEl.hasAttribute("dir")) {
+ // Set the dir attribute value only if dir is already present on the document.
+ let top = this.win.top;
+ let topDocEl = top.document.documentElement;
+ let isRtl = top.getComputedStyle(topDocEl).direction === "rtl";
+ docEl.setAttribute("dir", isRtl ? "rtl" : "ltr");
+ }
+ },
+
+ /**
+ * Mark all in collection as unselected; and id as selected
+ * @param {string} collection
+ * DOM collection of items
+ * @param {string} id
+ * The Id of the item within the collection to select
+ */
+ selectSingleNode: function (collection, id) {
+ [...collection].forEach(node => {
+ if (node.id === id) {
+ node.setAttribute("selected", "true");
+ node.setAttribute("aria-selected", "true");
+ } else {
+ node.removeAttribute("selected");
+ node.removeAttribute("aria-selected");
+ }
+ });
+ },
+
+ /**
+ * Switch to the tool with the given id
+ *
+ * @param {string} id
+ * The id of the tool to switch to
+ */
+ selectTool: function (id) {
+ this.emit("before-select", id);
+
+ let tabs = this.doc.querySelectorAll(".devtools-tab");
+ this.selectSingleNode(tabs, "toolbox-tab-" + id);
+
+ // If options is selected, the separator between it and the
+ // command buttons should be hidden.
+ let sep = this.doc.getElementById("toolbox-controls-separator");
+ if (id === "options") {
+ sep.setAttribute("invisible", "true");
+ } else {
+ sep.removeAttribute("invisible");
+ }
+
+ if (this.currentToolId == id) {
+ let panel = this._toolPanels.get(id);
+ if (panel) {
+ // We have a panel instance, so the tool is already fully loaded.
+
+ // re-focus tool to get key events again
+ this.focusTool(id);
+
+ // Return the existing panel in order to have a consistent return value.
+ return promise.resolve(panel);
+ }
+ // Otherwise, if there is no panel instance, it is still loading,
+ // so we are racing another call to selectTool with the same id.
+ return this.once("select").then(() => promise.resolve(this._toolPanels.get(id)));
+ }
+
+ if (!this.isReady) {
+ throw new Error("Can't select tool, wait for toolbox 'ready' event");
+ }
+
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+
+ if (tab) {
+ if (this.currentToolId) {
+ this._telemetry.toolClosed(this.currentToolId);
+ }
+ this._telemetry.toolOpened(id);
+ } else {
+ throw new Error("No tool found");
+ }
+
+ let tabstrip = this.doc.getElementById("toolbox-tabs");
+
+ // select the right tab, making 0th index the default tab if right tab not
+ // found.
+ tabstrip.selectedItem = tab || tabstrip.childNodes[0];
+
+ // and select the right iframe
+ let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
+ this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
+
+ this.lastUsedToolId = this.currentToolId;
+ this.currentToolId = id;
+ this._refreshConsoleDisplay();
+ if (id != "options") {
+ Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
+ }
+
+ return this.loadTool(id).then(panel => {
+ // focus the tool's frame to start receiving key events
+ this.focusTool(id);
+
+ this.emit("select", id);
+ this.emit(id + "-selected", panel);
+ return panel;
+ });
+ },
+
+ /**
+ * Focus a tool's panel by id
+ * @param {string} id
+ * The id of tool to focus
+ */
+ focusTool: function (id, state = true) {
+ let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
+
+ if (state) {
+ iframe.focus();
+ } else {
+ iframe.blur();
+ }
+ },
+
+ /**
+ * Focus split console's input line
+ */
+ focusConsoleInput: function () {
+ let consolePanel = this.getPanel("webconsole");
+ if (consolePanel) {
+ consolePanel.focusInput();
+ }
+ },
+
+ /**
+ * If the console is split and we are focusing an element outside
+ * of the console, then store the newly focused element, so that
+ * it can be restored once the split console closes.
+ */
+ _onFocus: function ({originalTarget}) {
+ // Ignore any non element nodes, or any elements contained
+ // within the webconsole frame.
+ let webconsoleURL = gDevTools.getToolDefinition("webconsole").url;
+ if (originalTarget.nodeType !== 1 ||
+ originalTarget.baseURI === webconsoleURL) {
+ return;
+ }
+
+ this._lastFocusedElement = originalTarget;
+ },
+
+ /**
+ * Opens the split console.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * loaded and focused.
+ */
+ openSplitConsole: function () {
+ this._splitConsole = true;
+ Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
+ this._refreshConsoleDisplay();
+ this.emit("split-console");
+
+ return this.loadTool("webconsole").then(() => {
+ this.focusConsoleInput();
+ });
+ },
+
+ /**
+ * Closes the split console.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * closed.
+ */
+ closeSplitConsole: function () {
+ this._splitConsole = false;
+ Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, false);
+ this._refreshConsoleDisplay();
+ this.emit("split-console");
+
+ if (this._lastFocusedElement) {
+ this._lastFocusedElement.focus();
+ }
+ return promise.resolve();
+ },
+
+ /**
+ * Toggles the split state of the webconsole. If the webconsole panel
+ * is already selected then this command is ignored.
+ *
+ * @returns {Promise} a promise that resolves once the tool has been
+ * opened or closed.
+ */
+ toggleSplitConsole: function () {
+ if (this.currentToolId !== "webconsole") {
+ return this.splitConsole ?
+ this.closeSplitConsole() :
+ this.openSplitConsole();
+ }
+
+ return promise.resolve();
+ },
+
+ /**
+ * Tells the target tab to reload.
+ */
+ reloadTarget: function (force) {
+ this.target.activeTab.reload({ force: force });
+ },
+
+ /**
+ * Loads the tool next to the currently selected tool.
+ */
+ selectNextTool: function () {
+ let tools = this.doc.querySelectorAll(".devtools-tab");
+ let selected = this.doc.querySelector(".devtools-tab[selected]");
+ let nextIndex = [...tools].indexOf(selected) + 1;
+ let next = tools[nextIndex] || tools[0];
+ let tool = next.getAttribute("toolid");
+ return this.selectTool(tool);
+ },
+
+ /**
+ * Loads the tool just left to the currently selected tool.
+ */
+ selectPreviousTool: function () {
+ let tools = this.doc.querySelectorAll(".devtools-tab");
+ let selected = this.doc.querySelector(".devtools-tab[selected]");
+ let prevIndex = [...tools].indexOf(selected) - 1;
+ let prev = tools[prevIndex] || tools[tools.length - 1];
+ let tool = prev.getAttribute("toolid");
+ return this.selectTool(tool);
+ },
+
+ /**
+ * Highlights the tool's tab if it is not the currently selected tool.
+ *
+ * @param {string} id
+ * The id of the tool to highlight
+ */
+ highlightTool: function (id) {
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab && tab.setAttribute("highlighted", "true");
+ },
+
+ /**
+ * De-highlights the tool's tab.
+ *
+ * @param {string} id
+ * The id of the tool to unhighlight
+ */
+ unhighlightTool: function (id) {
+ let tab = this.doc.getElementById("toolbox-tab-" + id);
+ tab && tab.removeAttribute("highlighted");
+ },
+
+ /**
+ * Raise the toolbox host.
+ */
+ raise: function () {
+ this.postMessage({
+ name: "raise-host"
+ });
+ },
+
+ /**
+ * Refresh the host's title.
+ */
+ _refreshHostTitle: function () {
+ let title;
+ if (this.target.name && this.target.name != this.target.url) {
+ title = L10N.getFormatStr("toolbox.titleTemplate2", this.target.name,
+ this.target.url);
+ } else {
+ title = L10N.getFormatStr("toolbox.titleTemplate1", this.target.url);
+ }
+ this.postMessage({
+ name: "set-host-title",
+ title
+ });
+ },
+
+ // Returns an instance of the preference actor
+ get _preferenceFront() {
+ return this.target.root.then(rootForm => {
+ return getPreferenceFront(this.target.client, rootForm);
+ });
+ },
+
+ _toggleAutohide: Task.async(function* () {
+ let prefName = "ui.popup.disable_autohide";
+ let front = yield this._preferenceFront;
+ let current = yield front.getBoolPref(prefName);
+ yield front.setBoolPref(prefName, !current);
+
+ this._updateNoautohideButton();
+ }),
+
+ _updateNoautohideButton: Task.async(function* () {
+ let menu = this.doc.getElementById("command-button-noautohide");
+ if (menu.getAttribute("hidden") === "true") {
+ return;
+ }
+ if (!this.target.root) {
+ return;
+ }
+ let prefName = "ui.popup.disable_autohide";
+ let front = yield this._preferenceFront;
+ let current = yield front.getBoolPref(prefName);
+ if (current) {
+ menu.setAttribute("checked", "true");
+ } else {
+ menu.removeAttribute("checked");
+ }
+ }),
+
+ _listFrames: function (event) {
+ if (!this._target.activeTab || !this._target.activeTab.traits.frames) {
+ // We are not targetting a regular TabActor
+ // it can be either an addon or browser toolbox actor
+ return promise.resolve();
+ }
+ let packet = {
+ to: this._target.form.actor,
+ type: "listFrames"
+ };
+ return this._target.client.request(packet, resp => {
+ this._updateFrames(null, { frames: resp.frames });
+ });
+ },
+
+ /**
+ * Show a drop down menu that allows the user to switch frames.
+ */
+ showFramesMenu: function (event) {
+ let menu = new Menu();
+ let target = event.target;
+
+ // Generate list of menu items from the list of frames.
+ this.frameMap.forEach(frame => {
+ // A frame is checked if it's the selected one.
+ let checked = frame.id == this.selectedFrameId;
+
+ // Create menu item.
+ menu.append(new MenuItem({
+ label: frame.url,
+ type: "radio",
+ checked,
+ click: () => {
+ this.onSelectFrame(frame.id);
+ }
+ }));
+ });
+
+ menu.once("open").then(() => {
+ target.setAttribute("open", "true");
+ });
+
+ menu.once("close").then(() => {
+ target.removeAttribute("open");
+ });
+
+ // Show a drop down menu with frames.
+ // XXX Missing menu API for specifying target (anchor)
+ // and relative position to it. See also:
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
+ let rect = target.getBoundingClientRect();
+ let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+ let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+ menu.popup(rect.left + screenX, rect.bottom + screenY, this);
+
+ return menu;
+ },
+
+ /**
+ * Select a frame by sending 'switchToFrame' packet to the backend.
+ */
+ onSelectFrame: function (frameId) {
+ // Send packet to the backend to select specified frame and
+ // wait for 'frameUpdate' event packet to update the UI.
+ let packet = {
+ to: this._target.form.actor,
+ type: "switchToFrame",
+ windowId: frameId
+ };
+ this._target.client.request(packet);
+ },
+
+ /**
+ * A handler for 'frameUpdate' packets received from the backend.
+ * Following properties might be set on the packet:
+ *
+ * destroyAll {Boolean}: All frames have been destroyed.
+ * selected {Number}: A frame has been selected
+ * frames {Array}: list of frames. Every frame can have:
+ * id {Number}: frame ID
+ * url {String}: frame URL
+ * title {String}: frame title
+ * destroy {Boolean}: Set to true if destroyed
+ * parentID {Number}: ID of the parent frame (not set
+ * for top level window)
+ */
+ _updateFrames: function (event, data) {
+ if (!Services.prefs.getBoolPref("devtools.command-button-frames.enabled")) {
+ return;
+ }
+
+ // We may receive this event before the toolbox is ready.
+ if (!this.isReady) {
+ return;
+ }
+
+ // Store (synchronize) data about all existing frames on the backend
+ if (data.destroyAll) {
+ this.frameMap.clear();
+ this.selectedFrameId = null;
+ } else if (data.selected) {
+ this.selectedFrameId = data.selected;
+ } else if (data.frames) {
+ data.frames.forEach(frame => {
+ if (frame.destroy) {
+ this.frameMap.delete(frame.id);
+
+ // Reset the currently selected frame if it's destroyed.
+ if (this.selectedFrameId == frame.id) {
+ this.selectedFrameId = null;
+ }
+ } else {
+ this.frameMap.set(frame.id, frame);
+ }
+ });
+ }
+
+ // If there is no selected frame select the first top level
+ // frame by default. Note that there might be more top level
+ // frames in case of the BrowserToolbox.
+ if (!this.selectedFrameId) {
+ let frames = [...this.frameMap.values()];
+ let topFrames = frames.filter(frame => !frame.parentID);
+ this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
+ }
+
+ // Check out whether top frame is currently selected.
+ // Note that only child frame has parentID.
+ let frame = this.frameMap.get(this.selectedFrameId);
+ let topFrameSelected = frame ? !frame.parentID : false;
+ let button = this.doc.getElementById("command-button-frames");
+ button.removeAttribute("checked");
+
+ // If non-top level frame is selected the toolbar button is
+ // marked as 'checked' indicating that a child frame is active.
+ if (!topFrameSelected && this.selectedFrameId) {
+ button.setAttribute("checked", "true");
+ }
+ },
+
+ /**
+ * Switch to the last used host for the toolbox UI.
+ */
+ switchToPreviousHost: function () {
+ return this.switchHost("previous");
+ },
+
+ /**
+ * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
+ * and focus the window when done.
+ *
+ * @param {string} hostType
+ * The host type of the new host object
+ */
+ switchHost: function (hostType) {
+ if (hostType == this.hostType || !this._target.isLocalTab) {
+ return null;
+ }
+
+ this.emit("host-will-change", hostType);
+
+ // ToolboxHostManager is going to call swapFrameLoaders which mess up with
+ // focus. We have to blur before calling it in order to be able to restore
+ // the focus after, in _onSwitchedHost.
+ this.focusTool(this.currentToolId, false);
+
+ // Host code on the chrome side will send back a message once the host
+ // switched
+ this.postMessage({
+ name: "switch-host",
+ hostType
+ });
+
+ return this.once("host-changed");
+ },
+
+ _onSwitchedHost: function ({ hostType }) {
+ this._hostType = hostType;
+
+ this._buildDockButtons();
+ this._addKeysToWindow();
+
+ // We blurred the tools at start of switchHost, but also when clicking on
+ // host switching button. We now have to restore the focus.
+ this.focusTool(this.currentToolId, true);
+
+ this.emit("host-changed");
+ this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
+ },
+
+ /**
+ * Return if the tool is available as a tab (i.e. if it's checked
+ * in the options panel). This is different from Toolbox.getPanel -
+ * a tool could be registered but not yet opened in which case
+ * isToolRegistered would return true but getPanel would return false.
+ */
+ isToolRegistered: function (toolId) {
+ return gDevTools.getToolDefinitionMap().has(toolId);
+ },
+
+ /**
+ * Handler for the tool-registered event.
+ * @param {string} event
+ * Name of the event ("tool-registered")
+ * @param {string} toolId
+ * Id of the tool that was registered
+ */
+ _toolRegistered: function (event, toolId) {
+ let tool = gDevTools.getToolDefinition(toolId);
+ this._buildTabForTool(tool);
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools
+ this.emit("tool-registered", toolId);
+ },
+
+ /**
+ * Handler for the tool-unregistered event.
+ * @param {string} event
+ * Name of the event ("tool-unregistered")
+ * @param {string|object} toolId
+ * Definition or id of the tool that was unregistered. Passing the
+ * tool id should be avoided as it is a temporary measure.
+ */
+ _toolUnregistered: function (event, toolId) {
+ if (typeof toolId != "string") {
+ toolId = toolId.id;
+ }
+
+ if (this._toolPanels.has(toolId)) {
+ let instance = this._toolPanels.get(toolId);
+ instance.destroy();
+ this._toolPanels.delete(toolId);
+ }
+
+ let radio = this.doc.getElementById("toolbox-tab-" + toolId);
+ let panel = this.doc.getElementById("toolbox-panel-" + toolId);
+
+ if (radio) {
+ if (this.currentToolId == toolId) {
+ let nextToolName = null;
+ if (radio.nextSibling) {
+ nextToolName = radio.nextSibling.getAttribute("toolid");
+ }
+ if (radio.previousSibling) {
+ nextToolName = radio.previousSibling.getAttribute("toolid");
+ }
+ if (nextToolName) {
+ this.selectTool(nextToolName);
+ }
+ }
+ radio.parentNode.removeChild(radio);
+ }
+
+ if (panel) {
+ panel.parentNode.removeChild(panel);
+ }
+
+ if (this.hostType == Toolbox.HostType.WINDOW) {
+ let doc = this.win.parent.document;
+ let key = doc.getElementById("key_" + toolId);
+ if (key) {
+ key.parentNode.removeChild(key);
+ }
+ }
+ // Emit the event so tools can listen to it from the toolbox level
+ // instead of gDevTools
+ this.emit("tool-unregistered", toolId);
+ },
+
+ /**
+ * Initialize the inspector/walker/selection/highlighter fronts.
+ * Returns a promise that resolves when the fronts are initialized
+ */
+ initInspector: function () {
+ if (!this._initInspector) {
+ this._initInspector = Task.spawn(function* () {
+ this._inspector = InspectorFront(this._target.client, this._target.form);
+ let pref = "devtools.inspector.showAllAnonymousContent";
+ let showAllAnonymousContent = Services.prefs.getBoolPref(pref);
+ this._walker = yield this._inspector.getWalker({ showAllAnonymousContent });
+ this._selection = new Selection(this._walker);
+
+ if (this.highlighterUtils.isRemoteHighlightable()) {
+ this.walker.on("highlighter-ready", this._highlighterReady);
+ this.walker.on("highlighter-hide", this._highlighterHidden);
+
+ let autohide = !flags.testing;
+ this._highlighter = yield this._inspector.getHighlighter(autohide);
+ }
+ }.bind(this));
+ }
+ return this._initInspector;
+ },
+
+ /**
+ * Destroy the inspector/walker/selection fronts
+ * Returns a promise that resolves when the fronts are destroyed
+ */
+ destroyInspector: function () {
+ if (this._destroyingInspector) {
+ return this._destroyingInspector;
+ }
+
+ this._destroyingInspector = Task.spawn(function* () {
+ if (!this._inspector) {
+ return;
+ }
+
+ // Releasing the walker (if it has been created)
+ // This can fail, but in any case, we want to continue destroying the
+ // inspector/highlighter/selection
+ // FF42+: Inspector actor starts managing Walker actor and auto destroy it.
+ if (this._walker && !this.walker.traits.autoReleased) {
+ try {
+ yield this._walker.release();
+ } catch (e) {
+ // Do nothing;
+ }
+ }
+
+ yield this.highlighterUtils.stopPicker();
+ yield this._inspector.destroy();
+ if (this._highlighter) {
+ // Note that if the toolbox is closed, this will work fine, but will fail
+ // in case the browser is closed and will trigger a noSuchActor message.
+ // We ignore the promise that |_hideBoxModel| returns, since we should still
+ // proceed with the rest of destruction if it fails.
+ // FF42+ now does the cleanup from the actor.
+ if (!this.highlighter.traits.autoHideOnDestroy) {
+ this.highlighterUtils.unhighlight();
+ }
+ yield this._highlighter.destroy();
+ }
+ if (this._selection) {
+ this._selection.destroy();
+ }
+
+ if (this.walker) {
+ this.walker.off("highlighter-ready", this._highlighterReady);
+ this.walker.off("highlighter-hide", this._highlighterHidden);
+ }
+
+ this._inspector = null;
+ this._highlighter = null;
+ this._selection = null;
+ this._walker = null;
+ }.bind(this));
+ return this._destroyingInspector;
+ },
+
+ /**
+ * Get the toolbox's notification component
+ *
+ * @return The notification box component.
+ */
+ getNotificationBox: function () {
+ return this.notificationBox;
+ },
+
+ /**
+ * Remove all UI elements, detach from target and clear up
+ */
+ destroy: function () {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+ let deferred = defer();
+ this._destroyer = deferred.promise;
+
+ this.emit("destroy");
+
+ this._target.off("navigate", this._refreshHostTitle);
+ this._target.off("frame-update", this._updateFrames);
+ this.off("select", this._refreshHostTitle);
+ this.off("host-changed", this._refreshHostTitle);
+ this.off("ready", this._showDevEditionPromo);
+
+ gDevTools.off("tool-registered", this._toolRegistered);
+ gDevTools.off("tool-unregistered", this._toolUnregistered);
+
+ gDevTools.off("pref-changed", this._prefChanged);
+
+ this._lastFocusedElement = null;
+ if (this._sourceMapService) {
+ this._sourceMapService.destroy();
+ this._sourceMapService = null;
+ }
+
+ if (this.webconsolePanel) {
+ this._saveSplitConsoleHeight();
+ this.webconsolePanel.removeEventListener("resize",
+ this._saveSplitConsoleHeight);
+ this.webconsolePanel = null;
+ }
+ if (this.closeButton) {
+ this.closeButton.removeEventListener("click", this.destroy, true);
+ this.closeButton = null;
+ }
+ if (this.textBoxContextMenuPopup) {
+ this.textBoxContextMenuPopup.removeEventListener("popupshowing",
+ this._updateTextBoxMenuItems, true);
+ this.textBoxContextMenuPopup = null;
+ }
+ if (this.tabbar) {
+ this.tabbar.removeEventListener("focus", this._onTabbarFocus, true);
+ this.tabbar.removeEventListener("click", this._onTabbarFocus, true);
+ this.tabbar.removeEventListener("keypress", this._onTabbarArrowKeypress);
+ this.tabbar = null;
+ }
+
+ let outstanding = [];
+ for (let [id, panel] of this._toolPanels) {
+ try {
+ gDevTools.emit(id + "-destroy", this, panel);
+ this.emit(id + "-destroy", panel);
+
+ outstanding.push(panel.destroy());
+ } catch (e) {
+ // We don't want to stop here if any panel fail to close.
+ console.error("Panel " + id + ":", e);
+ }
+ }
+
+ this.browserRequire = null;
+
+ // Now that we are closing the toolbox we can re-enable the cache settings
+ // and disable the service workers testing settings for the current tab.
+ // FF41+ automatically cleans up state in actor on disconnect.
+ if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
+ this.target.activeTab.reconfigure({
+ "cacheDisabled": false,
+ "serviceWorkersTestingEnabled": false
+ });
+ }
+
+ // Destroying the walker and inspector fronts
+ outstanding.push(this.destroyInspector().then(() => {
+ // Removing buttons
+ if (this._pickerButton) {
+ this._pickerButton.removeEventListener("click", this._togglePicker, false);
+ this._pickerButton = null;
+ }
+ }));
+
+ // Destroy the profiler connection
+ outstanding.push(this.destroyPerformance());
+
+ // Detach the thread
+ detachThread(this._threadClient);
+ this._threadClient = null;
+
+ // We need to grab a reference to win before this._host is destroyed.
+ let win = this.win;
+
+ if (this._requisition) {
+ CommandUtils.destroyRequisition(this._requisition, this.target);
+ }
+ this._telemetry.toolClosed("toolbox");
+ this._telemetry.destroy();
+
+ // Finish all outstanding tasks (which means finish destroying panels and
+ // then destroying the host, successfully or not) before destroying the
+ // target.
+ deferred.resolve(settleAll(outstanding)
+ .catch(console.error)
+ .then(() => {
+ this._removeHostListeners();
+
+ // `location` may already be null if the toolbox document is already
+ // in process of destruction. Otherwise if it is still around, ensure
+ // releasing toolbox document and triggering cleanup thanks to unload
+ // event. We do that precisely here, before nullifying the target as
+ // various cleanup code depends on the target attribute to be still
+ // defined.
+ if (win.location) {
+ win.location.replace("about:blank");
+ }
+
+ // Targets need to be notified that the toolbox is being torn down.
+ // This is done after other destruction tasks since it may tear down
+ // fronts and the debugger transport which earlier destroy methods may
+ // require to complete.
+ if (!this._target) {
+ return null;
+ }
+ let target = this._target;
+ this._target = null;
+ this.highlighterUtils.release();
+ target.off("close", this.destroy);
+ return target.destroy();
+ }, console.error).then(() => {
+ this.emit("destroyed");
+
+ // Free _host after the call to destroyed in order to let a chance
+ // to destroyed listeners to still query toolbox attributes
+ this._host = null;
+ this._win = null;
+ this._toolPanels.clear();
+
+ // Force GC to prevent long GC pauses when running tests and to free up
+ // memory in general when the toolbox is closed.
+ if (flags.testing) {
+ win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .garbageCollect();
+ }
+ }).then(null, console.error));
+
+ let leakCheckObserver = ({wrappedJSObject: barrier}) => {
+ // Make the leak detector wait until this toolbox is properly destroyed.
+ barrier.client.addBlocker("DevTools: Wait until toolbox is destroyed",
+ this._destroyer);
+ };
+
+ let topic = "shutdown-leaks-before-check";
+ Services.obs.addObserver(leakCheckObserver, topic, false);
+ this._destroyer.then(() => {
+ Services.obs.removeObserver(leakCheckObserver, topic);
+ });
+
+ return this._destroyer;
+ },
+
+ _highlighterReady: function () {
+ this.emit("highlighter-ready");
+ },
+
+ _highlighterHidden: function () {
+ this.emit("highlighter-hide");
+ },
+
+ /**
+ * For displaying the promotional Doorhanger on first opening of
+ * the developer tools, promoting the Developer Edition.
+ */
+ _showDevEditionPromo: function () {
+ // Do not display in browser toolbox
+ if (this.target.chrome) {
+ return;
+ }
+ showDoorhanger({ window: this.win, type: "deveditionpromo" });
+ },
+
+ /**
+ * Enable / disable necessary textbox menu items using globalOverlay.js.
+ */
+ _updateTextBoxMenuItems: function () {
+ let window = this.win;
+ ["cmd_undo", "cmd_delete", "cmd_cut",
+ "cmd_copy", "cmd_paste", "cmd_selectAll"].forEach(window.goUpdateCommand);
+ },
+
+ /**
+ * Open the textbox context menu at given coordinates.
+ * Panels in the toolbox can call this on contextmenu events with event.screenX/Y
+ * instead of having to implement their own copy/paste/selectAll menu.
+ * @param {Number} x
+ * @param {Number} y
+ */
+ openTextBoxContextMenu: function (x, y) {
+ this.textBoxContextMenuPopup.openPopupAtScreen(x, y, true);
+ },
+
+ /**
+ * Connects to the SPS profiler when the developer tools are open. This is
+ * necessary because of the WebConsole's `profile` and `profileEnd` methods.
+ */
+ initPerformance: Task.async(function* () {
+ // If target does not have profiler actor (addons), do not
+ // even register the shared performance connection.
+ if (!this.target.hasActor("profiler")) {
+ return promise.resolve();
+ }
+
+ if (this._performanceFrontConnection) {
+ return this._performanceFrontConnection.promise;
+ }
+
+ this._performanceFrontConnection = defer();
+ this._performance = createPerformanceFront(this._target);
+ yield this.performance.connect();
+
+ // Emit an event when connected, but don't wait on startup for this.
+ this.emit("profiler-connected");
+
+ this.performance.on("*", this._onPerformanceFrontEvent);
+ this._performanceFrontConnection.resolve(this.performance);
+ return this._performanceFrontConnection.promise;
+ }),
+
+ /**
+ * Disconnects the underlying Performance actor. If the connection
+ * has not finished initializing, as opening a toolbox does not wait,
+ * the performance connection destroy method will wait for it on its own.
+ */
+ destroyPerformance: Task.async(function* () {
+ if (!this.performance) {
+ return;
+ }
+ // If still connecting to performance actor, allow the
+ // actor to resolve its connection before attempting to destroy.
+ if (this._performanceFrontConnection) {
+ yield this._performanceFrontConnection.promise;
+ }
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ yield this.performance.destroy();
+ this._performance = null;
+ }),
+
+ /**
+ * Called when any event comes from the PerformanceFront. If the performance tool is
+ * already loaded when the first event comes in, immediately unbind this handler, as
+ * this is only used to queue up observed recordings before the performance tool can
+ * handle them, which will only occur when `console.profile()` recordings are started
+ * before the tool loads.
+ */
+ _onPerformanceFrontEvent: Task.async(function* (eventName, recording) {
+ if (this.getPanel("performance")) {
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ return;
+ }
+
+ this._performanceQueuedRecordings = this._performanceQueuedRecordings || [];
+ let recordings = this._performanceQueuedRecordings;
+
+ // Before any console recordings, we'll get a `console-profile-start` event
+ // warning us that a recording will come later (via `recording-started`), so
+ // start to boot up the tool and populate the tool with any other recordings
+ // observed during that time.
+ if (eventName === "console-profile-start" && !this._performanceToolOpenedViaConsole) {
+ this._performanceToolOpenedViaConsole = this.loadTool("performance");
+ let panel = yield this._performanceToolOpenedViaConsole;
+ yield panel.open();
+
+ panel.panelWin.PerformanceController.populateWithRecordings(recordings);
+ this.performance.off("*", this._onPerformanceFrontEvent);
+ }
+
+ // Otherwise, if it's a recording-started event, we've already started loading
+ // the tool, so just store this recording in our array to be later populated
+ // once the tool loads.
+ if (eventName === "recording-started") {
+ recordings.push(recording);
+ }
+ }),
+
+ /**
+ * Returns gViewSourceUtils for viewing source.
+ */
+ get gViewSourceUtils() {
+ return this.win.gViewSourceUtils;
+ },
+
+ /**
+ * Opens source in style editor. Falls back to plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInStyleEditor: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInStyleEditor(this, sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in debugger. Falls back to plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInDebugger: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInDebugger(this, sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in scratchpad. Falls back to plain "view-source:".
+ * TODO The `sourceURL` for scratchpad instances are like `Scratchpad/1`.
+ * If instances are scoped one-per-browser-window, then we should be able
+ * to infer the URL from this toolbox, or use the built in scratchpad IN
+ * the toolbox.
+ *
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSourceInScratchpad: function (sourceURL, sourceLine) {
+ return viewSource.viewSourceInScratchpad(sourceURL, sourceLine);
+ },
+
+ /**
+ * Opens source in plain "view-source:".
+ * @see devtools/client/shared/source-utils.js
+ */
+ viewSource: function (sourceURL, sourceLine) {
+ return viewSource.viewSource(this, sourceURL, sourceLine);
+ },
+};
diff --git a/devtools/client/framework/toolbox.xul b/devtools/client/framework/toolbox.xul
new file mode 100644
index 000000000..94aaecebd
--- /dev/null
+++ b/devtools/client/framework/toolbox.xul
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/toolbox.css" type="text/css"?>
+<?xml-stylesheet href="resource://devtools/client/shared/components/notification-box.css" type="text/css"?>
+
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
+%toolboxDTD;
+<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuStrings;
+<!ENTITY % globalKeysDTD SYSTEM "chrome://global/locale/globalKeys.dtd">
+%globalKeysDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/viewSourceUtils.js"/>
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript;version=1.8"
+ src="chrome://devtools/content/framework/toolbox-init.js"/>
+
+ <commandset id="editMenuCommands"/>
+ <keyset id="editMenuKeys"/>
+
+ <popupset>
+ <menupopup id="toolbox-textbox-context-popup">
+ <menuitem id="cMenu_undo"/>
+ <menuseparator/>
+ <menuitem id="cMenu_cut"/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="cMenu_paste"/>
+ <menuitem id="cMenu_delete"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+ </popupset>
+
+ <vbox id="toolbox-container" flex="1">
+ <div xmlns="http://www.w3.org/1999/xhtml" id="toolbox-notificationbox"/>
+ <toolbar class="devtools-tabbar">
+ <hbox id="toolbox-picker-container" />
+ <hbox id="toolbox-tabs" flex="1" role="tablist" />
+ <hbox id="toolbox-buttons" pack="end">
+ <html:button id="command-button-frames"
+ class="command-button command-button-invertable devtools-button"
+ title="&toolboxFramesTooltip;"
+ hidden="true" />
+ <html:button id="command-button-noautohide"
+ class="command-button command-button-invertable devtools-button"
+ title="&toolboxNoAutoHideTooltip;"
+ hidden="true" />
+ </hbox>
+ <vbox id="toolbox-controls-separator" class="devtools-separator"/>
+ <hbox id="toolbox-option-container"/>
+ <hbox id="toolbox-controls">
+ <hbox id="toolbox-dock-buttons"/>
+ <html:button id="toolbox-close"
+ class="devtools-button"
+ title="&toolboxCloseButton.tooltip;"/>
+ </hbox>
+ </toolbar>
+ <vbox flex="1" class="theme-body">
+ <!-- Set large flex to allow the toolbox-panel-webconsole to have a
+ height set to a small value without flexing to fill up extra
+ space. There must be a flex on both to ensure that the console
+ panel itself is sized properly -->
+ <box id="toolbox-deck" flex="1000" minheight="75" />
+ <splitter id="toolbox-console-splitter" class="devtools-horizontal-splitter" hidden="true" />
+ <box minheight="75" flex="1" id="toolbox-panel-webconsole" collapsed="true" />
+ </vbox>
+ <tooltip id="aHTMLTooltip" page="true" />
+ </vbox>
+</window>