Commit 2af86592 authored by Solly Ross's avatar Solly Ross

Support Running Mocha Tests from the Console

Previously, the only way to run the Mocha tests
(in 'test.*.js') is to write a web page to wrap
them (or use a provided one), and then load that
file in a browser.

This commit introduces a series of files to allow
you to run the Mocha tests from the command line
instead.

Normally, Mocha tests can be run from
the command line anyway.  However, since this
project was designed to work in web browsers
and not node, the code doesn't contain the
proper `require` calls, nor does it contain the
proper `module.exports` declarations.  Additionally,
some of the code is dependent on having a browser
environment.

To overcome these issues, a headless browser environment
is used.  The command file introduced in the commit,
`run_from_console.js`, can use one of two environments:
ZombieJS, a pure-javascript headless browser simulator, or
SpookyJS/CasperJS/PhantomJS, an actually WebKit-based
environment.

Because the environment-dependent code is separated
out in to different files ('run_from_console.zombie.js'
and 'run_from_console.casper.js'), the program can be
safely used if only one of the supported environments
is installed.

Additionally, the command will automatically generate
HTML and inject the required tests if there is no
pre-existing HTML file (although you can still use
pre-existing HTML files if you want to).

The required NPM modules for the base program are:

- commander
- ansi
- mocha (must be installed locally for the HTML files to use)
- chai (must be installed locally for the HTML files to use)
- temp

For Zombie, you need:

- zombie
- q

For Casper, you need:

- casperjs (must be installed locally in order to work properly)
- phantomjs
- phantom
- spooky

The command itself can be invoked as

   $ node run_from_console.js -t html_files

or

   $ node run_from_console.js -t js_test_files -i js_required_files

In both cases, the 'files' options should be a comma-separated list of
files.  The first case runs pre-existing HTML files.  The second case
generates HTML files to run the specified Mocha tests, and injects
the requirements specified as well.

