diff --git a/lib/reporters/brief.js b/lib/reporters/brief.js index 91fb7a4..a4d8148 100644 --- a/lib/reporters/brief.js +++ b/lib/reporters/brief.js @@ -412,3 +412,5 @@ BriefReporter.prototype = module.exports = { } } }; + +module.exports["suite:error"] = module.exports["uncaughtException"]; diff --git a/lib/reporters/json-proxy.js b/lib/reporters/json-proxy.js index b727f8b..d055ef2 100644 --- a/lib/reporters/json-proxy.js +++ b/lib/reporters/json-proxy.js @@ -110,6 +110,15 @@ }); }, + "suite:error": function (error) { + this.emit("suite:error", { + name: error.name, + message: error.message, + stack: error.stack, + uuid: error.uuid + }); + }, + "test:success": function (test) { this.emit("test:success", { name: test.name, diff --git a/lib/reporters/specification.js b/lib/reporters/specification.js index b350ba3..58a4c33 100644 --- a/lib/reporters/specification.js +++ b/lib/reporters/specification.js @@ -141,7 +141,7 @@ module.exports = { return prefix + (prefix.length > 0 ? " " : ""); }, - printStats: function (stats) { + "suite:end": function (stats) { this.printUncaughtExceptions(); stats = stats || {}; @@ -244,3 +244,4 @@ module.exports = { }; module.exports["test:error"] = module.exports["test:failure"]; +module.exports["suite:error"] = module.exports["uncaughtException"]; diff --git a/lib/reporters/xml.js b/lib/reporters/xml.js index a76b0e7..c39bad2 100644 --- a/lib/reporters/xml.js +++ b/lib/reporters/xml.js @@ -224,5 +224,6 @@ }; xmlReporter["test:timeout"] = xmlReporter["test:failure"]; + xmlReporter["suite:error"] = xmlReporter["uncaughtException"]; return xmlReporter; }); diff --git a/lib/test-runner.js b/lib/test-runner.js index 5d5c636..28f7afb 100644 --- a/lib/test-runner.js +++ b/lib/test-runner.js @@ -159,10 +159,6 @@ return unsatiesfied; } - function isAssertionError(err) { - return err && err.name === "AssertionError"; - } - function prepareResults(results, error) { return _.extend({}, results, { ok: (results.failures + results.errors + results.timeouts === 0) && !(error instanceof Error) @@ -298,14 +294,6 @@ }; } - function rejected(deferred) { - if (!deferred) { - deferred = when.defer(); - } - deferred.reject(); - return deferred.promise; - } - function listenForUncaughtExceptions() { var listener, listening = false; onUncaught = function (l) { @@ -434,6 +422,7 @@ this.configuration = opt.configuration; this.clients = 1; this.concurrent = false; + this.allowFocusMode = typeof(opt.allowFocusMode) === "undefined" || opt.allowFocusMode; if (opt.random === false) { this.randomize = function (coll) { return coll; }; @@ -488,6 +477,9 @@ if (ctxs.length == 0) { runSuitePromise = when.reject(new Error("No contexts")); } + if (this.focusMode && !this.allowFocusMode) { + runSuitePromise = when.reject(new Error("Focus mode is disabled")); + } if (!runSuitePromise) { runSuitePromise = this.runContexts(ctxs); } @@ -495,9 +487,14 @@ return runSuitePromise.always(_.bind(function (e) { var res = prepareResults(this.results, e); res.uuid = this.runtime && this.runtime.uuid; + if (e instanceof Error) { + // emit the error for display in the reporter + this.emit("suite:error", e); + } + this.emit("suite:end", res); + if (e instanceof Error) { - // @todo: this error should be displayed somewhere! throw e; // rethrow to reject the promise } return res; @@ -516,7 +513,7 @@ }, runContext: function (context, thisProto) { - if (!context) { return rejected(); } + if (!context) { return when.reject(new Error("Empty context")); } var reqs = unsatiesfiedRequirements(context); if (reqs.length > 0) { return when(emitUnsupported(this, context, reqs)); diff --git a/test/reporters/brief-test.js b/test/reporters/brief-test.js index 41d20e5..f9db962 100644 --- a/test/reporters/brief-test.js +++ b/test/reporters/brief-test.js @@ -759,6 +759,22 @@ helper.testCase("Brief reporter uncaught exceptions", { } }); +helper.testCase("Brief reporter prints suite:error", { + setUp: function () { + reporterSetUp.call(this); + this.firefox.emit("suite:configuration", { tests: 2 }); + }, + + "prints uncaught errors continuously": function () { + this.firefox.emit("context:start", { name: "Stuff" }); + + this.firefox.emit("suite:error", new Error("Test error")); + + this.assertIO("Uncaught exception in Firefox 16.0 on Ubuntu 64-bit:"); + this.assertIO("Error: Test error"); + } +}); + helper.testCase("Brief reporter messages", { setUp: function () { reporterSetUp.call(this); diff --git a/test/reporters/json-proxy-test.js b/test/reporters/json-proxy-test.js index 7690848..f9ba083 100644 --- a/test/reporters/json-proxy-test.js +++ b/test/reporters/json-proxy-test.js @@ -345,6 +345,25 @@ stack: "Trouble@here", uuid: "1" }); + }, + + "emits serializable error to suite:error": function () { + var listener = sinon.spy(); + this.proxy.on("suite:error", listener); + + var error = new Error("Oops"); + error.uuid = "1"; + this.runner.emit("suite:error", error); + + assert(listener.calledOnce); + + var actualSerializedError = listener.args[0][0]; + assert.match(actualSerializedError, { + name: "Error", + message: "Oops", + uuid: "1" + }); + assert.match(actualSerializedError.stack, "Error: Oops"); } }); diff --git a/test/reporters/specification-test.js b/test/reporters/specification-test.js index 3b2323b..7a90de8 100644 --- a/test/reporters/specification-test.js +++ b/test/reporters/specification-test.js @@ -206,6 +206,33 @@ helper.testCase("SpecificationReporterTestsRunning", { }); assert.match(this.out.toString(), "Skipping Stuff, unsupported requirements:\n localStorage\n document\n"); + }, + + "prints summary at the end": function () { + this.runner.emit("suite:end", { + contexts: 2, + tests: 2, + assertions: 2, + failures: 0, + errors: 0, + ok: true + }); + + assert.match(this.out.toString(), "2 test cases, 2 tests, 2 assertions, 0 failures, 0 errors, 0 timeouts"); + }, + + "prints uncaught exceptions at the end": function () { + this.runner.emit("uncaughtException", new Error("Don't catch me now")); + this.runner.emit("suite:end"); + assert.match(this.out.toString(), "Uncaught exception!"); + assert.match(this.out.toString(), "Error: Don't catch me now"); + }, + + "prints suite:error as uncaught exception": function () { + this.runner.emit("suite:error", new Error("No exclamations for exceptions - it's bad enough - don't shout")); + this.runner.emit("suite:end"); + assert.match(this.out.toString(), "Uncaught exception!"); + assert.match(this.out.toString(), "No exclamations for exceptions - it's bad enough - don't shout"); } }); diff --git a/test/reporters/xml-test.js b/test/reporters/xml-test.js index 0e6d668..41fd182 100644 --- a/test/reporters/xml-test.js +++ b/test/reporters/xml-test.js @@ -386,5 +386,14 @@ helper.testCase("XMLReporterTest", { this.runner.emit("suite:end"); refute.match(this.outputStream.toString(), "time=\"0\"name=\"#1\""); + }, + + "includes failure element for suite:error": function () { + this.runner.emit("suite:error", new Error("Borked test suite")); + this.runner.emit("suite:end"); + + this.assertIO(""); + this.assertIO(""); + this.assertIO(''); } }); diff --git a/test/test-runner-test.js b/test/test-runner-test.js index c701469..9b232b0 100644 --- a/test/test-runner-test.js +++ b/test/test-runner-test.js @@ -120,6 +120,16 @@ refute(runner.concurrent); assert.equals(runner.clients, 1); + }, + + "allows focus mode by default": function () { + var runner = testRunner.create(); + assert(runner.allowFocusMode); + }, + + "disallows focus mode via options": function () { + var runner = testRunner.create({ allowFocusMode: false }); + refute(runner.allowFocusMode); } }); @@ -785,11 +795,19 @@ }, "rejects when no contexts provided": function (done) { - var onSuiteEnd = sinon.spy(); + var onSuiteEnd = sinon.spy(), + onSuiteError = sinon.spy(); + this.runner.on('suite:end', onSuiteEnd); - this.runner.runSuite([]).then(null, done(function () { + this.runner.on('suite:error', onSuiteError); + this.runner.runSuite([]).then(null, done(function (err) { assert(onSuiteEnd.calledOnce, "Should still emit `suite:end`"); refute(onSuiteEnd.getCall(0).args[0].ok, "`suite:end` should emit with result.ok == false"); + + assert(onSuiteError.calledOnce, "Should have emitted `suite:error`"); + assert.same(onSuiteError.getCall(0).args[0], err, "Should have emitted with the same error as rejection"); + assert(err instanceof Error); + assert.equals(err.message, "No contexts"); })); } }); @@ -2838,6 +2856,46 @@ assert.isString(e.uuid); })); this.runner.runSuite([context]); + }, + + "fails when focus mode disabled": function (done) { + var onError = sinon.spy(); + + var runner = testRunner.create({ allowFocusMode: false }); + runner.on("suite:error", onError); + var context = testCase("Test", { + "=>don't do it": function () {} + }); + + runner.runSuite([ context ]).then(function () { + assert.fail("runSuite promise should have been rejected"); + }, function (err) { + assert(err instanceof Error); + assert.equals(err.message, "Focus mode is disabled"); + assert(onError.calledOnce, "Should have emitted `suite:error`"); + assert.same(onError.getCall(0).args[0], err, "`suite:error` should emit the same error as promise rejection"); + done(); + }) + }, + + "runs when focus mode disabled, but no focus rocket used": function (done) { + var onError = sinon.spy(); + + var runner = testRunner.create({ allowFocusMode: false }); + runner.on("suite:error", onError); + + var unfocused = sinon.spy(); + var context = testCase("Test", { + "do it": unfocused + }); + + runner.runSuite([ context ]).then(function () { + refute(onError.callCount, "Don't emit `suite:error` unnecessarily"); + assert(unfocused.calledOnce); + done(); + }, function () { + assert.fail("runSuite promise should have been rejected"); + }) } });