/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); // 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. let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); Services.prefs.setBoolPref("devtools.debugger.log", false); 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 { require } = devtools; let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); let { BrowserToolboxProcess } = Cu.import("resource:///modules/devtools/ToolboxProcess.jsm", {}); let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.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/"; gDevTools.testing = true; SimpleTest.registerCleanupFunction(() => { gDevTools.testing = false; }); // 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(); // 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 let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this); // Redeclare dbg_assert with a fatal behavior. function dbg_assert(cond, e) { if (!cond) { throw e; } } function addWindow(aUrl) { info("Adding window: " + aUrl); return promise.resolve(getDOMWindow(window.open(aUrl))); } function getDOMWindow(aReference) { return aReference .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); } 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; linkedBrowser.addEventListener("load", function onLoad() { linkedBrowser.removeEventListener("load", onLoad, true); info("Tab added and finished loading: " + aUrl); deferred.resolve(tab); }, true); 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); info("Tab removed and finished closing."); deferred.resolve(); }, false); targetBrowser.removeTab(aTab); return deferred.promise; } function addAddon(aUrl) { info("Installing addon: " + aUrl); let deferred = promise.defer(); AddonManager.getInstallForURL(aUrl, aInstaller => { aInstaller.install(); let listener = { onInstallEnded: function(aAddon, aAddonInstall) { aInstaller.removeListener(listener); deferred.resolve(aAddonInstall); } }; aInstaller.addListener(listener); }, "application/x-xpinstall"); return deferred.promise; } 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 getAddonActorForUrl(aClient, aUrl) { info("Get addon actor for URL: " + aUrl); let deferred = promise.defer(); aClient.listAddons(aResponse => { let addonActor = aResponse.addons.filter(aGrip => aGrip.url == aUrl).pop(); info("got addon actor for URL: " + addonActor.actor); 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 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 waitForEditorLocationSet(aPanel) { return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET); } 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); } 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 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 navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) { let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat); let activeTab = aPanel.panelWin.DebuggerController._target.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, aWindow) { if (aTarget instanceof XULElement) { return promise.resolve(aTarget); } else { return addTab(aTarget, aWindow); } } function initDebugger(aTarget, aWindow) { info("Initializing a debugger panel."); return getTab(aTarget, aWindow).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... panelWin.gClient.addOneTimeListener("resumed", () => { info("Debugger client resumed successfully."); prepareDebugger(debuggerPanel); deferred.resolve([aTab, debuggee, debuggerPanel, aWindow]); }); }); return deferred.promise; }); } function initAddonDebugger(aClient, aUrl, aFrame) { info("Initializing an addon debugger panel."); return getAddonActorForUrl(aClient, aUrl).then((addonActor) => { let targetOptions = { form: { addonActor: addonActor.actor, title: addonActor.name }, client: aClient, chrome: true }; let toolboxOptions = { customIframe: aFrame }; let target = devtools.TargetFactory.forTab(targetOptions); return gDevTools.showToolbox(target, "jsdebugger", devtools.Toolbox.HostType.CUSTOM, toolboxOptions); }).then(aToolbox => { info("Addon debugger panel shown successfully."); let debuggerPanel = aToolbox.getCurrentPanel(); // Wait for the initial resume... return waitForClientEvents(debuggerPanel, "resumed") .then(() => prepareDebugger(debuggerPanel)) .then(() => debuggerPanel); }); } function initChromeDebugger(aOnClose) { info("Initializing a chrome debugger process."); let deferred = promise.defer(); // Wait for the toolbox process to start... BrowserToolboxProcess.init(aOnClose, 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.FilteredSources._autoSelectFirstItem = true; view.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"); } function toggleBlackBoxing(aPanel, aSource = null) { function clickBlackBoxButton() { getBlackBoxButton(aPanel).click(); } const blackBoxChanged = waitForThreadEvents(aPanel, "blackboxchange"); if (aSource) { aPanel.panelWin.DebuggerView.Sources.selectedValue = aSource; ensureSourceIs(aPanel, aSource, true).then(clickBlackBoxButton); } else { clickBlackBoxButton(); } return blackBoxChanged; } function selectSourceAndGetBlackBoxButton(aPanel, aSource) { function returnBlackboxButton() { return getBlackBoxButton(aPanel); } aPanel.panelWin.DebuggerView.Sources.selectedValue = aSource; return ensureSourceIs(aPanel, aSource, 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 { left, top } = editor.getCoordsFromPosition(aCoords); bubble._findIdentifier(left, top); return promise.all([popupShown, fetchedProperties]).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); } }, tooltip.defaultShowDelay + 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)); } // Tracing helpers function startTracing(aPanel) { const deferred = promise.defer(); aPanel.panelWin.DebuggerController.Tracer.startTracing(aResponse => { if (aResponse.error) { deferred.reject(aResponse); } else { deferred.resolve(aResponse); } }); return deferred.promise; } function stopTracing(aPanel) { const deferred = promise.defer(); aPanel.panelWin.DebuggerController.Tracer.stopTracing(aResponse => { if (aResponse.error) { deferred.reject(aResponse); } else { deferred.resolve(aResponse); } }); return deferred.promise; } function filterTraces(aPanel, f) { const traces = aPanel.panelWin.document .getElementById("tracer-traces") .querySelector("scrollbox") .children; return Array.filter(traces, f); } function attachAddonActorForUrl(aClient, aUrl) { let deferred = promise.defer(); getAddonActorForUrl(aClient, aUrl).then(aGrip => { aClient.attachAddon(aGrip.actor, aResponse => { deferred.resolve([aGrip, aResponse]); }); }); return deferred.promise; }