summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/test')
-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
84 files changed, 6836 insertions, 0 deletions
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;
+}