diff --git a/browser/devtools/debugger/debugger-controller.js b/browser/devtools/debugger/debugger-controller.js index a2c58bfcea1..360ca70ec57 100644 --- a/browser/devtools/debugger/debugger-controller.js +++ b/browser/devtools/debugger/debugger-controller.js @@ -1714,12 +1714,6 @@ DebuggerController.Breakpoints = new Breakpoints(); * Export some properties to the global scope for easier access. */ Object.defineProperties(window, { - "dispatchEvent": { - get: function() ViewHelpers.dispatchEvent, - }, - "editor": { - get: function() DebuggerView.editor - }, "gTarget": { get: function() DebuggerController._target }, @@ -1729,18 +1723,6 @@ Object.defineProperties(window, { "gThreadClient": { get: function() DebuggerController.activeThread }, - "gThreadState": { - get: function() DebuggerController.ThreadState - }, - "gStackFrames": { - get: function() DebuggerController.StackFrames - }, - "gSourceScripts": { - get: function() DebuggerController.SourceScripts - }, - "gBreakpoints": { - get: function() DebuggerController.Breakpoints - }, "gCallStackPageSize": { get: function() CALL_STACK_PAGE_SIZE } diff --git a/browser/devtools/debugger/test/head.js b/browser/devtools/debugger/test/head.js index fc1046b1144..ad63af7e11a 100644 --- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -2,48 +2,45 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); -let tempScope = {}; -Cu.import("resource://gre/modules/Services.jsm", tempScope); -let Services = tempScope.Services; // Disable logging for faster test runs. Set this pref to true if you want to -// debug a test in your try runs. +// debug a test in your try runs. Both the debugger server and frontend will +// be affected by this pref. let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); Services.prefs.setBoolPref("devtools.debugger.log", false); -Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope); -Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope); -Cu.import("resource:///modules/source-editor.jsm", tempScope); -Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope); -Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope); -Cu.import("resource://gre/modules/AddonManager.jsm", tempScope); -let SourceEditor = tempScope.SourceEditor; -let DebuggerServer = tempScope.DebuggerServer; -let DebuggerTransport = tempScope.DebuggerTransport; -let DebuggerClient = tempScope.DebuggerClient; -let AddonManager = tempScope.AddonManager; -let gDevTools = tempScope.gDevTools; -let devtools = tempScope.devtools; +let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +let { Promise: promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}); +let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let { BrowserDebuggerProcess } = Cu.import("resource:///modules/devtools/DebuggerProcess.jsm", {}); +let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); +let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {}); +let { SourceEditor } = Cu.import("resource:///modules/source-editor.jsm", {}); +let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); let TargetFactory = devtools.TargetFactory; +let Toolbox = devtools.Toolbox; + +const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/"; + +// All tests are asynchronous. +waitForExplicitFinish(); + +registerCleanupFunction(function() { + info("finish() was called, cleaning up..."); + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + + // Properly shut down the server to avoid memory leaks. + DebuggerServer.destroy(); +}); // Import the GCLI test helper let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this); -const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/"; -const TAB1_URL = EXAMPLE_URL + "browser_dbg_tab1.html"; -const TAB2_URL = EXAMPLE_URL + "browser_dbg_tab2.html"; -const ADDON1_URL = EXAMPLE_URL + "browser_dbg_addon1.xpi"; -const ADDON2_URL = EXAMPLE_URL + "browser_dbg_addon2.xpi"; -const STACK_URL = EXAMPLE_URL + "browser_dbg_stack.html"; - -// Enable remote debugging for the relevant tests. -let gEnableRemote = Services.prefs.getBoolPref("devtools.debugger.remote-enabled"); -Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true); - // Redeclare dbg_assert with a fatal behavior. function dbg_assert(cond, e) { if (!cond) { @@ -51,197 +48,461 @@ function dbg_assert(cond, e) { } } -registerCleanupFunction(function() { - Services.prefs.setBoolPref("devtools.debugger.remote-enabled", gEnableRemote); - Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); - - // Properly shut down the server to avoid memory leaks. - DebuggerServer.destroy(); -}); - -if (!DebuggerServer.initialized) { - DebuggerServer.init(function() true); - DebuggerServer.addBrowserActors(); +function addWindow(aUrl) { + info("Adding window: " + aUrl); + return promise.resolve(getDOMWindow(window.open(aUrl))); } -waitForExplicitFinish(); - -function addWindow() { - let windowReference = window.open(); - let chromeWindow = windowReference +function getDOMWindow(aReference) { + return aReference .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); - - return chromeWindow; } -function addTab(aURL, aOnload, aWindow) { +function addTab(aUrl, aWindow) { + info("Adding tab: " + aUrl); + + let deferred = promise.defer(); let targetWindow = aWindow || window; let targetBrowser = targetWindow.gBrowser; targetWindow.focus(); - targetBrowser.selectedTab = targetBrowser.addTab(aURL); + let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); + let linkedBrowser = tab.linkedBrowser; - let tab = targetBrowser.selectedTab; - let browser = tab.linkedBrowser; - let win = browser.contentWindow; - let expectedReadyState = aURL == "about:blank" ? ["interactive", "complete"] : ["complete"]; + linkedBrowser.addEventListener("load", function onLoad() { + linkedBrowser.removeEventListener("load", onLoad, true); + deferred.resolve(tab); + }, true); - if (aOnload) { - let handler = function() { - if (browser.currentURI.spec != aURL || - expectedReadyState.indexOf((win.document || {}).readyState) == -1) { - return; - } - browser.removeEventListener("load", handler, true); - executeSoon(aOnload); - } - browser.addEventListener("load", handler, true); - } - - return tab; + return deferred.promise; } 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); + deferred.resolve(); + }, false); targetBrowser.removeTab(aTab); + return deferred.promise; } -function addAddon(aURL, aOnInstallEnded) { - AddonManager.getInstallForURL(aURL, function(aInstall) { - aInstall.install(); - var listener = { +function addAddon(aUrl) { + info("Installing addon: " + aUrl); + + let deferred = promise.defer(); + + AddonManager.getInstallForURL(aUrl, aInstaller => { + aInstaller.install(); + let listener = { onInstallEnded: function(aAddon, aAddonInstall) { - aInstall.removeListener(listener); - aOnInstallEnded(aAddonInstall); + aInstaller.removeListener(listener); + deferred.resolve(aAddonInstall); } }; - aInstall.addListener(listener); + aInstaller.addListener(listener); }, "application/x-xpinstall"); + + return deferred.promise; } -function removeAddon(aAddon, aOnUninstalled) { - var listener = { +function removeAddon(aAddon) { + info("Removing addon."); + + let deferred = promise.defer(); + + let listener = { onUninstalled: function(aUninstalledAddon) { - if (aUninstalledAddon != aAddon) + if (aUninstalledAddon != aAddon) { return; + } AddonManager.removeAddonListener(listener); - aOnUninstalled(); + deferred.resolve(); } }; AddonManager.addAddonListener(listener); aAddon.uninstall(); + + return deferred.promise; } -function closeDebuggerAndFinish(aRemoteFlag, aCallback, aWindow) { - let debuggerClosed = false; - let debuggerDisconnected = false; +function getTabActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); - ok(gTab, "There is a gTab to use for getting a toolbox reference"); - let target = TargetFactory.forTab(gTab); - - window.addEventListener("Debugger:Shutdown", function cleanup() { - window.removeEventListener("Debugger:Shutdown", cleanup, false); - debuggerDisconnected = true; - maybeFinish(); - }, false); - - let toolbox = gDevTools.getToolbox(target); - toolbox.destroy().then(function() { - debuggerClosed = true; - maybeFinish(); + aClient.listTabs(aResponse => { + let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop(); + deferred.resolve(tabActor); }); - function maybeFinish() { - if (debuggerClosed && debuggerDisconnected) { - (finish || aCallback)(); - } - } + return deferred.promise; } -function get_tab_actor_for_url(aClient, aURL, aCallback) { - aClient.listTabs(function(aResponse) { - for each (let tab in aResponse.tabs) { - if (tab.url == aURL) { - aCallback(tab); - return; - } - } +function getAddonActorForUrl(aClient, aUrl) { + let deferred = promise.defer(); + + aClient.listAddons(aResponse => { + let addonActor = aResponse.addons.filter(aGrip => aGrip.url == aUrl).pop(); + deferred.resolve(addonActor); }); + + return deferred.promise; } -function attach_tab_actor_for_url(aClient, aURL, aCallback) { - get_tab_actor_for_url(aClient, aURL, function(actor) { - aClient.attachTab(actor.actor, function(aResponse) { - aCallback(actor, aResponse); +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 attach_thread_actor_for_url(aClient, aURL, aCallback) { - attach_tab_actor_for_url(aClient, aURL, function(aTabActor, aResponse) { - aClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) { - // We don't care about the pause right now (use - // get_actor_for_url() if you do), so resume it. - aThreadClient.resume(function(aResponse) { - aCallback(aThreadClient); +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 wait_for_connect_and_resume(aOnDebugging, aTab) { - let target = TargetFactory.forTab(aTab); +function once(aTarget, aEventName, aUseCapture = false) { + info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); - gDevTools.showToolbox(target, "jsdebugger").then(function(toolbox) { - let dbg = toolbox.getCurrentPanel(); + let deferred = promise.defer(); - // Wait for the initial resume... - dbg.panelWin.gClient.addOneTimeListener("resumed", function() { - aOnDebugging(); - }); + 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 waitForSourceShown(aPanel, aUrl) { + return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.SOURCE_SHOWN).then(aSource => { + let sourceUrl = aSource.url; + info("Source shown: " + sourceUrl); + + if (!sourceUrl.contains(aUrl)) { + return waitForSourceShown(aPanel, aUrl); + } else { + ok(true, "The correct source has been shown."); + } }); } -function debug_tab_pane(aURL, aOnDebugging, aBeforeTabAdded) { - // Make any necessary preparations (start the debugger server etc.) - if (aBeforeTabAdded) { - aBeforeTabAdded(); +function ensureSourceIs(aPanel, aUrl, aWaitFlag = false) { + if (aPanel.panelWin.DebuggerView.Sources.selectedValue.contains(aUrl)) { + ok(true, "Expected source is shown: " + aUrl); + return promise.resolve(null); } + if (aWaitFlag) { + return waitForSourceShown(aPanel, aUrl); + } + ok(false, "Expected source was not already shown: " + aUrl); + return promise.reject(null); +} - let tab = addTab(aURL, function() { - let debuggee = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject; - let target = TargetFactory.forTab(gBrowser.selectedTab); +function waitForCaretUpdated(aPanel, aLine, aCol = 1) { + return waitForEditorEvents(aPanel, SourceEditor.EVENTS.SELECTION).then(() => { + let caret = aPanel.panelWin.DebuggerView.editor.getCaretPosition(); + info("Caret updated: " + (caret.line + 1) + ", " + (caret.col + 1)); - info("Opening Debugger"); - gDevTools.showToolbox(target, "jsdebugger").then(function(toolbox) { - let dbg = toolbox.getCurrentPanel(); + 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 caret = editor.getCaretPosition(); + + // Source editor starts counting line and column numbers from 0. + info("Current editor caret position: " + (caret.line + 1) + ", " + (caret.col + 1)); + return caret.line == (aLine - 1) && caret.col == (aCol - 1); +} + +function isEditorSel(aPanel, [start, end]) { + let editor = aPanel.panelWin.DebuggerView.editor; + let range = editor.getSelection(); + + // 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.addEventListener(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.removeEventListener(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 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 navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) { + let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat); + let activeTab = aPanel.panelWin.gClient.activeTab; + aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload(); + 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) { + if (aTarget instanceof XULElement) { + return promise.resolve(aTarget); + } else { + return addTab(aTarget); + } +} + +function initDebugger(aTarget, aWindow) { + info("Initializing a debugger panel."); + + return getTab(aTarget).then(aTab => { + info("Debugee tab added successfully: " + aTarget); + + let deferred = promise.defer(); + let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject; + let target = TargetFactory.forTab(aTab); + + gDevTools.showToolbox(target, "jsdebugger").then(aToolbox => { + info("Debugger panel shown successfully."); + + let debuggerPanel = aToolbox.getCurrentPanel(); + let panelWin = debuggerPanel.panelWin; // Wait for the initial resume... - dbg.panelWin.gClient.addOneTimeListener("resumed", function() { - info("Debugger has started"); - dbg._view.Variables.lazyEmpty = false; - dbg._view.Variables.lazyAppend = false; - dbg._view.Variables.lazyExpand = false; - aOnDebugging(tab, debuggee, dbg); + panelWin.gClient.addOneTimeListener("resumed", () => { + info("Debugger client resumed successfully."); + + prepareDebugger(debuggerPanel); + deferred.resolve([aTab, debuggee, debuggerPanel]); }); }); + + return deferred.promise; }); } -function debug_chrome(aURL, aOnClosing, aOnDebugging) { - let tab = addTab(aURL, function() { - let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject; +function initChromeDebugger(aOnClose) { + info("Initializing a chrome debugger process."); - info("Opening Browser Debugger"); - let win = BrowserDebuggerProcess.init(aOnClosing, function(process) { + let deferred = promise.defer(); - // The remote debugging process has started... - info("Browser Debugger has started"); - aOnDebugging(tab, debuggee, process); - }); + // Wait for the debugger process to start... + BrowserDebuggerProcess.init(aOnClose, aProcess => { + info("Chrome debugger process started successfully."); + + prepareDebugger(aProcess); + deferred.resolve(aProcess); }); + + return deferred.promise; +} + +function prepareDebugger(aDebugger) { + if ("target" in aDebugger) { + let variables = aDebugger.panelWin.DebuggerView.Variables; + variables.lazyEmpty = false; + variables.lazyAppend = false; + variables.lazyExpand = false; + variables.lazySearch = false; + } 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; }