UNPKG

8.67 kBJavaScriptView Raw
1/*
2 * grunt
3 * http://gruntjs.com/
4 *
5 * Copyright (c) 2012 "Cowboy" Ben Alman
6 * Licensed under the MIT license.
7 * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
8 */
9
10module.exports = function(grunt) {
11
12 // Nodejs libs.
13 var fs = require('fs');
14 var path = require('path');
15
16 // External libs.
17 var Tempfile = require('temporary/lib/file');
18
19 // Keep track of the last-started module, test and status.
20 var currentModule, currentTest, status;
21 // Keep track of the last-started test(s).
22 var unfinished = {};
23
24 // Allow an error message to retain its color when split across multiple lines.
25 function formatMessage(str) {
26 return String(str).split('\n').map(function(s) { return s.magenta; }).join('\n');
27 }
28
29 // Keep track of failed assertions for pretty-printing.
30 var failedAssertions = [];
31 function logFailedAssertions() {
32 var assertion;
33 // Print each assertion error.
34 while (assertion = failedAssertions.shift()) {
35 grunt.verbose.or.error(assertion.testName);
36 grunt.log.error('Message: ' + formatMessage(assertion.message));
37 if (assertion.actual !== assertion.expected) {
38 grunt.log.error('Actual: ' + formatMessage(assertion.actual));
39 grunt.log.error('Expected: ' + formatMessage(assertion.expected));
40 }
41 if (assertion.source) {
42 grunt.log.error(assertion.source.replace(/ {4}(at)/g, ' $1'));
43 }
44 grunt.log.writeln();
45 }
46 }
47
48 // Handle methods passed from PhantomJS, including QUnit hooks.
49 var phantomHandlers = {
50 // QUnit hooks.
51 moduleStart: function(name) {
52 unfinished[name] = true;
53 currentModule = name;
54 },
55 moduleDone: function(name, failed, passed, total) {
56 delete unfinished[name];
57 },
58 log: function(result, actual, expected, message, source) {
59 if (!result) {
60 failedAssertions.push({
61 actual: actual, expected: expected, message: message, source: source,
62 testName: currentTest
63 });
64 }
65 },
66 testStart: function(name) {
67 currentTest = (currentModule ? currentModule + ' - ' : '') + name;
68 grunt.verbose.write(currentTest + '...');
69 },
70 testDone: function(name, failed, passed, total) {
71 // Log errors if necessary, otherwise success.
72 if (failed > 0) {
73 // list assertions
74 if (grunt.option('verbose')) {
75 grunt.log.error();
76 logFailedAssertions();
77 } else {
78 grunt.log.write('F'.red);
79 }
80 } else {
81 grunt.verbose.ok().or.write('.');
82 }
83 },
84 done: function(failed, passed, total, duration) {
85 status.failed += failed;
86 status.passed += passed;
87 status.total += total;
88 status.duration += duration;
89 // Print assertion errors here, if verbose mode is disabled.
90 if (!grunt.option('verbose')) {
91 if (failed > 0) {
92 grunt.log.writeln();
93 logFailedAssertions();
94 } else {
95 grunt.log.ok();
96 }
97 }
98 },
99 // Error handlers.
100 done_fail: function(url) {
101 grunt.verbose.write('Running PhantomJS...').or.write('...');
102 grunt.log.error();
103 grunt.warn('PhantomJS unable to load "' + url + '" URI.', 90);
104 },
105 done_timeout: function() {
106 grunt.log.writeln();
107 grunt.warn('PhantomJS timed out, possibly due to a missing QUnit start() call.', 90);
108 },
109 // console.log pass-through.
110 console: console.log.bind(console),
111 // Debugging messages.
112 debug: grunt.log.debug.bind(grunt.log, 'phantomjs')
113 };
114
115 // ==========================================================================
116 // TASKS
117 // ==========================================================================
118
119 grunt.registerMultiTask('qunit', 'Run QUnit unit tests in a headless PhantomJS instance.', function() {
120 // Get files as URLs.
121 var urls = grunt.file.expandFileURLs(this.file.src);
122
123 // This task is asynchronous.
124 var done = this.async();
125
126 // Reset status.
127 status = {failed: 0, passed: 0, total: 0, duration: 0};
128
129 // Process each filepath in-order.
130 grunt.utils.async.forEachSeries(urls, function(url, next) {
131 var basename = path.basename(url);
132 grunt.verbose.subhead('Testing ' + basename).or.write('Testing ' + basename);
133
134 // Create temporary file to be used for grunt-phantom communication.
135 var tempfile = new Tempfile();
136 // Timeout ID.
137 var id;
138 // The number of tempfile lines already read.
139 var n = 0;
140
141 // Reset current module.
142 currentModule = null;
143
144 // Clean up.
145 function cleanup() {
146 clearTimeout(id);
147 tempfile.unlink();
148 }
149
150 // It's simple. As QUnit tests, assertions and modules begin and complete,
151 // the results are written as JSON to a temporary file. This polling loop
152 // checks that file for new lines, and for each one parses its JSON and
153 // executes the corresponding method with the specified arguments.
154 (function loopy() {
155 // Disable logging temporarily.
156 grunt.log.muted = true;
157 // Read the file, splitting lines on \n, and removing a trailing line.
158 var lines = grunt.file.read(tempfile.path).split('\n').slice(0, -1);
159 // Re-enable logging.
160 grunt.log.muted = false;
161 // Iterate over all lines that haven't already been processed.
162 var done = lines.slice(n).some(function(line) {
163 // Get args and method.
164 var args = JSON.parse(line);
165 var method = args.shift();
166 // Execute method if it exists.
167 if (phantomHandlers[method]) {
168 phantomHandlers[method].apply(null, args);
169 }
170 // If the method name started with test, return true. Because the
171 // Array#some method was used, this not only sets "done" to true,
172 // but stops further iteration from occurring.
173 return (/^done/).test(method);
174 });
175
176 if (done) {
177 // All done.
178 cleanup();
179 next();
180 } else {
181 // Update n so previously processed lines are ignored.
182 n = lines.length;
183 // Check back in a little bit.
184 id = setTimeout(loopy, 100);
185 }
186 }());
187
188 // Launch PhantomJS.
189 grunt.helper('phantomjs', {
190 code: 90,
191 args: [
192 // PhantomJS options.
193 '--config=' + grunt.task.getFile('qunit/phantom.json'),
194 // The main script file.
195 grunt.task.getFile('qunit/phantom.js'),
196 // The temporary file used for communications.
197 tempfile.path,
198 // The QUnit helper file to be injected.
199 grunt.task.getFile('qunit/qunit.js'),
200 // URL to the QUnit .html test file to run.
201 url
202 ],
203 done: function(err) {
204 if (err) {
205 cleanup();
206 done();
207 }
208 },
209 });
210 }, function(err) {
211 // All tests have been run.
212
213 // Log results.
214 if (status.failed > 0) {
215 grunt.warn(status.failed + '/' + status.total + ' assertions failed (' +
216 status.duration + 'ms)', Math.min(99, 90 + status.failed));
217 } else {
218 grunt.verbose.writeln();
219 grunt.log.ok(status.total + ' assertions passed (' + status.duration + 'ms)');
220 }
221
222 // All done!
223 done();
224 });
225 });
226
227 // ==========================================================================
228 // HELPERS
229 // ==========================================================================
230
231 grunt.registerHelper('phantomjs', function(options) {
232 return grunt.utils.spawn({
233 cmd: 'phantomjs',
234 args: options.args
235 }, function(err, result, code) {
236 if (!err) { return options.done(null); }
237 // Something went horribly wrong.
238 grunt.verbose.or.writeln();
239 grunt.log.write('Running PhantomJS...').error();
240 if (code === 127) {
241 grunt.log.errorlns(
242 'In order for this task to work properly, PhantomJS must be ' +
243 'installed and in the system PATH (if you can run "phantomjs" at' +
244 ' the command line, this task should work). Unfortunately, ' +
245 'PhantomJS cannot be installed automatically via npm or grunt. ' +
246 'See the grunt FAQ for PhantomJS installation instructions: ' +
247 'https://github.com/gruntjs/grunt/blob/master/docs/faq.md'
248 );
249 grunt.warn('PhantomJS not found.', options.code);
250 } else {
251 result.split('\n').forEach(grunt.log.error, grunt.log);
252 grunt.warn('PhantomJS exited unexpectedly with exit code ' + code + '.', options.code);
253 }
254 options.done(code);
255 });
256 });
257
258};