Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
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...
commit de201e8474b7138aab00a6810165ad3d3c5f6824 1 parent 3de574c
@ryanseddon ryanseddon authored
View
12 README.md
@@ -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
View
255 lib/hub/view/public/inject.js
@@ -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
22 scripts/postinstall.js
@@ -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
8 test/cli.js
@@ -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) {
View
28 test/fixture/jasmine.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+<head>
+ <title>Yeti Jasmine Test</title>
+ <link rel="stylesheet" type="text/css" href="../../dep/jasmine.css">
+ <script type="text/javascript" src="../../dep/jasmine.js"></script>
+ <script type="text/javascript" src="../../dep/jasmine-html.js"></script>
+</head>
+
+<body>
+
+<script type="text/javascript">
+ var jasmineEnv = jasmine.getEnv();
+ jasmineEnv.addReporter(new jasmine.HtmlReporter());
+
+ describe('ExampleSuite', function () {
+ it('should have a passing test', function() {
+ expect(true).toEqual(true);
+ });
+ });
+
+ if(!("$yetify" in window)) {
+ jasmineEnv.execute();
+ }
+</script>
+
+</body>
+</html>
View
26 test/fixture/mocha.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Yeti Mocha Test</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" href="../../dep/mocha.css" />
+ <script src="../../dep/mocha.js"></script>
+ <script src="../../dep/expect.js"></script>
+ </head>
+ <body>
+ <div id="mocha"></div>
+ <script>
+ mocha.setup('bdd');
+
+ describe('hi mocha', function(){
+ it('should have a passing test', function(){
+ expect(true).to.be(true);
+ })
+ });
+
+ if(!("$yetify" in window)) {
+ var runner = mocha.run();
+ }
+ </script>
+ </body>
+</html>
Please sign in to comment.
Something went wrong with that request. Please try again.