2 * QtWebKit-powered headless test runner using PhantomJS
4 * PhantomJS binaries: http://phantomjs.org/download.html
5 * Requires PhantomJS 1.6+ (1.7+ recommended)
8 * phantomjs runner.js [url-of-your-qunit-testsuite]
11 * phantomjs runner.js http://localhost/qunit/test/index.html
14 /*global phantom:false, require:false, console:false, window:false, QUnit:false */
19 var url, page, timeout,
20 args = require('system').args;
22 // arg[0]: scriptName, args[1...]: arguments
23 if (args.length < 2 || args.length > 3) {
24 console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]');
29 page = require('webpage').create();
30 if (args[2] !== undefined) {
31 timeout = parseInt(args[2], 10);
34 // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`)
35 page.onConsoleMessage = function(msg) {
39 page.onInitialized = function() {
40 page.evaluate(addLogging);
43 page.onCallback = function(message) {
48 if (message.name === 'QUnit.done') {
49 result = message.data;
50 failed = !result || !result.total || result.failed;
53 console.error('No tests were executed. Are you loading tests asynchronously?');
56 // Work-around to avoid "Unsafe JavaScript attempt to access frame" warning in PhantomJS 1.9.8.
57 // See: https://github.com/ariya/phantomjs/issues/12697
59 setTimeout(function () { phantom.exit(failed ? 1 : 0) }, 0);
61 else if (message.name == 'Blanket.done') {
62 console.log('Saving coverage data to data.lcov.');
64 var fs = require('fs');
65 var f = fs.open('data.lcov', 'w');
66 f.write(message.data);
73 page.open(url, function(status) {
74 if (status !== 'success') {
75 console.error('Unable to access network: ' + status);
78 // Cannot do this verification with the 'DOMContentLoaded' handler because it
79 // will be too late to attach it if a page does not have any script tags.
80 var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); });
82 console.error('The `QUnit` object is not present on this page.');
86 // Set a timeout on the test running, otherwise tests with async problems will hang forever
87 if (typeof timeout === 'number') {
88 setTimeout(function() {
89 console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...');
94 // Do nothing... the callback mechanism will handle everything!
98 function addLogging() {
99 window.document.addEventListener('DOMContentLoaded', function() {
100 var currentTestAssertions = [];
102 QUnit.log(function(details) {
105 // Ignore passing assertions
106 if (details.result) {
110 response = details.message || '';
112 if (typeof details.expected !== 'undefined') {
117 response += 'expected: ' + details.expected + ', but was: ' + details.actual;
120 if (details.source) {
121 response += "\n" + details.source;
124 currentTestAssertions.push('Failed assertion: ' + response);
127 QUnit.testDone(function(result) {
130 name = result.module + ': ' + result.name;
133 console.log('Test failed: ' + name);
135 for (i = 0, len = currentTestAssertions.length; i < len; i++) {
136 console.log(' ' + currentTestAssertions[i]);
140 currentTestAssertions.length = 0;
143 QUnit.done(function(result) {
144 console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.');
146 if (typeof window.callPhantom === 'function') {
148 'name': 'QUnit.done',
160 for (var i=0; i<arguments.length; i++) {
161 result += arguments[i];
167 for (var filename in coverage.files) {
168 var data = coverage.files[filename];
170 addLine('SF:', filename.replace('http://localhost:8000/', ''));
172 data.source.forEach(function(line, num) {
175 if (data[num] !== undefined) {
176 addLine('DA:', num, ',', data[num]);
180 addLine('end_of_record');
183 if (typeof window.callPhantom === 'function') {
185 'name': 'Blanket.done',