UNPKG

6.21 kBJavaScriptView Raw
1/*
2 * grunt
3 * https://github.com/cowboy/grunt
4 *
5 * Copyright (c) 2012 "Cowboy" Ben Alman
6 * Licensed under the MIT license.
7 * http://benalman.com/about/license/
8 */
9
10var fs = require('fs');
11var path = require('path');
12
13var hooker = require('hooker');
14var connect = require('connect');
15var HTTP = require('http');
16
17// Temporary file to be used for HTTP request socket.
18var Tempfile = require('temporary/lib/file');
19var tempfile;
20
21// Keep track of the last-started module, test and status.
22var currentModule, currentTest, status;
23// Keep track of the last-started test(s).
24var unfinished = {};
25
26// Keep track of failed assertions for pretty-printing.
27var failedAssertions = [];
28function logFailedAssertions() {
29 var assertion;
30 // Print each assertion error.
31 while (assertion = failedAssertions.shift()) {
32 verbose.or.error(assertion.testName);
33 log.error('Message: ' + String(assertion.message).magenta);
34 if (assertion.actual !== assertion.expected) {
35 log.error('Actual: ' + String(assertion.actual).magenta);
36 log.error('Expected: ' + String(assertion.expected).magenta);
37 }
38 log.error(assertion.source.replace(/ {4}(at)/g, ' $1'));
39 log.writeln();
40 }
41}
42
43// QUnit hooks.
44var qunit = {
45 moduleStart: function(name) {
46 unfinished[name] = true;
47 currentModule = name;
48 },
49 moduleDone: function(name, failed, passed, total) {
50 delete unfinished[name];
51 },
52 log: function(result, actual, expected, message, source) {
53 if (!result) {
54 failedAssertions.push({
55 actual: actual, expected: expected, message: message, source: source,
56 testName: currentTest
57 });
58 }
59 },
60 testStart: function(name) {
61 currentTest = (currentModule ? currentModule + ' - ' : '') + name;
62 verbose.write(currentTest + '...');
63 },
64 testDone: function(name, failed, passed, total) {
65 // Log errors if necessary, otherwise success.
66 if (failed > 0) {
67 // list assertions
68 if (option('verbose')) {
69 log.error();
70 logFailedAssertions();
71 } else {
72 log.write('F'.red);
73 }
74 } else {
75 verbose.ok().or.write('.');
76 }
77 },
78 done: function(failed, passed, total, duration) {
79 status.failed += failed;
80 status.passed += passed;
81 status.total += total;
82 status.duration += duration;
83 // Print assertion errors here, if verbose mode is disabled.
84 if (!option('verbose')) {
85 if (failed > 0) {
86 log.writeln();
87 logFailedAssertions();
88 } else {
89 log.ok();
90 }
91 }
92 }
93};
94
95// ============================================================================
96// TASKS
97// ============================================================================
98
99task.registerBasicTask('qunit', 'Run qunit tests in a headless browser.', function(data, name) {
100 // File paths.
101 var filepaths = file.expand(data);
102
103 // This task is asynchronous.
104 var done = this.async();
105
106 // Create socket tempfile.
107 tempfile = new Tempfile();
108
109 // Hook HTTP.request to use socket file for http://grunt/* requests.
110 hooker.hook(HTTP, 'request', function(options) {
111 if (options.host === 'grunt') {
112 options.socketPath = tempfile.path;
113 }
114 });
115
116 // Start static file server.
117 var server = connect(connect.static(process.cwd())).listen(tempfile.path);
118
119 // Reset status.
120 status = {failed: 0, passed: 0, total: 0, duration: 0};
121
122 // Load zombie and patch jsdom.
123 var Browser = require('zombie');
124
125 // Add a custom "text/grunt" type to zombie's jsdom for custom grunt-only scripts.
126 var jsdom = require('zombie/node_modules/jsdom/lib/jsdom');
127 var lang = jsdom.dom.level3.html.languageProcessors;
128
129 // Override the built-in JavaScript handler.
130 hooker.hook(lang, 'javascript', function(element, code, filename) {
131 // Piggy-back custom QUnit grunt reporter code onto request for qunit.js.
132 if (path.basename(filename) === 'qunit.js') {
133 code += fs.readFileSync(file.taskfile('qunit/qunit.js'), 'utf-8');
134 return hooker.filter(this, [element, code, filename]);
135 }
136 });
137
138 // When run from within this task, scripts specified as "text/grunt" will be
139 // run like JavaScript!
140 lang.grunt = lang.javascript;
141
142 // Process each filepath in-order.
143 async.forEachSeries(filepaths, function(filepath, next) {
144 var basename = path.basename(filepath);
145 verbose.subhead('Testing ' + basename).or.write('Testing ' + basename);
146
147 // Reset current module.
148 currentModule = null;
149
150 // Create a new browser.
151 var browser = new Browser({debug: false, silent: false});
152
153 // Messages are recieved from QUnit via alert!
154 browser.onalert(function(message) {
155 var args = JSON.parse(message);
156 var method = args.shift();
157 if (qunit[method]) {
158 qunit[method].apply(null, args);
159 }
160 if (method === 'done') {
161 next();
162 }
163 });
164
165 // For some reason, the browser.wait callback fires well before all the
166 // events have completed, for example, when there are heavily async tests
167 // running for more than a few seconds. This "fix" simply re-queues the
168 // browser.wait about a zillion times, which seems to work.
169 //
170 // This may be related to issue https://github.com/joyent/node/issues/2515
171 (function loopy(i) {
172 browser.wait(10000, function() {
173 if (i === 0) {
174 // Simulate window.load event.
175 // https://github.com/assaf/zombie/issues/172
176 browser.fire('load', browser.window);
177 }
178 if (i < 10000) {
179 loopy(i + 1);
180 }
181 });
182 }(0));
183
184 // Actually load test page.
185 browser.window.location = 'http://grunt/' + filepath;
186 }, function(err) {
187 // All tests have been run.
188
189 // Log results.
190 if (status.failed > 0) {
191 fail.warn(status.failed + '/' + status.total + ' assertions failed (' +
192 status.duration + 'ms)', Math.min(99, 90 + status.failed));
193 } else {
194 verbose.writeln();
195 log.ok(status.total + ' assertions passed (' + status.duration + 'ms)');
196 }
197
198 // Clean up.
199 hooker.unhook(HTTP, 'request');
200 server.close();
201 tempfile.unlink();
202 hooker.unhook(lang, 'javascript');
203 delete lang.grunt;
204
205 // All done!
206 done();
207 });
208});