Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 065d1a19473bc91ce904538e827c9f59ae0287f8 1 parent 8615e95
@kmiyashiro authored
View
3  .gitignore
@@ -12,4 +12,5 @@ logs
results
node_modules
-npm-debug.log
+npm-debug.log
+.DS_Store
View
22 LICENSE-MIT
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Kelly Miyashiro
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
View
36 README.md
@@ -1,4 +1,34 @@
-grunt-mocha-task
-================
+# grunt-mocha
-Grunt task for running mocha specs in a headless browser (PhantomJS)
+(package/README format heavily borrowed from [grunt-jasmine-task](https://github.com/jzaefferer/grunt-css) and builtin QUnit task)
+
+[Grunt](https://github.com/cowboy/grunt) plugin for running Mocha browser specs in a headless browser (PhantomJS)
+
+## Getting Started
+
+Install this grunt plugin next to your project's [grunt.js gruntfile][getting_started] with: `npm install grunt-mocha`
+
+(ie. the plugin is installed locally. If you want to install it globally - which is not recommended - check out the official [grunt documentation][plugin_docs])
+
+Then add this line to your project's `grunt.js` gruntfile at the bottom:
+
+```javascript
+grunt.loadNpmTasks('grunt-mocha');
+```
+
+Also add this to the ```grunt.initConfig``` object in the same file:
+
+```javascript
+mocha: {
+ index: ['specs/index.html']
+},
+```
+Replace ```specs/index.html``` with the location of your mocha spec running html file.
+
+Now you can run the mocha task with:
+
+```grunt mocha```
+
+## License
+Copyright (c) 2012 Kelly Miyashiro
+Licensed under the MIT license.
View
41 package.json
@@ -0,0 +1,41 @@
+{
+ "name": "grunt-mocha",
+ "description": "Grunt task for running Mocha specs",
+ "version": "0.1.0",
+ "homepage": "https://github.com/kmiyashiro/grunt-mocha",
+ "author": {
+ "name": "Kelly Miyashiro",
+ "email": "miyashiro.kelly@gmail.com",
+ "url": "http://non-diligent.com"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/kmiyashiro/grunt-mocha.git"
+ },
+ "bugs": {
+ "url": "https://github.com/kmiyashiro/grunt-mocha/issues"
+ },
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "https://github.com/kmiyashiro/grunt-mocha/blob/master/LICENSE-MIT"
+ }
+ ],
+ "main": "grunt.js",
+ "engines": {
+ "node": "*"
+ },
+ "scripts": {
+ "test": "grunt test"
+ },
+ "dependencies": {
+ "grunt": "~0.3.8",
+ "temporary": "~0.0.2"
+ },
+ "devDependencies": {
+ "grunt": "~0.3.8"
+ },
+ "keywords": [
+ "gruntplugin"
+ ]
+}
View
289 tasks/mocha.js
@@ -0,0 +1,289 @@
+// Important: You must install `temporary` `npm install temporary`
+/*
+ * grunt
+ * https://github.com/cowboy/grunt
+ *
+ * Copyright (c) 2012 "Cowboy" Ben Alman
+ * Licensed under the MIT license.
+ * http://benalman.com/about/license/
+ */
+
+module.exports = function(grunt) {
+ // Grunt utilities.
+ var task = grunt.task;
+ var file = grunt.file;
+ var utils = grunt.utils;
+ var log = grunt.log;
+ var verbose = grunt.verbose;
+ var fail = grunt.fail;
+ var option = grunt.option;
+ var config = grunt.config;
+ var template = grunt.template;
+
+ // Nodejs libs.
+ var fs = require('fs');
+ var path = require('path');
+ var growl = require('growl');
+
+ // External libs.
+ var Tempfile = require('temporary/lib/file');
+
+ // Keep track of the last-started module, test and status.
+ var currentModule, currentTest, status;
+ // Keep track of the last-started test(s).
+ var unfinished = {};
+
+ // Allow an error message to retain its color when split across multiple lines.
+ function formatMessage(str) {
+ return String(str).split('\n').map(function(s) { return s.magenta; }).join('\n');
+ }
+
+ // Keep track of failed assertions for pretty-printing.
+ var failedAssertions = [];
+ function logFailedAssertions() {
+ var assertion;
+ // Print each assertion error.
+ while (assertion = failedAssertions.shift()) {
+ verbose.or.error(assertion.testName);
+ log.error('Message: ' + formatMessage(assertion.message));
+ if (assertion.actual !== assertion.expected) {
+ log.error('Actual: ' + formatMessage(assertion.actual));
+ log.error('Expected: ' + formatMessage(assertion.expected));
+ }
+ if (assertion.source) {
+ log.error(assertion.source.replace(/ {4}(at)/g, ' $1'));
+ }
+ log.writeln();
+ }
+ }
+
+ // Handle methods passed from PhantomJS, including Mocha hooks.
+ var phantomHandlers = {
+ // Mocha hooks.
+ suiteStart: function(name) {
+ unfinished[name] = true;
+ currentModule = name;
+ },
+ suiteDone: function(name, failed, passed, total) {
+ delete unfinished[name];
+ },
+ // TODO: Make one of these for mocha, this is for qunit, see testFail
+ // log: function(result, actual, expected, message, source) {
+ // if (!result) {
+ // failedAssertions.push({
+ // actual: actual,
+ // expected: expected,
+ // message: message,
+ // source: source,
+ // testName: currentTest
+ // });
+ // }
+ // },
+ testStart: function(name) {
+ currentTest = (currentModule ? currentModule + ' - ' : '') + name;
+ verbose.write(currentTest + '...');
+ },
+ testFail: function(name, result) {
+ result.testName = currentTest;
+ failedAssertions.push(result);
+ },
+ testDone: function(title, state) {
+ // Log errors if necessary, otherwise success.
+ if (state == 'failed') {
+ // list assertions
+ if (option('verbose')) {
+ log.error();
+ // logFailedAssertions();
+ } else {
+ log.write('F'.red);
+ }
+ } else {
+ verbose.ok().or.write('.');
+ }
+ },
+ done: function(failed, passed, total, duration) {
+ status.failed += failed;
+ status.passed += passed;
+ status.total += total;
+ status.duration += duration;
+ // Print assertion errors here, if verbose mode is disabled.
+ if (!option('verbose')) {
+ if (failed > 0) {
+ log.writeln();
+ logFailedAssertions();
+ } else {
+ log.ok();
+ }
+ }
+ },
+ // Error handlers.
+ done_fail: function(url) {
+ verbose.write('Running PhantomJS...').or.write('...');
+ log.error();
+ grunt.warn('PhantomJS unable to load "' + url + '" URI.', 90);
+ },
+ done_timeout: function() {
+ log.writeln();
+ grunt.warn('PhantomJS timed out, possibly due to a missing Mocha run() call.', 90);
+ },
+
+ // console.log pass-through.
+ // console: console.log.bind(console),
+ // Debugging messages.
+ debug: log.debug.bind(log, 'phantomjs')
+ };
+
+ // ==========================================================================
+ // TASKS
+ // ==========================================================================
+
+ grunt.registerMultiTask('mocha', 'Run Mocha unit tests in a headless PhantomJS instance.', function() {
+ // Get files as URLs.
+ var urls = file.expandFileURLs(this.file.src);
+
+ // This task is asynchronous.
+ var done = this.async();
+
+ // Reset status.
+ status = {failed: 0, passed: 0, total: 0, duration: 0};
+
+ // Process each filepath in-order.
+ utils.async.forEachSeries(urls, function(url, next) {
+ var basename = path.basename(url);
+ verbose.subhead('Testing ' + basename).or.write('Testing ' + basename);
+
+ // Create temporary file to be used for grunt-phantom communication.
+ var tempfile = new Tempfile();
+ // Timeout ID.
+ var id;
+ // The number of tempfile lines already read.
+ var n = 0;
+
+ // Reset current module.
+ currentModule = null;
+
+ // Clean up.
+ function cleanup() {
+ clearTimeout(id);
+ tempfile.unlink();
+ }
+
+ // It's simple. As Mocha tests, assertions and modules begin and complete,
+ // the results are written as JSON to a temporary file. This polling loop
+ // checks that file for new lines, and for each one parses its JSON and
+ // executes the corresponding method with the specified arguments.
+ (function loopy() {
+ // Disable logging temporarily.
+ log.muted = true;
+ // Read the file, splitting lines on \n, and removing a trailing line.
+ var lines = file.read(tempfile.path).split('\n').slice(0, -1);
+ // Re-enable logging.
+ log.muted = false;
+ // Iterate over all lines that haven't already been processed.
+ var done = lines.slice(n).some(function(line) {
+ // Get args and method.
+ var args = JSON.parse(line);
+ var method = args.shift();
+ // Execute method if it exists.
+ if (phantomHandlers[method]) {
+ phantomHandlers[method].apply(null, args);
+ }
+ // If the method name started with test, return true. Because the
+ // Array#some method was used, this not only sets "done" to true,
+ // but stops further iteration from occurring.
+ return (/^done/).test(method);
+ });
+
+ if (done) {
+ // All done.
+ cleanup();
+ next();
+ } else {
+ // Update n so previously processed lines are ignored.
+ n = lines.length;
+ // Check back in a little bit.
+ id = setTimeout(loopy, 100);
+ }
+ }());
+
+ // Launch PhantomJS.
+ grunt.helper('phantomjs', {
+ code: 90,
+ args: [
+ // The main script file.
+ task.getFile('mocha/phantom-mocha-runner.js'),
+ // The temporary file used for communications.
+ tempfile.path,
+ // The Mocha helper file to be injected.
+ // task.getFile('../test/run-mocha.js'),
+ task.getFile('mocha/mocha-helper.js'),
+ // URL to the Mocha .html test file to run.
+ url,
+ // PhantomJS options.
+ '--config=' + task.getFile('mocha/phantom.json')
+ ],
+ done: function(err) {
+ if (err) {
+ cleanup();
+ done();
+ }
+ },
+ });
+ }, function(err) {
+ // All tests have been run.
+
+ // Log results.
+ if (status.failed > 0) {
+ growl(status.failed + ' of ' + status.total + ' tests failed!', {
+ image: 'tasks/mocha/error.png',
+ title: 'Tests Failed',
+ priority: 3
+ });
+ grunt.warn(status.failed + '/' + status.total + ' assertions failed (' +
+ status.duration + 'ms)', Math.min(99, 90 + status.failed));
+ } else {
+ growl('All Clear: ' + status.total + ' tests passed', {
+ title: 'Tests Passed',
+ image: 'tasks/mocha/ok.png'
+ });
+ verbose.writeln();
+ log.ok(status.total + ' assertions passed (' + status.duration + 'ms)');
+ }
+
+ // All done!
+ done();
+ });
+ });
+
+ // ==========================================================================
+ // HELPERS
+ // ==========================================================================
+
+ grunt.registerHelper('phantomjs', function(options) {
+ return utils.spawn({
+ cmd: 'phantomjs',
+ args: options.args
+ }, function(err, result, code) {
+ if (!err) { return options.done(null); }
+ // Something went horribly wrong.
+ verbose.or.writeln();
+ log.write('Running PhantomJS...').error();
+ if (code === 127) {
+ log.errorlns(
+ 'In order for this task to work properly, PhantomJS must be ' +
+ 'installed and in the system PATH (if you can run "phantomjs" at' +
+ ' the command line, this task should work). Unfortunately, ' +
+ 'PhantomJS cannot be installed automatically via npm or grunt. ' +
+ 'See the grunt FAQ for PhantomJS installation instructions: ' +
+ 'https://github.com/cowboy/grunt/blob/master/docs/faq.md'
+ );
+ grunt.warn('PhantomJS not found.', options.code);
+ } else {
+ result.split('\n').forEach(log.error, log);
+ grunt.warn('PhantomJS exited unexpectedly with exit code ' + code + '.', options.code);
+ }
+ options.done(code);
+ });
+ });
+
+};
View
BIN  tasks/mocha/error.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
69 tasks/mocha/mocha-helper.js
@@ -0,0 +1,69 @@
+/*
+ * Is injected into the spec runner file
+
+ * Copyright (c) 2012 Kelly Miyashiro
+ * Copyright (c) 2012 "Cowboy" Ben Alman
+ * Licensed under the MIT license.
+ * http://benalman.com/about/license/
+ */
+
+/*global mocha:true, alert:true*/
+
+// Send messages to the parent phantom.js process via alert! Good times!!
+function sendMessage() {
+ var args = [].slice.call(arguments);
+ alert(JSON.stringify(args));
+}
+
+var GruntReporter = function(runner){
+ mocha.reporters.HTML.call(this, runner);
+
+ /**
+ * Listen on `event` with callback `fn`.
+ */
+
+ // function on(el, event, fn) {
+ // if (el.addEventListener) {
+ // el.addEventListener(event, fn, false);
+ // } else {
+ // el.attachEvent('on' + event, fn);
+ // }
+ // }
+
+ runner.on('test', function(test) {
+ sendMessage('testStart', test.title);
+ });
+
+ runner.on('test end', function(test) {
+ sendMessage('testDone', test.title, test.state);
+ });
+
+ runner.on('suite', function(suite) {
+ sendMessage('suiteStart', suite.title);
+ });
+
+ runner.on('suite end', function(suite) {
+ if (suite.root) return;
+ sendMessage('suiteDone', suite.title);
+ });
+
+ runner.on('fail', function(test, err) {
+ sendMessage('testFail', test.title, err);
+ });
+
+ runner.on('end', function() {
+ var output = {
+ failed : this.failures,
+ passed : this.total - this.failures,
+ total : this.total
+ };
+
+ sendMessage('done', output.failed,output.passed, output.total);
+ });
+};
+
+mocha.setup({
+ ui: 'bdd',
+ ignoreLeaks: true,
+ reporter: GruntReporter
+});
View
BIN  tasks/mocha/ok.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
145 tasks/mocha/phantom-mocha-runner.js
@@ -0,0 +1,145 @@
+/*
+ * grunt
+ * https://github.com/cowboy/grunt
+ *
+ * Adapated for Mocha by Kelly Miyashiro (miyashiro.kelly@gmail.com)
+ *
+ * Copyright (c) 2012 "Cowboy" Ben Alman
+ * Licensed under the MIT license.
+ * http://benalman.com/about/license/
+ */
+
+/*global phantom:true*/
+
+var fs = require('fs');
+
+// The temporary file used for communications.
+var tmpfile = phantom.args[0];
+// The Mocha helper file to be injected.
+var mochaHelper = phantom.args[1];
+// The Mocha .html test file to run.
+var url = phantom.args[2];
+
+// Keep track of the last time a Mocha message was sent.
+var last = new Date();
+
+// Messages are sent to the parent by appending them to the tempfile.
+function sendMessage(args) {
+ last = new Date();
+ fs.write(tmpfile, JSON.stringify(args) + '\n', 'a');
+ // Exit when all done.
+ if (/^done/.test(args[0])) {
+ phantom.exit();
+ }
+}
+
+// Send a debugging message.
+function sendDebugMessage() {
+ sendMessage(['debug'].concat([].slice.call(arguments)));
+}
+
+// Abort if Mocha doesn't do anything for a while.
+setInterval(function() {
+ if (new Date() - last > 5000) {
+ sendMessage(['done_timeout']);
+ }
+}, 1000);
+
+// Create a new page.
+var page = require('webpage').create();
+
+// Mocha sends its messages via alert(jsonstring);
+page.onAlert = function(args) {
+ sendMessage(JSON.parse(args));
+};
+
+page.onError = function(msg, trace) {
+ var error = 'Page error: ' + msg + '\n';
+ trace.forEach(function(item) {
+ error += ' ' + item.file + ':' + item.line + '\n';
+ });
+ sendDebugMessage(error);
+};
+
+// Additional message sending
+page.onConsoleMessage = function(message) {
+ var output;
+ sendMessage(['console', message]);
+
+ // if (/^done/.test(message)) {
+ // output = JSON.parse(message.replace(/^done/, ''));
+ // output.unshift('done');
+ //
+ // sendMessage(output);
+ // return;
+ // }
+ //
+ // // if (/^testFail/.test(message)) {
+ // // output = JSON.parse(message.replace(/^testFail/, ''));
+ // // output.unshift(['log', null, true, false]);
+ // //
+ // // sendMessage(output);
+ // // return;
+ // // }
+ //
+ // var hooks = [
+ // 'testStart',
+ // 'testDone',
+ // 'testFail',
+ // 'suiteStart',
+ // 'suiteDone'
+ // ];
+ //
+ // for (var i = 0, len = hooks.length; i < len; i++) {
+ // var re = new RegExp('^' + hooks[i]);
+ //
+ // if (re.test(message)) {
+ // output = JSON.parse(message.replace(re, ''));
+ // output.unshift(hooks[i]);
+ //
+ // sendMessage(output);
+ // return;
+ // }
+ // }
+};
+
+// Keep track if Mocha has been injected already
+var injected;
+
+page.onResourceRequested = function(request) {
+ if (/\/mocha\.js$/.test(request.url)) {
+ // Reset injected to false, if for some reason a redirect occurred and
+ // the test page (including mocha.js) had to be re-requested.
+ injected = false;
+ }
+ sendDebugMessage('onResourceRequested', request.url);
+};
+page.onResourceReceived = function(request) {
+ if (request.stage === 'end') {
+ sendDebugMessage('onResourceReceived', request.url);
+ }
+};
+
+page.onInitialized = function() {
+ page.evaluate(function() {
+ window.PHANTOMJS = true;
+ });
+};
+
+page.open(url, function(status) {
+ // Only execute this code if Mocha has not yet been injected.
+ if (injected) { return; }
+ injected = true;
+ // The window has loaded.
+ if (status !== 'success') {
+ // File loading failure.
+ sendMessage(['done_fail', url]);
+ } else {
+ // Inject Mocha helper file.
+ sendDebugMessage('inject', mochaHelper);
+ page.injectJs(mochaHelper);
+ // Because injection happens after window load, "begin" must be sent
+ // manually.
+ sendMessage(['begin']);
+ }
+});
View
1  tasks/mocha/phantom.json
@@ -0,0 +1 @@
+{}
Please sign in to comment.
Something went wrong with that request. Please try again.