Bug 836298 - Add conditional pause on exception draft
authorRob Wu <rob@robwu.nl>
Mon, 12 Sep 2016 04:40:28 -0700
changeset 412775 b9258a577ef49aeaae561b4a780ff894aa1118f9
parent 412489 cfdb7af3af2e92e95f71ca2f1672bf5433beeb89
child 531067 f12da3a350ab481c8a02c1aac18b6d4d20078132
push id29257
push userbmo:rob@robwu.nl
push dateMon, 12 Sep 2016 20:59:03 +0000
bugs836298
milestone51.0a1
Bug 836298 - Add conditional pause on exception MozReview-Commit-ID: JDaXiVWUup
devtools/client/debugger/debugger.xul
devtools/client/debugger/test/mochitest/browser2.ini
devtools/client/debugger/test/mochitest/browser_dbg_pause-exceptions-03.js
devtools/client/debugger/views/options-view.js
devtools/client/locales/en-US/debugger.dtd
devtools/server/actors/script.js
devtools/shared/client/main.js
--- 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);