Additionally, there are extra arguments that apply to both forms:
'-a' can be used to print all test results, not just the failures,
'-c' may be used to force color to be enabled (when outputting to
a pipe, such as when `less -R` is in use), and '-e' is used to
set the environment.  Use the '-h' or '--help' options to see
a detailed description of all options, and their long-form versions.
parent 75d69b9f
var Spooky = require('spooky');
var path = require('path');
var phantom_path = require('phantomjs').path;
var casper_path = path.resolve(__dirname, 'node_modules/casperjs/bin/casperjs');
process.env['PHANTOMJS_EXECUTABLE'] = phantom_path;
var casper_opts = {
child: {
transport: 'http',
command: casper_path
},
casper: {
logLevel: 'debug',
verbose: true
}
}
var provide_emitter = function(file_paths) {
var spooky = new Spooky(casper_opts, function(err) {
if (err) {
if (err.stack) console.warn(err.stack);
else console.warn(err);
return;
}
spooky.start('about:blank');
file_paths.forEach(function(file_path, path_ind) {
spooky.thenOpen('file://'+file_path);
spooky.then([{ path_ind: path_ind }, function() {
var res_json = {
file_ind: path_ind
}
res_json.num_tests = this.evaluate(function() { return document.querySelectorAll('li.test').length });
res_json.num_passes = this.evaluate(function() { return document.querySelectorAll('li.test.pass').length });
res_json.num_fails = this.evaluate(function() { return document.querySelectorAll('li.test.fail').length });
res_json.num_slow = this.evaluate(function() { return document.querySelectorAll('li.test.pass:not(.fast)').length });
res_json.duration = this.evaluate(function() { return document.querySelector('li.duration em').textContent });
res_json.suites = this.evaluate(function() {
var traverse_node = function(elem) {
if (elem.classList.contains('suite')) {
var res = {
type: 'suite',
name: elem.querySelector('h1').textContent,
has_subfailures: elem.querySelectorAll('li.test.fail').length > 0,
}
var child_elems = elem.querySelector('ul').children;
res.children = Array.prototype.map.call(child_elems, traverse_node);
return res;
}
else {
var h2_content = elem.querySelector('h2').childNodes;
var res = {
type: 'test',
text: h2_content[0].textContent,
}
if (elem.classList.contains('pass')) {
res.pass = true;
res.slow = !elem.classList.contains('fast');
res.duration = h2_content[1].textContent;
}
else {
res.error = elem.querySelector('pre.error').textContent;
}
return res;
}
}
var top_suites = document.querySelectorAll('#mocha-report > li.suite');
return Array.prototype.map.call(top_suites, traverse_node);
});
res_json.replay = this.evaluate(function() { return document.querySelector('a.replay').textContent });
this.emit('test_ready', res_json);
}]);
});
spooky.run();
});
return spooky;
}
module.exports = {
provide_emitter: provide_emitter,
name: 'SpookyJS (CapserJS on PhantomJS)'
}
#!/usr/bin/env node
var ansi = require('ansi');
var program = require('commander');
var path = require('path');
var make_list = function(val) {
return val.split(',');
}
program
.option('-t, --tests <testlist>', 'Run the specified html-file-based test(s). \'testlist\' should be a comma-separated list', make_list, [])
.option('-a, --print-all', 'Print all tests, not just the failures')
.option('--disable-color', 'Explicitly disable color')
.option('-c, --color', 'Explicitly enable color (default is to use color when not outputting to a pipe)')
.option('-i, --auto-inject <includefiles>', 'Treat the test list as a set of mocha JS files, and automatically generate HTML files with which to test test. \'includefiles\' should be a comma-separated list of paths to javascript files to include in each of the generated HTML files', make_list, null)
.option('-p, --provider <name>', 'Use the given provider (defaults to "casper"). Currently, may be "casper" or "zombie"', 'casper')
.parse(process.argv);
var file_paths = [];
if (program.autoInject) {
var temp = require('temp');
var fs = require('fs');
temp.track();
var template = {
header: "<html>\n<head>\n<meta charset='utf-8' />\<link rel='stylesheet' href='node_modules/mocha/mocha.css'/>\n</head>\n<body><div id='mocha'></div>",
script_tag: function(p) { return "<script src='" + p + "'></script>" },
footer: "<script>\nmocha.checkLeaks();\nmocha.globals(['navigator', 'create', 'ClientUtils', '__utils__']);\nmocha.run();\n</script>\n</body>\n</html>"
};
template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/chai/chai.js'));
template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/mocha/mocha.js'));
template.header += "\n<script>mocha.setup('bdd');</script>";
template.header = program.autoInject.reduce(function(acc, sn) {
return acc + "\n" + template.script_tag(path.resolve(process.cwd(), sn));
}, template.header);
file_paths = program.tests.map(function(jsn, ind) {
var templ = template.header;
templ += "\n";
templ += template.script_tag(path.resolve(process.cwd(), jsn));
templ += template.footer;
var tempfile = temp.openSync({ prefix: 'novnc-zombie-inject-', suffix: '-file_num-'+ind+'.html' });
fs.writeSync(tempfile.fd, templ);
fs.closeSync(tempfile.fd);
return tempfile.path;
});
}
else {
file_paths = program.tests.map(function(fn) {
return path.resolve(process.cwd(), fn);
});
}
var failure_count = 0;
var use_ansi = false;
if (program.color) use_ansi = true;
else if (program.disableColor) use_ansi = false;
else if (process.stdout.isTTY) use_ansi = true;
var cursor = ansi(process.stdout, { enabled: use_ansi });
var prov = require(path.resolve(__dirname, 'run_from_console.'+program.provider+'.js'));
cursor
.write("Running tests ")
.bold()
.write(program.tests.join(', '))
.reset()
.grey()
.write(' using provider '+prov.name)
.reset()
.write("\n");
//console.log("Running tests %s using provider %s", program.tests.join(', '), prov.name);
var provider = prov.provide_emitter(file_paths);
provider.on('test_ready', function(test_json) {
console.log('');
filename = program.tests[test_json.file_ind];
cursor.bold();
console.log('Results for %s:', filename);
console.log(Array('Results for :'.length+filename.length+1).join('='));
cursor.reset();
console.log('');
cursor.write(''+test_json.num_tests+' tests run, ')
cursor
.green()
.write(''+test_json.num_passes+' passed');
if (test_json.num_slow > 0) {
cursor
.reset()
.write(' (');
cursor
.yellow()
.write(''+test_json.num_slow+' slow')
.reset()
.write(')');
}
cursor
.reset()
.write(', ');
cursor
.red()
.write(''+test_json.num_fails+' failed');
cursor
.reset()
.write(' -- duration: '+test_json.duration+"\n");
console.log('');
if (test_json.num_fails > 0 || program.printAll) {
var traverse_tree = function(indentation, node) {
if (node.type == 'suite') {
if (!node.has_subfailures && !program.printAll) return;
if (indentation == 0) {
cursor.bold();
console.log(node.name);
console.log(Array(node.name.length+1).join('-'));
cursor.reset();
}
else {
cursor
.write(Array(indentation+3).join('#'))
.bold()
.write(' '+node.name+' ')
.reset()
.write(Array(indentation+3).join('#'))
.write("\n");
}
console.log('');
for (var i = 0; i < node.children.length; i++) {
traverse_tree(indentation+1, node.children[i]);
}
}
else {
if (!node.pass) {
cursor.magenta();
console.log('- failed: '+node.text+test_json.replay);
cursor.red();
console.log(' '+node.error.split("\n")[0]); // the split is to avoid a weird thing where in PhantomJS, we get a stack trace too
cursor.reset();
console.log('');
}
else if (program.printAll) {
if (node.slow) cursor.yellow();
else cursor.green();
cursor
.write('- pass: '+node.text)
.grey()
.write(' ('+node.duration+') ');
/*if (node.slow) cursor.yellow();
else cursor.green();*/
cursor
//.write(test_json.replay)
.reset()
.write("\n");
console.log('');
}
}
}
for (var i = 0; i < test_json.suites.length; i++) {
traverse_tree(0, test_json.suites[i]);
}
}
if (test_json.num_fails == 0) {
cursor.fg.green();
console.log('all tests passed :-)');
cursor.reset();
}
});
/*provider.on('console', function(line) {
//console.log(line);
});*/
/*gprom.finally(function(ph) {
ph.exit();
// exit with a status code that actually gives information
if (program.exitWithFailureCount) process.exit(failure_count);
});*/
var Browser = require('zombie');
var path = require('path');
var EventEmitter = require('events').EventEmitter;
var Q = require('q');
var provide_emitter = function(file_paths) {
var emitter = new EventEmitter();
file_paths.reduce(function(prom, file_path, path_ind) {
return prom.then(function(browser) {
browser.visit('file://'+file_path, function() {
if (browser.error) throw new Error(browser.errors);
var res_json = {};
res_json.file_ind = path_ind;
res_json.num_tests = browser.querySelectorAll('li.test').length;
res_json.num_fails = browser.querySelectorAll('li.test.fail').length;
res_json.num_passes = browser.querySelectorAll('li.test.pass').length;
res_json.num_slow = browser.querySelectorAll('li.test.pass:not(.fast)').length;
res_json.duration = browser.text('li.duration em');
var traverse_node = function(elem) {
var classList = elem.className.split(' ');
if (classList.indexOf('suite') > -1) {
var res = {
type: 'suite',
name: elem.querySelector('h1').textContent,
has_subfailures: elem.querySelectorAll('li.test.fail').length > 0
}
var child_elems = elem.querySelector('ul').children;
res.children = Array.prototype.map.call(child_elems, traverse_node);
return res;
}
else {
var h2_content = elem.querySelector('h2').childNodes;
var res = {
type: 'test',
text: h2_content[0].textContent
}
if (classList.indexOf('pass') > -1) {
res.pass = true;
res.slow = classList.indexOf('fast') < 0;
res.duration = h2_content[1].textContent;
}
else {
res.error = elem.querySelector('pre.error').textContent;
}
return res;
}
}
var top_suites = browser.querySelectorAll('#mocha-report > li.suite');
res_json.suites = Array.prototype.map.call(top_suites, traverse_node);
res_json.replay = browser.querySelector('a.replay').textContent;
emitter.emit('test_ready', res_json);
});
return new Browser();
});
}, Q(new Browser()));
return emitter;
}
module.exports = {
provide_emitter: provide_emitter,
name: 'ZombieJS'
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment