diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/framework/test | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/framework/test')
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; +} |