From c627a3d085b1d261a4a97bd57751a53159d57e5c Mon Sep 17 00:00:00 2001 From: amsul Date: Fri, 17 Apr 2015 19:33:09 -0400 Subject: [PATCH] added allowUncaught option (#553) allows unhandled exceptions to propagate in the browser added tests for allowUncaught option global error handler prints to dom with allowUncaught --- lib/mocha.js | 13 +++++++++++++ lib/runnable.js | 36 ++++++++++++++++++++++++------------ lib/runner.js | 5 ++++- support/tail.js | 2 +- test/runnable.js | 29 +++++++++++++++++++++++++++++ test/runner.js | 15 +++++++++++++++ 6 files changed, 86 insertions(+), 14 deletions(-) diff --git a/lib/mocha.js b/lib/mocha.js index 8a11124cb1..3ef690d121 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -401,6 +401,18 @@ Mocha.prototype.noHighlighting = function() { return this; }; +/** + * Enable uncaught errors to propagate (in browser). + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.allowUncaught = function(){ + this.options.allowUncaught = true; + return this; +}; + /** * Delay root suite execution. * @returns {Mocha} @@ -428,6 +440,7 @@ Mocha.prototype.run = function(fn){ runner.ignoreLeaks = false !== options.ignoreLeaks; runner.fullStackTrace = options.fullStackTrace; runner.asyncOnly = options.asyncOnly; + runner.allowUncaught = options.allowUncaught; if (options.grep) runner.grep(options.grep, options.invert); if (options.globals) runner.globals(options.globals); if (options.growl) this._growl(runner, reporter); diff --git a/lib/runnable.js b/lib/runnable.js index 3ea6080699..df2579d326 100644 --- a/lib/runnable.js +++ b/lib/runnable.js @@ -229,24 +229,22 @@ Runnable.prototype.run = function(fn){ if (this.async) { this.resetTimeout(); - try { - this.fn.call(ctx, function(err){ - if (err instanceof Error || toString.call(err) === "[object Error]") return done(err); - if (null != err) { - if (Object.prototype.toString.call(err) === '[object Object]') { - return done(new Error('done() invoked with non-Error: ' + JSON.stringify(err))); - } else { - return done(new Error('done() invoked with non-Error: ' + err)); - } - } - done(); - }); + if (this.allowUncaught) { + callFnAsync(this.fn); + } else try { + callFnAsync(this.fn); } catch (err) { done(utils.getError(err)); } return; } + if (this.allowUncaught) { + callFn(this.fn); + done(); + return; + } + // sync or promise-returning try { if (this.pending) { @@ -277,4 +275,18 @@ Runnable.prototype.run = function(fn){ done(); } } + + function callFnAsync(fn) { + fn.call(ctx, function(err){ + if (err instanceof Error || toString.call(err) === "[object Error]") return done(err); + if (null != err) { + if (Object.prototype.toString.call(err) === '[object Object]') { + return done(new Error('done() invoked with non-Error: ' + JSON.stringify(err))); + } else { + return done(new Error('done() invoked with non-Error: ' + err)); + } + } + done(); + }); + } }; diff --git a/lib/runner.js b/lib/runner.js index 3199dc921a..c281f42e66 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -383,7 +383,10 @@ Runner.prototype.runTest = function(fn){ if (this.asyncOnly) test.asyncOnly = true; - try { + if (this.allowUncaught) { + test.allowUncaught = true; + test.run(fn); + } else try { test.on('error', function(err){ self.fail(test, err); }); diff --git a/support/tail.js b/support/tail.js index 030177aad7..3a966f5fbd 100644 --- a/support/tail.js +++ b/support/tail.js @@ -53,7 +53,7 @@ process.on = function(e, fn){ if ('uncaughtException' == e) { global.onerror = function(err, url, line){ fn(new Error(err + ' (' + url + ':' + line + ')')); - return true; + return !mocha.allowUncaught; }; uncaughtExceptionHandlers.push(fn); } diff --git a/test/runnable.js b/test/runnable.js index 2508a5fa25..6d66e31012 100644 --- a/test/runnable.js +++ b/test/runnable.js @@ -132,6 +132,20 @@ describe('Runnable(title, fn)', function(){ }) }) }) + + describe('when an exception is thrown and is allowed to remain uncaught', function(){ + it('throws an error when it is allowed', function(done) { + var test = new Runnable('foo', function(){ + throw new Error('fail'); + }); + test.allowUncaught = true; + function fail() { + test.run(function(err) {}); + } + fail.should.throw('fail'); + done(); + }) + }) }) describe('when timeouts are disabled', function() { @@ -239,6 +253,21 @@ describe('Runnable(title, fn)', function(){ }); }) + describe('when an exception is thrown and is allowed to remain uncaught', function(){ + it('throws an error when it is allowed', function(done) { + var test = new Runnable('foo', function(done){ + throw new Error('fail'); + process.nextTick(done); + }); + test.allowUncaught = true; + function fail() { + test.run(function(err) {}); + } + fail.should.throw('fail'); + done(); + }) + }) + describe('when an error is passed', function(){ it('should invoke the callback', function(done){ var calls = 0; diff --git a/test/runner.js b/test/runner.js index a2ce512514..0177a131cb 100644 --- a/test/runner.js +++ b/test/runner.js @@ -292,6 +292,21 @@ describe('Runner', function(){ }) }); + describe('allowUncaught', function() { + it('should allow unhandled errors to propagate through', function(done) { + var newRunner = new Runner(suite); + newRunner.allowUncaught = true; + newRunner.test = new Test('failing test', function() { + throw new Error('allow unhandled errors'); + }); + function fail() { + newRunner.runTest(); + } + fail.should.throw('allow unhandled errors'); + done(); + }); + }); + describe('stackTrace', function() { var stack = [ 'AssertionError: foo bar' , 'at EventEmitter. (/usr/local/dev/test.js:16:12)'