/* -*- 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 , 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}; }