--- a/devtools/client/debugger/debugger.xul
+++ b/devtools/client/debugger/debugger.xul
@@ -128,16 +128,20 @@
label="&debuggerUI.autoPrettyPrint;"
accesskey="&debuggerUI.autoPrettyPrint.accesskey;"
command="toggleAutoPrettyPrint"/>
<menuitem id="pause-on-exceptions"
type="checkbox"
label="&debuggerUI.pauseExceptions;"
accesskey="&debuggerUI.pauseExceptions.accesskey;"
command="togglePauseOnExceptions"/>
+ <menuitem id="conditional-pause-on-exceptions"
+ type="checkbox"
+ label="&debuggerUI.condPauseExceptions;"
+ command="showConditionalPauseOnExceptions"/>
<menuitem id="ignore-caught-exceptions"
type="checkbox"
label="&debuggerUI.ignoreCaughtExceptions;"
accesskey="&debuggerUI.ignoreCaughtExceptions.accesskey;"
command="toggleIgnoreCaughtExceptions"/>
<menuitem id="show-panes-on-startup"
type="checkbox"
label="&debuggerUI.showPanesOnInit;"
@@ -493,9 +497,19 @@
noautofocus="true"
consumeoutsideclicks="false">
<vbox>
<label id="conditional-breakpoint-panel-description"
value="&debuggerUI.condBreakPanelTitle;"/>
<textbox id="conditional-breakpoint-panel-textbox"/>
</vbox>
</panel>
+
+ <panel id="conditional-pause-on-exceptions-panel"
+ noautofocus="true"
+ consumeoutsideclicks="false">
+ <vbox>
+ <label id="conditional-pause-on-exceptions-panel-description"
+ value="&debuggerUI.condPauseExceptionsPanelTitle;"/>
+ <textbox id="conditional-pause-on-exceptions-textbox"/>
+ </vbox>
+ </panel>
</window>
--- a/devtools/client/debugger/test/mochitest/browser2.ini
+++ b/devtools/client/debugger/test/mochitest/browser2.ini
@@ -172,16 +172,18 @@ skip-if = e10s && debug
[browser_dbg_parser-function-defaults.js]
[browser_dbg_parser-spread-expression.js]
[browser_dbg_parser-template-strings.js]
skip-if = e10s && debug
[browser_dbg_pause-exceptions-01.js]
skip-if = e10s && debug
[browser_dbg_pause-exceptions-02.js]
skip-if = e10s && debug
+[browser_dbg_pause-exceptions-03.js]
+skip-if = e10s && debug
[browser_dbg_pause-no-step.js]
skip-if = e10s && debug
[browser_dbg_pause-resume.js]
skip-if = e10s && debug
[browser_dbg_pause-warning.js]
skip-if = e10s && debug
[browser_dbg_paused-keybindings.js]
skip-if = e10s
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-03.js
@@ -0,0 +1,312 @@
+/* -*- 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/ */
+
+/**
+ * Make sure that conditional pausing on exceptions works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pause-exceptions.html";
+
+var gTab, gPanel, gDebugger;
+var gFrames, gVariables, gPrefs, gOptions;
+
+function test() {
+ requestLongerTimeout(2);
+ let options = {
+ source: TAB_URL,
+ line: 1
+ };
+ initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
+ gTab = aTab;
+ gPanel = aPanel;
+ gDebugger = gPanel.panelWin;
+ gFrames = gDebugger.DebuggerView.StackFrames;
+ gVariables = gDebugger.DebuggerView.Variables;
+ gPrefs = gDebugger.Prefs;
+ gOptions = gDebugger.DebuggerView.Options;
+
+ is(gPrefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should be disabled by default.");
+ isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should not be checked.");
+
+ testPauseOnExceptionsDisabled()
+ .then(enablePauseOnExceptions)
+ .then(disableIgnoreCaughtExceptions)
+ .then(() => setConditionalException("if (true) true;"))
+ .then(testPauseOnExceptionsEnabled)
+ // Treat errors as true.
+ .then(() => setConditionalException("throw new Error('Some error')"))
+ .then(testPauseOnExceptionsEnabled)
+ // Tests that $url is present and a string. If it is really a string then
+ // the condition is false (since the URL presumably does not include the
+ // substring) and the next step passes. Otherwise an error is thrown and
+ // treated as true, so pausing on exceptions is not disabled.
+ // The test would then stall after this point.
+ .then(() => setConditionalException("$scriptUrl.includes('No match!')"))
+ .then(testPauseOnExceptionsDisabled)
+ // Tests whether $exception exists following the same logic as before.
+ .then(() => setConditionalException("typeof $exception == 'undefined'"))
+ .then(testPauseOnExceptionsDisabled)
+ .then(() => setConditionalException("debugger; [] // Array is truthy."))
+ .then(testPauseOnExceptionsEnabled)
+ .then(() => setConditionalException(""))
+ .then(testPauseOnExceptionsEnabled)
+ .then(disablePauseOnExceptions)
+ .then(enableIgnoreCaughtExceptions)
+ .then(() => closeDebuggerAndFinish(gPanel))
+ .then(null, aError => {
+ ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+ });
+ });
+}
+
+function testPauseOnExceptionsDisabled() {
+ let finished = waitForCaretAndScopes(gPanel, 26).then(() => {
+ info("Testing disabled pause-on-exceptions.");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused (1).");
+ ok(isCaretPos(gPanel, 26),
+ "Should be paused on the debugger statement (1).");
+
+ let innerScope = gVariables.getScopeAtIndex(0);
+ let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(gFrames.itemCount, 1,
+ "Should have one frame.");
+ is(gVariables._store.length, 4,
+ "Should have four scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "this",
+ "Should have the right property name for 'this'.");
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>",
+ "Should have the right property value for 'this'.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "Should not be paused after resuming.");
+ ok(isCaretPos(gPanel, 26),
+ "Should be idle on the debugger statement.");
+
+ ok(true, "Frames were cleared, debugger didn't pause again.");
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return finished;
+}
+
+function testPauseOnExceptionsEnabled() {
+ let finished = waitForCaretAndScopes(gPanel, 19).then(() => {
+ info("Testing enabled pause-on-exceptions.");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ ok(isCaretPos(gPanel, 19),
+ "Should be paused on the debugger statement.");
+
+ let innerScope = gVariables.getScopeAtIndex(0);
+ let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(gFrames.itemCount, 1,
+ "Should have one frame.");
+ is(gVariables._store.length, 4,
+ "Should have four scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "<exception>",
+ "Should have the right property name for <exception>.");
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "Error",
+ "Should have the right property value for <exception>.");
+
+ let finished = waitForCaretAndScopes(gPanel, 26).then(() => {
+ info("Testing enabled pause-on-exceptions and resumed after pause.");
+
+ is(gDebugger.gThreadClient.state, "paused",
+ "Should only be getting stack frames while paused.");
+ ok(isCaretPos(gPanel, 26),
+ "Should be paused on the debugger statement.");
+
+ let innerScope = gVariables.getScopeAtIndex(0);
+ let innerNodes = innerScope.target.querySelector(".variables-view-element-details").childNodes;
+
+ is(gFrames.itemCount, 1,
+ "Should have one frame.");
+ is(gVariables._store.length, 4,
+ "Should have four scopes.");
+
+ is(innerNodes[0].querySelector(".name").getAttribute("value"), "this",
+ "Should have the right property name for 'this'.");
+ is(innerNodes[0].querySelector(".value").getAttribute("value"), "<button>",
+ "Should have the right property value for 'this'.");
+
+ let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_CLEARED).then(() => {
+ isnot(gDebugger.gThreadClient.state, "paused",
+ "Should not be paused after resuming.");
+ ok(isCaretPos(gPanel, 26),
+ "Should be idle on the debugger statement.");
+
+ ok(true, "Frames were cleared, debugger didn't pause again.");
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ });
+
+ EventUtils.sendMouseEvent({ type: "mousedown" },
+ gDebugger.document.getElementById("resume"),
+ gDebugger);
+
+ return finished;
+ });
+
+ generateMouseClickInTab(gTab, "content.document.querySelector('button')");
+
+ return finished;
+}
+
+function enablePauseOnExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.pauseOnExceptions, true,
+ "The pause-on-exceptions pref should now be enabled.");
+ is(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should now be checked.");
+
+ ok(true, "Pausing on exceptions was enabled.");
+ deferred.resolve();
+ });
+
+ gOptions._pauseOnExceptionsItem.setAttribute("checked", "true");
+ gOptions._togglePauseOnExceptions();
+
+ return deferred.promise;
+}
+
+function disablePauseOnExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.pauseOnExceptions, false,
+ "The pause-on-exceptions pref should now be disabled.");
+ isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+ "The pause-on-exceptions menu item should now be unchecked.");
+
+ ok(true, "Pausing on exceptions was disabled.");
+ deferred.resolve();
+ });
+
+ gOptions._pauseOnExceptionsItem.setAttribute("checked", "false");
+ gOptions._togglePauseOnExceptions();
+
+ return deferred.promise;
+}
+
+function enableIgnoreCaughtExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.ignoreCaughtExceptions, true,
+ "The ignore-caught-exceptions pref should now be enabled.");
+ is(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true",
+ "The ignore-caught-exceptions menu item should now be checked.");
+
+ ok(true, "Ignore caught exceptions was enabled.");
+ deferred.resolve();
+ });
+
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true");
+ gOptions._toggleIgnoreCaughtExceptions();
+
+ return deferred.promise;
+}
+
+function disableIgnoreCaughtExceptions() {
+ let deferred = promise.defer();
+
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gPrefs.ignoreCaughtExceptions, false,
+ "The ignore-caught-exceptions pref should now be disabled.");
+ isnot(gOptions._ignoreCaughtExceptionsItem.getAttribute("checked"), "true",
+ "The ignore-caught-exceptions menu item should now be unchecked.");
+
+ ok(true, "Ignore caught exceptions was disabled.");
+ deferred.resolve();
+ });
+
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false");
+ gOptions._toggleIgnoreCaughtExceptions();
+
+ return deferred.promise;
+}
+
+function setConditionalException(conditionString) {
+ info("Will set conditional exception to: " + conditionString);
+ let expectedAttr = conditionString ? "true" : "false";
+ let oppositeAttr = conditionString ? "false" : "true";
+
+ function promiseEvent(element, eventName) {
+ return new Promise(resolve => {
+ element.addEventListener(eventName, function listener() {
+ element.removeEventListener(eventName, listener, false);
+ resolve();
+ }, false);
+ });
+ }
+
+ let resultPromise = promiseEvent(gOptions._condExceptionPanel, "popupshown")
+ .then(() => {
+ let {activeElement} = gDebugger.document;
+ isnot(activeElement, null, "Some element should be focused");
+ activeElement = gDebugger.document.getBindingParent(activeElement);
+ is(activeElement, gOptions._condExceptionTextbox,
+ "The conditional-pause-on-exceptions-textbox text box should be focused");
+
+ is(gOptions._condPauseOnExceptionsItem.getAttribute("checked"),
+ gOptions._condExceptionTextbox.value ? "true" : "false",
+ "The conditional-pause-on-exceptions menu item should match the state of the current exception condition");
+
+ // The checkbox state should ultimately depend on the condition string.
+ // So let's flip it and check again later.
+ gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", oppositeAttr);
+ gOptions._condExceptionTextbox.value = conditionString;
+
+ let deferred = promise.defer();
+ gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+ is(gOptions._condPauseOnExceptionsItem.getAttribute("checked"), expectedAttr,
+ "The conditional-pause-on-exceptions menu item should now be set based on the condition");
+
+ info("Exception condition was set.");
+ deferred.resolve();
+ });
+ let hiddenPanelPromise = promiseEvent(gOptions._condExceptionPanel, "popuphidden");
+
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ return Promise.all([hiddenPanelPromise, deferred.promise]);
+ })
+ gOptions._showConditionalPauseOnExceptions();
+ return resultPromise;
+}
+
+registerCleanupFunction(function () {
+ gTab = null;
+ gPanel = null;
+ gDebugger = null;
+ gFrames = null;
+ gVariables = null;
+ gPrefs = null;
+ gOptions = null;
+});
--- a/devtools/client/debugger/views/options-view.js
+++ b/devtools/client/debugger/views/options-view.js
@@ -20,67 +20,84 @@ function OptionsView(DebuggerController,
dumpn("OptionsView was instantiated");
this.DebuggerController = DebuggerController;
this.DebuggerView = DebuggerView;
this._toggleAutoPrettyPrint = this._toggleAutoPrettyPrint.bind(this);
this._togglePauseOnExceptions = this._togglePauseOnExceptions.bind(this);
this._toggleIgnoreCaughtExceptions = this._toggleIgnoreCaughtExceptions.bind(this);
+ this._showConditionalPauseOnExceptions = this._showConditionalPauseOnExceptions.bind(this);
this._toggleShowPanesOnStartup = this._toggleShowPanesOnStartup.bind(this);
this._toggleShowVariablesOnlyEnum = this._toggleShowVariablesOnlyEnum.bind(this);
this._toggleShowVariablesFilterBox = this._toggleShowVariablesFilterBox.bind(this);
this._toggleShowOriginalSource = this._toggleShowOriginalSource.bind(this);
this._toggleAutoBlackBox = this._toggleAutoBlackBox.bind(this);
+
+ this._onCondExceptionPanelShown = this._onCondExceptionPanelShown.bind(this);
+ this._onCondExceptionPanelHiding = this._onCondExceptionPanelHiding.bind(this);
+ this._onCondExceptionTextboxKeyPress = this._onCondExceptionTextboxKeyPress.bind(this);
}
OptionsView.prototype = {
/**
* Initialization function, called when the debugger is started.
*/
initialize: function () {
dumpn("Initializing the OptionsView");
this._button = document.getElementById("debugger-options");
this._autoPrettyPrint = document.getElementById("auto-pretty-print");
this._pauseOnExceptionsItem = document.getElementById("pause-on-exceptions");
+ this._condPauseOnExceptionsItem = document.getElementById("conditional-pause-on-exceptions");
this._ignoreCaughtExceptionsItem = document.getElementById("ignore-caught-exceptions");
this._showPanesOnStartupItem = document.getElementById("show-panes-on-startup");
this._showVariablesOnlyEnumItem = document.getElementById("show-vars-only-enum");
this._showVariablesFilterBoxItem = document.getElementById("show-vars-filter-box");
this._showOriginalSourceItem = document.getElementById("show-original-source");
this._autoBlackBoxItem = document.getElementById("auto-black-box");
+ this._condExceptionPanel = document.getElementById("conditional-pause-on-exceptions-panel");
+ this._condExceptionTextbox = document.getElementById("conditional-pause-on-exceptions-textbox");
this._autoPrettyPrint.setAttribute("checked", Prefs.autoPrettyPrint);
this._pauseOnExceptionsItem.setAttribute("checked", Prefs.pauseOnExceptions);
+ this._condPauseOnExceptionsItem.setAttribute("checked", !!this._pauseOnExceptionCondition);
this._ignoreCaughtExceptionsItem.setAttribute("checked", Prefs.ignoreCaughtExceptions);
this._showPanesOnStartupItem.setAttribute("checked", Prefs.panesVisibleOnStartup);
this._showVariablesOnlyEnumItem.setAttribute("checked", Prefs.variablesOnlyEnumVisible);
this._showVariablesFilterBoxItem.setAttribute("checked", Prefs.variablesSearchboxVisible);
this._showOriginalSourceItem.setAttribute("checked", Prefs.sourceMapsEnabled);
this._autoBlackBoxItem.setAttribute("checked", Prefs.autoBlackBox);
+ this._condExceptionPanel.addEventListener("popupshown", this._onCondExceptionPanelShown, false);
+ this._condExceptionPanel.addEventListener("popuphiding", this._onCondExceptionPanelHiding, false);
+ this._condExceptionTextbox.addEventListener("keypress", this._onCondExceptionTextboxKeyPress, false);
+
this._addCommands();
},
/**
* Destruction function, called when the debugger is closed.
*/
destroy: function () {
dumpn("Destroying the OptionsView");
- // Nothing to do here yet.
+
+ this._condExceptionPanel.removeEventListener("popupshown", this._onCondExceptionPanelShown, false);
+ this._condExceptionPanel.removeEventListener("popuphiding", this._onCondExceptionPanelHiding, false);
+ this._condExceptionTextbox.removeEventListener("keypress", this._onCondExceptionTextboxKeyPress, false);
},
/**
* Add commands that XUL can fire.
*/
_addCommands: function () {
XULUtils.addCommands(document.getElementById("debuggerCommands"), {
toggleAutoPrettyPrint: () => this._toggleAutoPrettyPrint(),
togglePauseOnExceptions: () => this._togglePauseOnExceptions(),
+ showConditionalPauseOnExceptions: () => this._showConditionalPauseOnExceptions(),
toggleIgnoreCaughtExceptions: () => this._toggleIgnoreCaughtExceptions(),
toggleShowPanesOnStartup: () => this._toggleShowPanesOnStartup(),
toggleShowOnlyEnum: () => this._toggleShowVariablesOnlyEnum(),
toggleShowVariablesFilterBox: () => this._toggleShowVariablesFilterBox(),
toggleShowOriginalSource: () => this._toggleShowOriginalSource(),
toggleAutoBlackBox: () => this._toggleAutoBlackBox()
});
},
@@ -119,25 +136,34 @@ OptionsView.prototype = {
* Listener handling the 'pause on exceptions' menuitem command.
*/
_togglePauseOnExceptions: function () {
Prefs.pauseOnExceptions =
this._pauseOnExceptionsItem.getAttribute("checked") == "true";
this.DebuggerController.activeThread.pauseOnExceptions(
Prefs.pauseOnExceptions,
+ this._pauseOnExceptionCondition,
Prefs.ignoreCaughtExceptions);
},
+ _showConditionalPauseOnExceptions: function () {
+ // Undo the checkbox change. The checkbox is used to signal whether a
+ // conditional breakpoint is active.
+ this._condPauseOnExceptionsItem.setAttribute("checked", !!this._pauseOnExceptionCondition);
+ this._condExceptionPanel.openPopup(this._button, "bottomcenter topright");
+ },
+
_toggleIgnoreCaughtExceptions: function () {
Prefs.ignoreCaughtExceptions =
this._ignoreCaughtExceptionsItem.getAttribute("checked") == "true";
this.DebuggerController.activeThread.pauseOnExceptions(
Prefs.pauseOnExceptions,
+ this._pauseOnExceptionCondition,
Prefs.ignoreCaughtExceptions);
},
/**
* Listener handling the 'show panes on startup' menuitem command.
*/
_toggleShowPanesOnStartup: function () {
Prefs.panesVisibleOnStartup =
@@ -198,18 +224,42 @@ OptionsView.prototype = {
this.DebuggerController.reconfigureThread({
useSourceMaps: Prefs.sourceMapsEnabled,
autoBlackBox: pref
});
}, POPUP_HIDDEN_DELAY);
});
},
+ _onCondExceptionPanelShown: function () {
+ this._condExceptionTextbox.focus();
+ this._condExceptionTextbox.select();
+ },
+
+ _onCondExceptionPanelHiding: function () {
+ this._pauseOnExceptionCondition = this._condExceptionTextbox.value;
+ this._condPauseOnExceptionsItem.setAttribute("checked", !!this._pauseOnExceptionCondition);
+
+ this.DebuggerController.activeThread.pauseOnExceptions(
+ Prefs.pauseOnExceptions,
+ this._pauseOnExceptionCondition,
+ Prefs.ignoreCaughtExceptions);
+ },
+
+ _onCondExceptionTextboxKeyPress: function (e) {
+ if (e.keyCode == KeyCodes.DOM_VK_RETURN) {
+ this._condExceptionPanel.hidePopup();
+ }
+ },
+
_button: null,
_pauseOnExceptionsItem: null,
+ _condPauseOnExceptionsItem: null,
_showPanesOnStartupItem: null,
_showVariablesOnlyEnumItem: null,
_showVariablesFilterBoxItem: null,
_showOriginalSourceItem: null,
- _autoBlackBoxItem: null
+ _autoBlackBoxItem: null,
+ _condExceptionPanel: null,
+ _condExceptionTextbox: null,
};
DebuggerView.Options = new OptionsView(DebuggerController, DebuggerView);
--- a/devtools/client/locales/en-US/debugger.dtd
+++ b/devtools/client/locales/en-US/debugger.dtd
@@ -62,16 +62,22 @@
- the button that clears the collected tracing data in the tracing tab. -->
<!ENTITY debuggerUI.clearButton.tooltip "Clear the collected traces">
<!-- LOCALIZATION NOTE (debuggerUI.pauseExceptions): This is the label for the
- checkbox that toggles pausing on exceptions. -->
<!ENTITY debuggerUI.pauseExceptions "Pause on Exceptions">
<!ENTITY debuggerUI.pauseExceptions.accesskey "E">
+<!-- LOCALIZATION NOTE (debuggerUI.condPauseExceptions): This is the label for
+ - the menu item that shows the panel to edit the condition for pausing on
+ - exceptions. -->
+<!ENTITY debuggerUI.condPauseExceptions "Conditional Pause on Exceptions">
+<!ENTITY debuggerUI.condPauseExceptions.accesskey "E">
+
<!-- LOCALIZATION NOTE (debuggerUI.ignoreCaughtExceptions): This is the label for the
- checkbox that toggles ignoring caught exceptions. -->
<!ENTITY debuggerUI.ignoreCaughtExceptions "Ignore Caught Exceptions">
<!ENTITY debuggerUI.ignoreCaughtExceptions.accesskey "C">
<!-- LOCALIZATION NOTE (debuggerUI.showPanesOnInit): This is the label for the
- checkbox that toggles visibility of panes when opening the debugger. -->
<!ENTITY debuggerUI.showPanesOnInit "Show Panes on Startup">
@@ -147,16 +153,21 @@
<!ENTITY debuggerUI.focusVariables "Focus Variables Tree">
<!ENTITY debuggerUI.focusVariables.key "V">
<!ENTITY debuggerUI.focusVariables.accesskey "V">
<!-- LOCALIZATION NOTE (debuggerUI.condBreakPanelTitle): This is the text that
- appears in the conditional breakpoint panel popup as a description. -->
<!ENTITY debuggerUI.condBreakPanelTitle "This breakpoint will stop execution only if the following expression is true">
+<!-- LOCALIZATION NOTE (debuggerUI.condPauseExceptionsPanelTitle): This is the
+ - text that appears in the panel where a textbox is shown to change the
+ - condition for pausing on exceptions -->
+<!ENTITY debuggerUI.condPauseExceptionsPanelTitle "The debugger will pause on exceptions when the following expression is true (or empty)">
+
<!-- LOCALIZATION NOTE (debuggerUI.seMenuBreak): This is the text that
- appears in the source editor context menu for adding a breakpoint. -->
<!ENTITY debuggerUI.seMenuBreak "Add Breakpoint">
<!ENTITY debuggerUI.seMenuBreak.key "B">
<!-- LOCALIZATION NOTE (debuggerUI.seMenuCondBreak): This is the text that
- appears in the source editor context menu for adding a conditional
- breakpoint. -->
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -1006,16 +1006,17 @@ const ThreadActor = ActorClassWithSpec(t
} else {
this._clearSteppingHooks(this.youngestFrame);
resumeLimitHandled = resolve(true);
}
return resumeLimitHandled.then(() => {
if (aRequest) {
this._options.pauseOnExceptions = aRequest.pauseOnExceptions;
+ this._options.pauseOnExceptionCondition = aRequest.pauseOnExceptionCondition;
this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions;
this.maybePauseOnExceptions();
this._maybeListenToEvents(aRequest);
}
let packet = this._resumed();
this._popThreadPause();
// Tell anyone who cares of the resume (as of now, that's the xpcshell
@@ -1857,16 +1858,28 @@ const ThreadActor = ActorClassWithSpec(t
}
try {
let packet = this._paused(aFrame);
if (!packet) {
return undefined;
}
+ if (this._options.pauseOnExceptionCondition && aFrame.environment) {
+ let bindings = {
+ $scriptUrl: url,
+ $exception: aValue,
+ };
+ let result = aFrame.evalWithBindings(this._options.pauseOnExceptionCondition, bindings);
+ if (result && "return" in result && !result.return) {
+ this._resumed();
+ return undefined;
+ }
+ }
+
packet.why = { type: "exception",
exception: createValueGrip(aValue, this._pausePool,
this.objectGrip)
};
this.conn.send(packet);
this._pushThreadPause();
} catch (e) {
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -1700,16 +1700,17 @@ function ThreadClient(aClient, aActor) {
}
ThreadClient.prototype = {
_state: "paused",
get state() { return this._state; },
get paused() { return this._state === "paused"; },
_pauseOnExceptions: false,
+ _pauseOnExceptionCondition: '',
_ignoreCaughtExceptions: false,
_pauseOnDOMEvents: null,
_actor: null,
get actor() { return this._actor; },
get _transport() { return this.client._transport; },
@@ -1739,16 +1740,19 @@ ThreadClient.prototype = {
// Put the client in a tentative "resuming" state so we can prevent
// further requests that should only be sent in the paused state.
this._state = "resuming";
if (this._pauseOnExceptions) {
aPacket.pauseOnExceptions = this._pauseOnExceptions;
}
+ if (this._pauseOnExceptionCondition) {
+ aPacket.pauseOnExceptionCondition = this._pauseOnExceptionCondition;
+ }
if (this._ignoreCaughtExceptions) {
aPacket.ignoreCaughtExceptions = this._ignoreCaughtExceptions;
}
if (this._pauseOnDOMEvents) {
aPacket.pauseOnDOMEvents = this._pauseOnDOMEvents;
}
return aPacket;
},
@@ -1850,25 +1854,33 @@ ThreadClient.prototype = {
_doInterrupt: DebuggerClient.requester({
type: "interrupt",
when: args(0)
}),
/**
* Enable or disable pausing when an exception is thrown.
*
- * @param boolean aFlag
+ * @param boolean aPauseOnExceptions
* Enables pausing if true, disables otherwise.
+ * @param string aPauseOnExceptionCondition
+ * If not empty, this string is evaluated before pausing. If the
+ * code evaluates to a truthy value the debugger pauses. Otherwise the
+ * exception is ignored.
+ * @param boolean aIgnoreCaughtExceptions
+ * Ignore caught exceptions if true.
* @param function aOnResponse
* Called with the response packet.
*/
pauseOnExceptions: function (aPauseOnExceptions,
+ aPauseOnExceptionCondition,
aIgnoreCaughtExceptions,
aOnResponse = noop) {
this._pauseOnExceptions = aPauseOnExceptions;
+ this._pauseOnExceptionCondition = aPauseOnExceptionCondition;
this._ignoreCaughtExceptions = aIgnoreCaughtExceptions;
// Otherwise send the flag using a standard resume request.
if (!this.paused) {
return this.interrupt(aResponse => {
if (aResponse.error) {
// Can't continue if pausing failed.
aOnResponse(aResponse);