Permalink
Browse files

Support other test frameworks & better output for QUnit. Fix #112

Added support for Jasmine, Mocha and better output in
QUnit to include expected / actual comparisons.
  • Loading branch information...
ryanseddon committed Nov 17, 2012
1 parent 3de574c commit de201e8474b7138aab00a6810165ad3d3c5f6824
Showing with 333 additions and 18 deletions.
  1. +5 −7 README.md
  2. +249 −6 lib/hub/view/public/inject.js
  3. +20 −2 scripts/postinstall.js
  4. +5 −3 test/cli.js
  5. +28 −0 test/fixture/jasmine.html
  6. +26 −0 test/fixture/mocha.html
View
@@ -4,8 +4,7 @@
Yeti is a command-line tool for launching JavaScript unit tests in a browser
and reporting the results without leaving your terminal.
-Yeti is designed to work with tests built on [YUI Test][yuitest]
-or [QUnit][] just as they are.
+Yeti is designed to work with tests built on [YUI Test][yuitest], [QUnit][], [Mocha][] or [Jasmine][] just as they are.
## Install Yeti
@@ -229,12 +228,9 @@ It's tested on Linux and OS X.
You must start Yeti's client in the directory you'll be serving tests from. For security reasons, Yeti will reject requests that try to access files outside of the directory you start Yeti in.
-### Limited QUnit support
+### Full QUnit, Mocha and Jasmine support
-QUnit does not provide a detailed result summary when testing completes;
-instead, QUnit requires Yeti to collect results as testing runs.
-This is not implemented. Therefore, QUnit test details are limited
-to total, pass, and fail test counts.
+QUnit, Mocha and Jasmine testing frameworks have full support in Yeti including errors and actual / expected output on failing tests.
## Install latest Yeti snapshot
@@ -350,5 +346,7 @@ for license text and copyright information.
[YUI]: http://yuilibrary.com/
[yuitest]: http://yuilibrary.com/yuitest/
[QUnit]: http://qunitjs.com/
+ [Mocha]: http://visionmedia.github.com/mocha/
+ [Jasmine]: http://pivotal.github.com/jasmine/
[doctype]: http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#the-doctype
[No-Quirks Mode]: http://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#no-quirks-mode
@@ -1,4 +1,4 @@
-/*global window, YUI, Image, SimpleEvents, SockJS */
+/*global window, YUI, Image, SimpleEvents, SockJS, document */
YUI.add("tempest-base-core", function (Y, name) {
"use strict";
@@ -781,14 +781,257 @@ window.$yetify = function $yetify(options) {
detectFn: function (win) {
return win.QUnit;
},
- bindFn: function (win) {
+ bindFn: function (win, undef) {
+ var self = this,
+ tostring = {}.toString,
+ qunit = win.QUnit,
+ data = {},
+ tests = {},
+ count = 1,
+ curTestName;
+
+ function complete(results) {
+ self.fire("results", results);
+ }
+
+ function type(obj) {
+ return tostring.call(obj).match(/^\[object\s+(.*?)\]$/)[1];
+ }
+
+ function message(result) {
+ if (result.result === "fail") {
+ if (result.actual !== undef && result.expected !== undef) {
+ if (!result.message) {
+ result.message = "";
+ }
+ var expectedType = type(result.expected),
+ actualType = type(result.actual);
+
+ result.message = result.message + "\nExpected: " + result.expected.toString() + " (" + expectedType + ")\nActual: " + result.actual.toString() + " (" + actualType + ")";
+
+ // Delete props so we don't get any circular refs
+ delete result.actual;
+ delete result.expected;
+ }
+ }
+
+ return result.message || "";
+ }
+
+ qunit.log = function (test) {
+ tests["test" + count] = {
+ message: test.message,
+ result: (test.result) ? test.result : "fail",
+ name: "test" + count,
+ actual: test.actual,
+ expected: test.expected
+ };
+
+ count = count + 1;
+ };
+
+ qunit.moduleStart = function (test) {
+ curTestName = test.name;
+ };
+
+ qunit.moduleDone = function (test) {
+ var testName = curTestName,
+ i;
+
+ data[testName] = {
+ name: testName,
+ passed: test.passed,
+ failed: test.failed,
+ total: test.total
+ };
+
+ for (i in tests) {
+ data[testName][tests[i].name] = {
+ result: tests[i].result,
+ message: message(tests[i]),
+ name: tests[i].name
+ };
+ }
+
+ tests = {};
+ count = 1;
+ };
+
+ qunit.done = function (tests) {
+ var results = data;
+
+ results.passed = tests.passed;
+ results.failed = tests.failed;
+ results.total = tests.total;
+ results.duration = tests.runtime;
+ results.name = document.title;
+
+ complete(results);
+ };
+
+ qunit.start();
+ }
+ });
+
+ driver.addAutomation("test", "jasmine", {
+ detectFn: function (win) {
+ return win.jasmine;
+ },
+ bindFn: function (win, undef) {
+ var self = this,
+ tostring = {}.toString,
+ jasmine = win.jasmine,
+ env = jasmine.getEnv(),
+ data = { name: "" },
+ reporter = new jasmine.JsApiReporter();
+
+ env.addReporter(reporter);
+
+ function complete(results) {
+ self.fire("results", results);
+ }
+
+ function type(obj) {
+ return tostring.call(obj).match(/^\[object\s+(.*?)\]$/)[1];
+ }
+
+ function message(result) {
+ if (!result.passed_) {
+ if (result.actual !== undef && result.expected !== undef) {
+ result.message = result.message + "\nExpected: " + result.expected.toString() + " (" + type(result.expected) + ")\nActual: " + result.actual.toString() + " (" + type(result.actual) + ")";
+
+ // Delete props so we don't get any circular refs
+ delete result.actual;
+ delete result.expected;
+ }
+ }
+ return result.message;
+ }
+
+ reporter.reportRunnerStarting = function (runner) {
+ data.name = runner.queue.blocks[0].description;
+ };
+
+ // This will fire for each test passing an object of lot's of juicy info
+ reporter.reportSpecResults = function (runner) {
+ var suite = runner.suite,
+ suiteName = suite.description,
+ results = suite.results(),
+ test = runner.results_.items_[0];
+
+ // If suite already exists update object info, otherwise create it
+ if (data[suiteName]) {
+ data[suiteName].passed = results.passedCount;
+ data[suiteName].failed = results.failedCount;
+ data[suiteName].total = results.totalCount;
+ } else {
+ data[suiteName] = {
+ name: suiteName,
+ passed: results.passedCount,
+ failed: results.failedCount,
+ total: results.totalCount
+ };
+ }
+
+ data[suiteName][runner.description] = {
+ result: (test && test.passed_) ? test.passed_ : "fail",
+ message: test ? message(test):'',
+ name: runner.description
+ };
+
+ self.fire("beat", data[suiteName]);
+ };
+
+ // Fires when test runner has completed
+ reporter.reportRunnerResults = function (suite) {
+ var tests = suite.results(),
+ results = data;
+
+ results.passed = tests.passedCount || 0;
+ results.failed = tests.failedCount || 0;
+ results.total = tests.totalCount;
+ // TODO: How do I get the test suite runtime?
+ results.duration = 0;
+
+ complete(results);
+ };
+
+ env.execute();
+ }
+ });
+
+ driver.addAutomation("test", "mocha", {
+ detectFn: function (win) {
+ return win.mocha;
+ },
+ bindFn: function (win, undef) {
var self = this,
- q = win.QUnit;
+ tostring = {}.toString,
+ mocha = win.mocha,
+ runner = mocha.run(),
+ data = {},
+ tests = {},
+ passed = 0,
+ failed = 0,
+ total = 0;
+
+ function complete(results) {
+ self.fire("results", results);
+ }
+
+ runner.ignoreLeaks = true;
+
+ runner.on('test end', function (test) {
+ var suiteName = test.title;
+
+ tests[suiteName] = {
+ message: (test.state === 'failed') ? test.err.message : "",
+ result: (test.state === 'passed') ? true : "fail",
+ name: suiteName
+ };
+
+ passed = (test.state === 'passed') ? passed + 1 : passed;
+ failed = (test.state === 'failed') ? failed + 1 : failed;
+ total = total + 1;
+ });
+
- q.done = Y.bind(self.fire, self, "results");
- q.log = Y.bind(self.fire, self, "beat");
+ runner.on('suite end', function (module) {
+ if (module.suites.length === 0) {
+ var suiteName = module.fullTitle(),
+ i;
+
+ data[suiteName] = {
+ name: suiteName,
+ passed: passed,
+ failed: failed,
+ total: total
+ };
+
+ for (i in tests) {
+ data[suiteName][tests[i].name] = {
+ result: tests[i].result,
+ message: tests[i].message,
+ name: tests[i].name
+ };
+ }
+
+ tests = {};
+ passed = failed = total = 0;
+ }
+ });
+ runner.on('end', function (test) {
+ var results = data;
- q.start();
+ results.passed = (runner.total - runner.failures) || 0;
+ results.failed = runner.failures || 0;
+ results.total = runner.total;
+ // TODO: How do I get the test suite runtime?
+ results.duration = 0;
+ results.name = document.title;
+
+ complete(results);
+ });
}
});
View
@@ -5,7 +5,8 @@
var fs = require("fs"),
url = require("url"),
path = require("path"),
- http = require("http");
+ http = require("http"),
+ https = require("https");
var depDir = path.join(__dirname, "..", "dep");
@@ -14,6 +15,14 @@ var YUI_TEST_URL = "http://yui.yahooapis.com/combo?3.7.3/build/yui-base/yui-base
var QUNIT_JS_URL = "http://code.jquery.com/qunit/qunit-1.10.0.js";
var QUNIT_CSS_URL = "http://code.jquery.com/qunit/qunit-1.10.0.css";
+var JASMINE_JS_URL = "https://raw.github.com/pivotal/jasmine/master/lib/jasmine-core/jasmine.js";
+var JASMINE_JS_REPORTER_URL = "https://raw.github.com/pivotal/jasmine/master/lib/jasmine-core/jasmine-html.js";
+var JASMINE_CSS_URL = "https://raw.github.com/pivotal/jasmine/master/lib/jasmine-core/jasmine.css";
+
+var MOCHA_JS_URL = "https://raw.github.com/visionmedia/mocha/master/mocha.js";
+var MOCHA_JS_ASSERTION_URL = "https://raw.github.com/LearnBoost/expect.js/master/expect.js";
+var MOCHA_CSS_URL = "https://raw.github.com/visionmedia/mocha/master/mocha.css";
+
var YUI_RUNTIME_URL = "http://yui.yahooapis.com/combo?3.7.3/build/yui-base/yui-base-min.js&3.7.3/build/oop/oop-min.js&3.7.3/build/event-custom-base/event-custom-base-min.js&3.7.3/build/event-custom-complex/event-custom-complex-min.js&3.7.3/build/attribute-events/attribute-events-min.js&3.7.3/build/attribute-core/attribute-core-min.js&3.7.3/build/base-core/base-core-min.js&3.7.3/build/cookie/cookie-min.js&3.7.3/build/array-extras/array-extras-min.js";
function log() {
@@ -62,6 +71,9 @@ function die(message) {
}
function saveURLToDep(sourceURL, filename, cb) {
+ var protocol = url.parse(sourceURL).protocol;
+
+ protocol = (protocol === "http:") ? http : https;
filename = path.join(depDir, filename);
function done() {
@@ -70,7 +82,7 @@ function saveURLToDep(sourceURL, filename, cb) {
log("Saving", sourceURL, "as", filename);
- http.get(url.parse(sourceURL), function onResponse(res) {
+ protocol.get(url.parse(sourceURL), function onResponse(res) {
if (res.statusCode !== 200) {
die("Got status " + res.statusCode + " for URL " + sourceURL);
return;
@@ -99,6 +111,12 @@ function download(err) {
[YUI_TEST_URL, "yui-test.js"],
[QUNIT_JS_URL, "qunit.js"],
[QUNIT_CSS_URL, "qunit.css"],
+ [JASMINE_JS_URL, "jasmine.js"],
+ [JASMINE_JS_REPORTER_URL, "jasmine-html.js"],
+ [JASMINE_CSS_URL, "jasmine.css"],
+ [MOCHA_JS_URL, "mocha.js"],
+ [MOCHA_JS_ASSERTION_URL, "expect.js"],
+ [MOCHA_CSS_URL, "mocha.css"],
[YUI_RUNTIME_URL, "yui-runtime.js"],
["http://cdn.sockjs.org/sockjs-0.3.min.js", "sock.js"]
].forEach(function downloader(args) {
View
@@ -103,7 +103,9 @@ vows.describe("Yeti CLI").addBatch({
"cli.js",
"-p", "9011",
__dirname + "/fixture/basic.html",
- __dirname + "/fixture/qunit.html"
+ __dirname + "/fixture/qunit.html",
+ __dirname + "/fixture/jasmine.html",
+ __dirname + "/fixture/mocha.html"
]);
}),
"prints hub creation message on stderr": function (topic) {
@@ -149,7 +151,7 @@ vows.describe("Yeti CLI").addBatch({
assert.isUndefined(topic.stack);
},
"the stderr output contains the test results": function (topic) {
- assert.include(topic.output, "2 tests passed");
+ assert.include(topic.output, "4 tests passed");
},
"the stderr output contains Agent complete": function (topic) {
assert.include(topic.output, "Agent complete");
@@ -166,7 +168,7 @@ vows.describe("Yeti CLI").addBatch({
"node",
"cli.js",
"-p", "9012",
- __dirname + "/fixture/query-string.html",
+ __dirname + "/fixture/query-string.html"
]);
}),
"prints hub creation message on stderr": function (topic) {
Oops, something went wrong.

0 comments on commit de201e8

Please sign in to comment.