diff options
Diffstat (limited to 'devtools/client/debugger/test/mochitest/head.js')
-rw-r--r-- | devtools/client/debugger/test/mochitest/head.js | 1351 |
1 files changed, 1351 insertions, 0 deletions
diff --git a/devtools/client/debugger/test/mochitest/head.js b/devtools/client/debugger/test/mochitest/head.js new file mode 100644 index 000000000..1f9d38b82 --- /dev/null +++ b/devtools/client/debugger/test/mochitest/head.js @@ -0,0 +1,1351 @@ +/* -*- 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"; + +// shared-head.js handles imports, constants, and utility functions +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this); + +// Disable logging for faster test runs. Set this pref to true if you want to +// debug a test in your try runs. Both the debugger server and frontend will +// be affected by this pref. +var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +Services.prefs.setBoolPref("devtools.debugger.log", false); + +var { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {}); +var { DebuggerServer } = require("devtools/server/main"); +var { DebuggerClient, ObjectClient } = require("devtools/shared/client/main"); +var { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); +var EventEmitter = require("devtools/shared/event-emitter"); +var { Toolbox } = require("devtools/client/framework/toolbox"); + +const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry); + +// Override promise with deprecated-sync-thenables +promise = Cu.import("resource://devtools/shared/deprecated-sync-thenables.js", {}).Promise; + +const EXAMPLE_URL = "http://example.com/browser/devtools/client/debugger/test/mochitest/"; +const FRAME_SCRIPT_URL = getRootDirectory(gTestPath) + "code_frame-script.js"; +const CHROME_URL = "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/"; +const CHROME_URI = Services.io.newURI(CHROME_URL, null, null); + +Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); + +registerCleanupFunction(function* () { + Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); + + info("finish() was called, cleaning up..."); + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + + while (gBrowser && gBrowser.tabs && gBrowser.tabs.length > 1) { + info("Destroying toolbox."); + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + info("Removing tab."); + gBrowser.removeCurrentTab(); + } + + // Properly shut down the server to avoid memory leaks. + DebuggerServer.destroy(); + + // Debugger tests use a lot of memory, so force a GC to help fragmentation. + info("Forcing GC after debugger test."); + Cu.forceGC(); +}); + +// Import the GCLI test helper +var testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +testDir = testDir.replace(/\/\//g, "/"); +testDir = testDir.replace("chrome:/mochitest", "chrome://mochitest"); +var helpersjs = testDir + "/../../../commandline/test/helpers.js"; +Services.scriptloader.loadSubScript(helpersjs, this); + +function addWindow(aUrl) { + info("Adding window: " + aUrl); + return promise.resolve(getChromeWindow(window.open(aUrl))); +} + +function getChromeWindow(aWindow) { + return aWindow + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); +} + +// Override addTab/removeTab as defined by shared-head, since these have +// an extra window parameter and add a frame script +this.addTab = function addTab(aUrl, aWindow) { + info("Adding tab: " + aUrl); + + let deferred = promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + + targetWindow.focus(); + let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); + let linkedBrowser = tab.linkedBrowser; + + info("Loading frame script with url " + FRAME_SCRIPT_URL + "."); + linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + + BrowserTestUtils.browserLoaded(linkedBrowser) + .then(function () { + info("Tab added and finished loading: " + aUrl); + deferred.resolve(tab); + }); + + return deferred.promise; +}; + +this.removeTab = function removeTab(aTab, aWindow) { + info("Removing tab."); + + let deferred = promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + let tabContainer = targetBrowser.tabContainer; + + tabContainer.addEventListener("TabClose", function onClose(aEvent) { + tabContainer.removeEventListener("TabClose", onClose, false); + + info("Tab removed and finished closing."); + deferred.resolve(); + }, false); + + targetBrowser.removeTab(aTab); + return deferred.promise; +}; + +function getAddonURIFromPath(aPath) { + let chromeURI = Services.io.newURI(aPath, null, CHROME_URI); + return chromeRegistry.convertChromeURL(chromeURI).QueryInterface(Ci.nsIFileURL); +} + +function getTemporaryAddonURLFromPath(aPath) { + return getAddonURIFromPath(aPath).spec; +} + +function addTemporaryAddon(aPath) { + let addonFile = getAddonURIFromPath(aPath).file; + info("Installing addon: " + addonFile.path); + + return AddonManager.installTemporaryAddon(addonFile); +} + +function removeAddon(aAddon) { + info("Removing addon."); + + let deferred = promise.defer(); + + let listener = { + onUninstalled: function (aUninstalledAddon) { + if (aUninstalledAddon != aAddon) { + return; + } + AddonManager.removeAddonListener(listener); + deferred.resolve(); + } + }; + AddonManager.addAddonListener(listener); + aAddon.uninstall(); + + return deferred.promise; +} + +function getTabActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); + + aClient.listTabs(aResponse => { + let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop(); + deferred.resolve(tabActor); + }); + + return deferred.promise; +} + +function getAddonActorForId(aClient, aAddonId) { + info("Get addon actor for ID: " + aAddonId); + let deferred = promise.defer(); + + aClient.listAddons(aResponse => { + let addonActor = aResponse.addons.filter(aGrip => aGrip.id == aAddonId).pop(); + info("got addon actor for ID: " + aAddonId); + deferred.resolve(addonActor); + }); + + return deferred.promise; +} + +function attachTabActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); + + getTabActorForUrl(aClient, aUrl).then(aGrip => { + aClient.attachTab(aGrip.actor, aResponse => { + deferred.resolve([aGrip, aResponse]); + }); + }); + + return deferred.promise; +} + +function attachThreadActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); + + attachTabActorForUrl(aClient, aUrl).then(([aGrip, aResponse]) => { + aClient.attachThread(aResponse.threadActor, (aResponse, aThreadClient) => { + aThreadClient.resume(aResponse => { + deferred.resolve(aThreadClient); + }); + }); + }); + + return deferred.promise; +} + +function once(aTarget, aEventName, aUseCapture = false) { + info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in aTarget) && (remove in aTarget)) { + aTarget[add](aEventName, function onEvent(...aArgs) { + aTarget[remove](aEventName, onEvent, aUseCapture); + deferred.resolve.apply(deferred, aArgs); + }, aUseCapture); + break; + } + } + + return deferred.promise; +} + +function waitForTick() { + let deferred = promise.defer(); + executeSoon(deferred.resolve); + return deferred.promise; +} + +function waitForTime(aDelay) { + let deferred = promise.defer(); + setTimeout(deferred.resolve, aDelay); + return deferred.promise; +} + +function waitForSourceLoaded(aPanel, aUrl) { + let { Sources } = aPanel.panelWin.DebuggerView; + let isLoaded = Sources.items.some(item => + item.attachment.source.url === aUrl); + if (isLoaded) { + info("The correct source has been loaded."); + return promise.resolve(null); + } else { + return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.NEW_SOURCE).then(() => { + // Wait for it to be loaded in the UI and appear into Sources.items. + return waitForTick(); + }).then(() => { + return waitForSourceLoaded(aPanel, aUrl); + }); + } + +} + +function waitForSourceShown(aPanel, aUrl) { + return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.SOURCE_SHOWN).then(aSource => { + let sourceUrl = aSource.url || aSource.introductionUrl; + info("Source shown: " + sourceUrl); + + if (!sourceUrl.includes(aUrl)) { + return waitForSourceShown(aPanel, aUrl); + } else { + ok(true, "The correct source has been shown."); + } + }); +} + +function waitForEditorLocationSet(aPanel) { + return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET); +} + +function ensureSourceIs(aPanel, aUrlOrSource, aWaitFlag = false) { + let sources = aPanel.panelWin.DebuggerView.Sources; + + if (sources.selectedValue === aUrlOrSource || + (sources.selectedItem && + sources.selectedItem.attachment.source.url.includes(aUrlOrSource))) { + ok(true, "Expected source is shown: " + aUrlOrSource); + return promise.resolve(null); + } + if (aWaitFlag) { + return waitForSourceShown(aPanel, aUrlOrSource); + } + ok(false, "Expected source was not already shown: " + aUrlOrSource); + return promise.reject(null); +} + +function waitForCaretUpdated(aPanel, aLine, aCol = 1) { + return waitForEditorEvents(aPanel, "cursorActivity").then(() => { + let cursor = aPanel.panelWin.DebuggerView.editor.getCursor(); + info("Caret updated: " + (cursor.line + 1) + ", " + (cursor.ch + 1)); + + if (!isCaretPos(aPanel, aLine, aCol)) { + return waitForCaretUpdated(aPanel, aLine, aCol); + } else { + ok(true, "The correct caret position has been set."); + } + }); +} + +function ensureCaretAt(aPanel, aLine, aCol = 1, aWaitFlag = false) { + if (isCaretPos(aPanel, aLine, aCol)) { + ok(true, "Expected caret position is set: " + aLine + "," + aCol); + return promise.resolve(null); + } + if (aWaitFlag) { + return waitForCaretUpdated(aPanel, aLine, aCol); + } + ok(false, "Expected caret position was not already set: " + aLine + "," + aCol); + return promise.reject(null); +} + +function isCaretPos(aPanel, aLine, aCol = 1) { + let editor = aPanel.panelWin.DebuggerView.editor; + let cursor = editor.getCursor(); + + // Source editor starts counting line and column numbers from 0. + info("Current editor caret position: " + (cursor.line + 1) + ", " + (cursor.ch + 1)); + return cursor.line == (aLine - 1) && cursor.ch == (aCol - 1); +} + +function isDebugPos(aPanel, aLine) { + let editor = aPanel.panelWin.DebuggerView.editor; + let location = editor.getDebugLocation(); + + // Source editor starts counting line and column numbers from 0. + info("Current editor debug position: " + (location + 1)); + return location != null && editor.hasLineClass(aLine - 1, "debug-line"); +} + +function isEditorSel(aPanel, [start, end]) { + let editor = aPanel.panelWin.DebuggerView.editor; + let range = { + start: editor.getOffset(editor.getCursor("start")), + end: editor.getOffset(editor.getCursor()) + }; + + // Source editor starts counting line and column numbers from 0. + info("Current editor selection: " + (range.start + 1) + ", " + (range.end + 1)); + return range.start == (start - 1) && range.end == (end - 1); +} + +function waitForSourceAndCaret(aPanel, aUrl, aLine, aCol) { + return promise.all([ + waitForSourceShown(aPanel, aUrl), + waitForCaretUpdated(aPanel, aLine, aCol) + ]); +} + +function waitForCaretAndScopes(aPanel, aLine, aCol) { + return promise.all([ + waitForCaretUpdated(aPanel, aLine, aCol), + waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES) + ]); +} + +function waitForSourceAndCaretAndScopes(aPanel, aUrl, aLine, aCol) { + return promise.all([ + waitForSourceAndCaret(aPanel, aUrl, aLine, aCol), + waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.FETCHED_SCOPES) + ]); +} + +function waitForDebuggerEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for debugger event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let panelWin = aPanel.panelWin; + let count = 0; + + panelWin.on(aEventName, function onEvent(aEventName, ...aArgs) { + info("Debugger event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' panel events have been fired."); + panelWin.off(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} + +function waitForEditorEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for editor event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let editor = aPanel.panelWin.DebuggerView.editor; + let count = 0; + + editor.on(aEventName, function onEvent(...aArgs) { + info("Editor event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' editor events have been fired."); + editor.off(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} + +function waitForThreadEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for thread event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let thread = aPanel.panelWin.gThreadClient; + let count = 0; + + thread.addListener(aEventName, function onEvent(aEventName, ...aArgs) { + info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' thread events have been fired."); + thread.removeListener(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} + +function waitForClientEvents(aPanel, aEventName, aEventRepeat = 1) { + info("Waiting for client event: '" + aEventName + "' to fire: " + aEventRepeat + " time(s)."); + + let deferred = promise.defer(); + let client = aPanel.panelWin.gClient; + let count = 0; + + client.addListener(aEventName, function onEvent(aEventName, ...aArgs) { + info("Thread event '" + aEventName + "' fired: " + (++count) + " time(s)."); + + if (count == aEventRepeat) { + ok(true, "Enough '" + aEventName + "' thread events have been fired."); + client.removeListener(aEventName, onEvent); + deferred.resolve.apply(deferred, aArgs); + } + }); + + return deferred.promise; +} + +function ensureThreadClientState(aPanel, aState) { + let thread = aPanel.panelWin.gThreadClient; + let state = thread.state; + + info("Thread is: '" + state + "'."); + + if (state == aState) { + return promise.resolve(null); + } else { + return waitForThreadEvents(aPanel, aState); + } +} + +function reload(aPanel, aUrl) { + let activeTab = aPanel.panelWin.DebuggerController._target.activeTab; + aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload(); +} + +function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) { + let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat); + reload(aPanel, aUrl); + return finished; +} + +function navigateActiveTabInHistory(aPanel, aDirection, aWaitForEventName, aEventRepeat) { + let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat); + content.history[aDirection](); + return finished; +} + +function reloadActiveTab(aPanel, aWaitForEventName, aEventRepeat) { + return navigateActiveTabTo(aPanel, null, aWaitForEventName, aEventRepeat); +} + +function clearText(aElement) { + info("Clearing text..."); + aElement.focus(); + aElement.value = ""; +} + +function setText(aElement, aText) { + clearText(aElement); + info("Setting text: " + aText); + aElement.value = aText; +} + +function typeText(aElement, aText) { + info("Typing text: " + aText); + aElement.focus(); + EventUtils.sendString(aText, aElement.ownerDocument.defaultView); +} + +function backspaceText(aElement, aTimes) { + info("Pressing backspace " + aTimes + " times."); + for (let i = 0; i < aTimes; i++) { + aElement.focus(); + EventUtils.sendKey("BACK_SPACE", aElement.ownerDocument.defaultView); + } +} + +function getTab(aTarget, aWindow) { + if (aTarget instanceof XULElement) { + return promise.resolve(aTarget); + } else { + return addTab(aTarget, aWindow); + } +} + +function getSources(aClient) { + info("Getting sources."); + + let deferred = promise.defer(); + + aClient.getSources((packet) => { + deferred.resolve(packet.sources); + }); + + return deferred.promise; +} + +/** + * Optionaly open a new tab and then open the debugger panel. + * The returned promise resolves only one the panel is fully set. + + * @param {String|xul:tab} urlOrTab + * If a string, consider it as the url of the tab to open before opening the + * debugger panel. + * Otherwise, if a <xul:tab>, do nothing, but open the debugger panel against + * the given tab. + * @param {Object} options + * Set of optional arguments: + * - {String} source + * If given, assert the default loaded source once the debugger is loaded. + * This string can be partial to only match a part of the source name. + * If null, do not expect any source and skip SOURCE_SHOWN wait. + * - {Number} line + * If given, wait for the caret to be set on a precise line + * + * @return {Promise} + * Resolves once debugger panel is fully set according to the given options. + */ +let initDebugger = Task.async(function*(urlOrTab, options) { + let { window, source, line } = options || {}; + info("Initializing a debugger panel."); + + let tab, url; + if (urlOrTab instanceof XULElement) { + // `urlOrTab` Is a Tab. + tab = urlOrTab; + } else { + // `urlOrTab` is an url. Open an empty tab first in order to load the page + // only once the panel is ready. That to be able to safely catch the + // SOURCE_SHOWN event. + tab = yield addTab("about:blank", window); + url = urlOrTab; + } + info("Debugee tab added successfully: " + urlOrTab); + + let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject; + let target = TargetFactory.forTab(tab); + + let toolbox = yield gDevTools.showToolbox(target, "jsdebugger"); + info("Debugger panel shown successfully."); + + let debuggerPanel = toolbox.getCurrentPanel(); + let panelWin = debuggerPanel.panelWin; + let { Sources } = panelWin.DebuggerView; + + prepareDebugger(debuggerPanel); + + if (url && url != "about:blank") { + let onCaretUpdated; + if (line) { + onCaretUpdated = waitForCaretUpdated(debuggerPanel, line); + } + if (source === null) { + // When there is no source in the document, we shouldn't wait for + // SOURCE_SHOWN event + yield reload(debuggerPanel, url); + } else { + yield navigateActiveTabTo(debuggerPanel, + url, + panelWin.EVENTS.SOURCE_SHOWN); + } + if (source) { + let isSelected = Sources.selectedItem.attachment.source.url === source; + if (!isSelected) { + // Ensure that the source is loaded first before trying to select it + yield waitForSourceLoaded(debuggerPanel, source); + // Select the js file. + let onSource = waitForSourceAndCaret(debuggerPanel, source, line ? line : 1); + Sources.selectedValue = getSourceActor(Sources, source); + yield onSource; + } + } + yield onCaretUpdated; + } + + return [tab, debuggee, debuggerPanel, window]; +}); + +// Creates an add-on debugger for a given add-on. The returned AddonDebugger +// object must be destroyed before finishing the test +function initAddonDebugger(aAddonId) { + let addonDebugger = new AddonDebugger(); + return addonDebugger.init(aAddonId).then(() => addonDebugger); +} + +function AddonDebugger() { + this._onMessage = this._onMessage.bind(this); + this._onConsoleAPICall = this._onConsoleAPICall.bind(this); + EventEmitter.decorate(this); +} + +AddonDebugger.prototype = { + init: Task.async(function* (aAddonId) { + info("Initializing an addon debugger panel."); + + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + DebuggerServer.allowChromeProcess = true; + + this.frame = document.createElement("iframe"); + this.frame.setAttribute("height", 400); + document.documentElement.appendChild(this.frame); + window.addEventListener("message", this._onMessage); + + let transport = DebuggerServer.connectPipe(); + this.client = new DebuggerClient(transport); + + yield this.client.connect(); + + let addonActor = yield getAddonActorForId(this.client, aAddonId); + + let targetOptions = { + form: addonActor, + client: this.client, + chrome: true, + isTabActor: false + }; + + let toolboxOptions = { + customIframe: this.frame + }; + + this.target = TargetFactory.forTab(targetOptions); + let toolbox = yield gDevTools.showToolbox(this.target, "jsdebugger", Toolbox.HostType.CUSTOM, toolboxOptions); + + info("Addon debugger panel shown successfully."); + + this.debuggerPanel = toolbox.getCurrentPanel(); + yield waitForSourceShown(this.debuggerPanel, ""); + + prepareDebugger(this.debuggerPanel); + yield this._attachConsole(); + }), + + destroy: Task.async(function* () { + yield this.client.close(); + yield this.debuggerPanel._toolbox.destroy(); + this.frame.remove(); + window.removeEventListener("message", this._onMessage); + }), + + _attachConsole: function () { + let deferred = promise.defer(); + this.client.attachConsole(this.target.form.consoleActor, ["ConsoleAPI"], (aResponse, aWebConsoleClient) => { + if (aResponse.error) { + deferred.reject(aResponse); + } + else { + this.webConsole = aWebConsoleClient; + this.client.addListener("consoleAPICall", this._onConsoleAPICall); + deferred.resolve(); + } + }); + return deferred.promise; + }, + + _onConsoleAPICall: function (aType, aPacket) { + if (aPacket.from != this.webConsole.actor) + return; + this.emit("console", aPacket.message); + }, + + /** + * Returns a list of the groups and sources in the UI. The returned array + * contains objects for each group with properties name and sources. The + * sources property contains an array with objects for each source for that + * group with properties label and url. + */ + getSourceGroups: Task.async(function* () { + let debuggerWin = this.debuggerPanel.panelWin; + let sources = yield getSources(debuggerWin.gThreadClient); + ok(sources.length, "retrieved sources"); + + // groups will be the return value, groupmap and the maps we put in it will + // be used as quick lookups to add the url information in below + let groups = []; + let groupmap = new Map(); + + let uigroups = this.debuggerPanel.panelWin.document.querySelectorAll(".side-menu-widget-group"); + for (let g of uigroups) { + let name = g.querySelector(".side-menu-widget-group-title .name").value; + let group = { + name: name, + sources: [] + }; + groups.push(group); + let labelmap = new Map(); + groupmap.set(name, labelmap); + + for (let l of g.querySelectorAll(".dbg-source-item")) { + let source = { + label: l.value, + url: null + }; + + labelmap.set(l.value, source); + group.sources.push(source); + } + } + + for (let source of sources) { + let { label, group } = debuggerWin.DebuggerView.Sources.getItemByValue(source.actor).attachment; + + if (!groupmap.has(group)) { + ok(false, "Saw a source group not in the UI: " + group); + continue; + } + + if (!groupmap.get(group).has(label)) { + ok(false, "Saw a source label not in the UI: " + label); + continue; + } + + groupmap.get(group).get(label).url = source.url.split(" -> ").pop(); + } + + return groups; + }), + + _onMessage: function (event) { + if (typeof(event.data) !== "string") { + return; + } + let json = JSON.parse(event.data); + switch (json.name) { + case "toolbox-title": + this.title = json.data.value; + break; + } + } +}; + +function initChromeDebugger(aOnClose) { + info("Initializing a chrome debugger process."); + + let deferred = promise.defer(); + + // Wait for the toolbox process to start... + BrowserToolboxProcess.init(aOnClose, (aEvent, aProcess) => { + info("Browser toolbox process started successfully."); + + prepareDebugger(aProcess); + deferred.resolve(aProcess); + }); + + return deferred.promise; +} + +function prepareDebugger(aDebugger) { + if ("target" in aDebugger) { + let view = aDebugger.panelWin.DebuggerView; + view.Variables.lazyEmpty = false; + view.Variables.lazySearch = false; + view.Filtering.FilteredSources._autoSelectFirstItem = true; + view.Filtering.FilteredFunctions._autoSelectFirstItem = true; + } else { + // Nothing to do here yet. + } +} + +function teardown(aPanel, aFlags = {}) { + info("Destroying the specified debugger."); + + let toolbox = aPanel._toolbox; + let tab = aPanel.target.tab; + let debuggerRootActorDisconnected = once(window, "Debugger:Shutdown"); + let debuggerPanelDestroyed = once(aPanel, "destroyed"); + let devtoolsToolboxDestroyed = toolbox.destroy(); + + return promise.all([ + debuggerRootActorDisconnected, + debuggerPanelDestroyed, + devtoolsToolboxDestroyed + ]).then(() => aFlags.noTabRemoval ? null : removeTab(tab)); +} + +function closeDebuggerAndFinish(aPanel, aFlags = {}) { + let thread = aPanel.panelWin.gThreadClient; + if (thread.state == "paused" && !aFlags.whilePaused) { + ok(false, "You should use 'resumeDebuggerThenCloseAndFinish' instead, " + + "unless you're absolutely sure about what you're doing."); + } + return teardown(aPanel, aFlags).then(finish); +} + +function resumeDebuggerThenCloseAndFinish(aPanel, aFlags = {}) { + let deferred = promise.defer(); + let thread = aPanel.panelWin.gThreadClient; + thread.resume(() => closeDebuggerAndFinish(aPanel, aFlags).then(deferred.resolve)); + return deferred.promise; +} + +// Blackboxing helpers + +function getBlackBoxButton(aPanel) { + return aPanel.panelWin.document.getElementById("black-box"); +} + +/** + * Returns the node that has the black-boxed class applied to it. + */ +function getSelectedSourceElement(aPanel) { + return aPanel.panelWin.DebuggerView.Sources.selectedItem.prebuiltNode; +} + +function toggleBlackBoxing(aPanel, aSourceActor = null) { + function clickBlackBoxButton() { + getBlackBoxButton(aPanel).click(); + } + + const blackBoxChanged = waitForDispatch( + aPanel, + aPanel.panelWin.constants.BLACKBOX + ).then(() => { + return aSourceActor ? + getSource(aPanel, aSourceActor) : + getSelectedSource(aPanel); + }); + + if (aSourceActor) { + aPanel.panelWin.DebuggerView.Sources.selectedValue = aSourceActor; + ensureSourceIs(aPanel, aSourceActor, true).then(clickBlackBoxButton); + } else { + clickBlackBoxButton(); + } + + return blackBoxChanged; +} + +function selectSourceAndGetBlackBoxButton(aPanel, aUrl) { + function returnBlackboxButton() { + return getBlackBoxButton(aPanel); + } + + let sources = aPanel.panelWin.DebuggerView.Sources; + sources.selectedValue = getSourceActor(sources, aUrl); + return ensureSourceIs(aPanel, aUrl, true).then(returnBlackboxButton); +} + +// Variables view inspection popup helpers + +function openVarPopup(aPanel, aCoords, aWaitForFetchedProperties) { + let events = aPanel.panelWin.EVENTS; + let editor = aPanel.panelWin.DebuggerView.editor; + let bubble = aPanel.panelWin.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + let popupShown = once(tooltip, "popupshown"); + let fetchedProperties = aWaitForFetchedProperties + ? waitForDebuggerEvents(aPanel, events.FETCHED_BUBBLE_PROPERTIES) + : promise.resolve(null); + let updatedFrame = waitForDebuggerEvents(aPanel, events.FETCHED_SCOPES); + + let { left, top } = editor.getCoordsFromPosition(aCoords); + bubble._findIdentifier(left, top); + return promise.all([popupShown, fetchedProperties, updatedFrame]).then(waitForTick); +} + +// Simulates the mouse hovering a variable in the debugger +// Takes in account the position of the cursor in the text, if the text is +// selected and if a button is currently pushed (aButtonPushed > 0). +// The function returns a promise which returns true if the popup opened or +// false if it didn't +function intendOpenVarPopup(aPanel, aPosition, aButtonPushed) { + let bubble = aPanel.panelWin.DebuggerView.VariableBubble; + let editor = aPanel.panelWin.DebuggerView.editor; + let tooltip = bubble._tooltip; + + let { left, top } = editor.getCoordsFromPosition(aPosition); + + const eventDescriptor = { + clientX: left, + clientY: top, + buttons: aButtonPushed + }; + + bubble._onMouseMove(eventDescriptor); + + const deferred = promise.defer(); + window.setTimeout( + function () { + if (tooltip.isEmpty()) { + deferred.resolve(false); + } else { + deferred.resolve(true); + } + }, + bubble.TOOLTIP_SHOW_DELAY + 1000 + ); + + return deferred.promise; +} + +function hideVarPopup(aPanel) { + let bubble = aPanel.panelWin.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + let popupHiding = once(tooltip, "popuphiding"); + bubble.hideContents(); + return popupHiding.then(waitForTick); +} + +function hideVarPopupByScrollingEditor(aPanel) { + let editor = aPanel.panelWin.DebuggerView.editor; + let bubble = aPanel.panelWin.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + + let popupHiding = once(tooltip, "popuphiding"); + editor.setFirstVisibleLine(0); + return popupHiding.then(waitForTick); +} + +function reopenVarPopup(...aArgs) { + return hideVarPopup.apply(this, aArgs).then(() => openVarPopup.apply(this, aArgs)); +} + +function attachAddonActorForId(aClient, aAddonId) { + let deferred = promise.defer(); + + getAddonActorForId(aClient, aAddonId).then(aGrip => { + aClient.attachAddon(aGrip.actor, aResponse => { + deferred.resolve([aGrip, aResponse]); + }); + }); + + return deferred.promise; +} + +function doResume(aPanel) { + const threadClient = aPanel.panelWin.gThreadClient; + return threadClient.resume(); +} + +function doInterrupt(aPanel) { + const threadClient = aPanel.panelWin.gThreadClient; + return threadClient.interrupt(); +} + +function pushPrefs(...aPrefs) { + let deferred = promise.defer(); + SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve); + return deferred.promise; +} + +function popPrefs() { + let deferred = promise.defer(); + SpecialPowers.popPrefEnv(deferred.resolve); + return deferred.promise; +} + +// Source helpers + +function getSelectedSource(panel) { + const win = panel.panelWin; + return win.queries.getSelectedSource(win.DebuggerController.getState()); +} + +function getSource(panel, actor) { + const win = panel.panelWin; + return win.queries.getSource(win.DebuggerController.getState(), actor); +} + +function getSelectedSourceURL(aSources) { + return (aSources.selectedItem && + aSources.selectedItem.attachment.source.url); +} + +function getSourceURL(aSources, aActor) { + let item = aSources.getItemByValue(aActor); + return item && item.attachment.source.url; +} + +function getSourceActor(aSources, aURL) { + let item = aSources.getItemForAttachment(a => a.source && a.source.url === aURL); + return item && item.value; +} + +function getSourceForm(aSources, aURL) { + let item = aSources.getItemByValue(getSourceActor(aSources, aURL)); + return item.attachment.source; +} + +var nextId = 0; + +function jsonrpc(tab, method, params) { + return new Promise(function (resolve, reject) { + let currentId = nextId++; + let messageManager = tab.linkedBrowser.messageManager; + messageManager.sendAsyncMessage("jsonrpc", { + method: method, + params: params, + id: currentId + }); + messageManager.addMessageListener("jsonrpc", function listener(res) { + const { data: { result, error, id } } = res; + if (id !== currentId) { + return; + } + + messageManager.removeMessageListener("jsonrpc", listener); + if (error != null) { + reject(error); + } + + resolve(result); + }); + }); +} + +function callInTab(tab, name) { + info("Calling function with name '" + name + "' in tab."); + + return jsonrpc(tab, "call", [name, Array.prototype.slice.call(arguments, 2)]); +} + +function evalInTab(tab, string) { + info("Evalling string in tab."); + + return jsonrpc(tab, "_eval", [string]); +} + +function createWorkerInTab(tab, url) { + info("Creating worker with url '" + url + "' in tab."); + + return jsonrpc(tab, "createWorker", [url]); +} + +function terminateWorkerInTab(tab, url) { + info("Terminating worker with url '" + url + "' in tab."); + + return jsonrpc(tab, "terminateWorker", [url]); +} + +function postMessageToWorkerInTab(tab, url, message) { + info("Posting message to worker with url '" + url + "' in tab."); + + return jsonrpc(tab, "postMessageToWorker", [url, message]); +} + +function generateMouseClickInTab(tab, path) { + info("Generating mouse click in tab."); + + return jsonrpc(tab, "generateMouseClick", [path]); +} + +function connect(client) { + info("Connecting client."); + return client.connect(); +} + +function close(client) { + info("Waiting for client to close.\n"); + return client.close(); +} + +function listTabs(client) { + info("Listing tabs."); + return client.listTabs(); +} + +function findTab(tabs, url) { + info("Finding tab with url '" + url + "'."); + for (let tab of tabs) { + if (tab.url === url) { + return tab; + } + } + return null; +} + +function attachTab(client, tab) { + info("Attaching to tab with url '" + tab.url + "'."); + return new Promise(function (resolve) { + client.attachTab(tab.actor, function (response, tabClient) { + resolve([response, tabClient]); + }); + }); +} + +function listWorkers(tabClient) { + info("Listing workers."); + return new Promise(function (resolve) { + tabClient.listWorkers(function (response) { + resolve(response); + }); + }); +} + +function findWorker(workers, url) { + info("Finding worker with url '" + url + "'."); + for (let worker of workers) { + if (worker.url === url) { + return worker; + } + } + return null; +} + +function attachWorker(tabClient, worker) { + info("Attaching to worker with url '" + worker.url + "'."); + return new Promise(function (resolve, reject) { + tabClient.attachWorker(worker.actor, function (response, workerClient) { + resolve([response, workerClient]); + }); + }); +} + +function waitForWorkerListChanged(tabClient) { + info("Waiting for worker list to change."); + return new Promise(function (resolve) { + tabClient.addListener("workerListChanged", function listener() { + tabClient.removeListener("workerListChanged", listener); + resolve(); + }); + }); +} + +function attachThread(workerClient, options) { + info("Attaching to thread."); + return new Promise(function (resolve, reject) { + workerClient.attachThread(options, function (response, threadClient) { + resolve([response, threadClient]); + }); + }); +} + +function waitForWorkerClose(workerClient) { + info("Waiting for worker to close."); + return new Promise(function (resolve) { + workerClient.addOneTimeListener("close", function () { + info("Worker did close."); + resolve(); + }); + }); +} + +function resume(threadClient) { + info("Resuming thread."); + return threadClient.resume(); +} + +function findSource(sources, url) { + info("Finding source with url '" + url + "'.\n"); + for (let source of sources) { + if (source.url === url) { + return source; + } + } + return null; +} + +function waitForEvent(client, type, predicate) { + return new Promise(function (resolve) { + function listener(type, packet) { + if (!predicate(packet)) { + return; + } + client.removeListener(listener); + resolve(packet); + } + + if (predicate) { + client.addListener(type, listener); + } else { + client.addOneTimeListener(type, function (type, packet) { + resolve(packet); + }); + } + }); +} + +function waitForPause(threadClient) { + info("Waiting for pause.\n"); + return waitForEvent(threadClient, "paused"); +} + +function setBreakpoint(sourceClient, location) { + info("Setting breakpoint.\n"); + return sourceClient.setBreakpoint(location); +} + +function source(sourceClient) { + info("Getting source.\n"); + return sourceClient.source(); +} + +// Return a promise with a reference to jsterm, opening the split +// console if necessary. This cleans up the split console pref so +// it won't pollute other tests. +function getSplitConsole(toolbox, win) { + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled"); + }); + + if (!win) { + win = toolbox.win; + } + + if (!toolbox.splitConsole) { + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + } + + return new Promise(resolve => { + toolbox.getPanelWhenReady("webconsole").then(() => { + ok(toolbox.splitConsole, "Split console is shown."); + let jsterm = toolbox.getPanel("webconsole").hud.jsterm; + resolve(jsterm); + }); + }); +} + +// navigation + +function waitForNavigation(gPanel) { + const target = gPanel.panelWin.gTarget; + const deferred = promise.defer(); + target.once("navigate", () => { + deferred.resolve(); + }); + info("Waiting for navigation..."); + return deferred.promise; +} + +// actions + +function bindActionCreators(panel) { + const win = panel.panelWin; + const dispatch = win.DebuggerController.dispatch; + const { bindActionCreators } = win.require("devtools/client/shared/vendor/redux"); + return bindActionCreators(win.actions, dispatch); +} + +// Wait until an action of `type` is dispatched. This is different +// then `_afterDispatchDone` because it doesn't wait for async actions +// to be done/errored. Use this if you want to listen for the "start" +// action of an async operation (somewhat rare). +function waitForNextDispatch(store, type) { + return new Promise(resolve => { + store.dispatch({ + // Normally we would use `services.WAIT_UNTIL`, but use the + // internal name here so tests aren't forced to always pass it + // in + type: "@@service/waitUntil", + predicate: action => action.type === type, + run: (dispatch, getState, action) => { + resolve(action); + } + }); + }); +} + +// Wait until an action of `type` is dispatched. If it's part of an +// async operation, wait until the `status` field is "done" or "error" +function _afterDispatchDone(store, type) { + return new Promise(resolve => { + store.dispatch({ + // Normally we would use `services.WAIT_UNTIL`, but use the + // internal name here so tests aren't forced to always pass it + // in + type: "@@service/waitUntil", + predicate: action => { + if (action.type === type) { + return action.status ? + (action.status === "done" || action.status === "error") : + true; + } + }, + run: (dispatch, getState, action) => { + resolve(action); + } + }); + }); +} + +function waitForDispatch(panel, type, eventRepeat = 1) { + const controller = panel.panelWin.DebuggerController; + const actionType = panel.panelWin.constants[type]; + let count = 0; + + return Task.spawn(function* () { + info("Waiting for " + type + " to dispatch " + eventRepeat + " time(s)"); + while (count < eventRepeat) { + yield _afterDispatchDone(controller, actionType); + count++; + info(type + " dispatched " + count + " time(s)"); + } + }); +} + +function* initWorkerDebugger(TAB_URL, WORKER_URL) { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let tab = yield addTab(TAB_URL); + let { tabs } = yield listTabs(client); + let [, tabClient] = yield attachTab(client, findTab(tabs, TAB_URL)); + + yield createWorkerInTab(tab, WORKER_URL); + + let { workers } = yield listWorkers(tabClient); + let [, workerClient] = yield attachWorker(tabClient, + findWorker(workers, WORKER_URL)); + + let toolbox = yield gDevTools.showToolbox(TargetFactory.forWorker(workerClient), + "jsdebugger", + Toolbox.HostType.WINDOW); + + let debuggerPanel = toolbox.getCurrentPanel(); + let gDebugger = debuggerPanel.panelWin; + + return {client, tab, tabClient, workerClient, toolbox, gDebugger}; +} + |