UNPKG

11.4 kBJavaScriptView Raw
1/*
2 * grunt-contrib-jasmine
3 * http://gruntjs.com/
4 *
5 * Copyright (c) 2016 GruntJS Team
6 * Licensed under the MIT license.
7 */
8
9'use strict';
10
11module.exports = function(grunt) {
12
13 // node api
14 var fs = require('fs'),
15 path = require('path');
16
17 // npm lib
18 var puppeteer = require('puppeteer'),
19 chalk = require('chalk'),
20 _ = require('lodash');
21
22 // local lib
23 var jasmine = require('./lib/jasmine').init(grunt);
24
25 var junitTemplate = path.join(__dirname, '/jasmine/templates/JUnit.tmpl');
26
27 var status = {};
28
29 let resolveJasmine;
30 const jasminePromise = new Promise((resolve) => {
31 resolveJasmine = resolve;
32 });
33
34 var symbols = {
35 none: {
36 check: '',
37 error: '',
38 splat: ''
39 },
40 short: {
41 check: '.',
42 error: 'X',
43 splat: '*'
44 },
45 full: {
46 check: '✓',
47 error: 'X',
48 splat: '*'
49 }
50 };
51
52 // With node.js on Windows: use symbols available in terminal default fonts
53 // https://github.com/visionmedia/mocha/pull/641
54 if (process && process.platform === 'win32') {
55 symbols = {
56 none: {
57 check: '',
58 error: '',
59 splat: ''
60 },
61 short: {
62 check: '.',
63 error: '\u00D7',
64 splat: '*'
65 },
66 full: {
67 check: '\u221A',
68 error: '\u00D7',
69 splat: '*'
70 }
71 };
72 }
73
74 grunt.registerMultiTask('jasmine', 'Run Jasmine specs headlessly.', async function() {
75 // Merge task-specific options with these defaults.
76 var options = this.options({
77 version: '2.2.0',
78 timeout: 10000,
79 styles: [],
80 specs: [],
81 helpers: [],
82 vendor: [],
83 polyfills: [],
84 customBootFile: null,
85 tempDir: '.grunt/grunt-contrib-jasmine',
86 outfile: '_SpecRunner.html',
87 host: '',
88 template: path.join(__dirname, '/jasmine/templates/DefaultRunner.tmpl'),
89 templateOptions: {},
90 junit: {},
91 ignoreEmpty: grunt.option('force') === true,
92 display: 'full',
93 summary: false
94 });
95
96 if (grunt.option('debug')) {
97 grunt.log.debug(options);
98 }
99
100 // The filter returned no spec files so skip headless.
101 if (!jasmine.buildSpecrunner(this.filesSrc, options)) {
102 return;
103 }
104
105 // If we're just building (e.g. for web), skip headless.
106 if (this.flags.build) {
107 return;
108 }
109
110 var done = this.async();
111 const err = await launchPuppeteer(options);
112 var success = !err && status.failed === 0;
113
114 if (err) {
115 grunt.log.error(err);
116 }
117 if (status.failed === 0) {
118 grunt.log.ok('0 failures');
119 } else {
120 grunt.log.error(status.failed + ' failures');
121 }
122
123 teardown(options, function() {
124 done(success);
125 });
126 });
127
128 async function launchPuppeteer(options) {
129 var file = options.outfile;
130
131 if (options.host) {
132 if (!(/\/$/).test(options.host)) {
133 options.host += '/';
134 }
135 file = options.host + options.outfile;
136 } else {
137 file = `file://${path.join(__dirname, '..', file)}`;
138 }
139
140 grunt.log.subhead('Testing Jasmine specs via Headless Chrome');
141 const browser = await puppeteer.launch();
142 const page = await browser.newPage();
143
144 try {
145 await setup(options, page);
146 await page.goto(file, { waitUntil: 'domcontentloaded' });
147
148 await jasminePromise;
149 } catch (error) {
150 grunt.log.error('Error caught from Puppeteer');
151 grunt.warn(error.stack);
152 }
153
154 await page.close();
155 await browser.close();
156
157 return;
158 }
159
160 function teardown(options, cb) {
161 if (!options.keepRunner && fs.statSync(options.outfile).isFile()) {
162 fs.unlinkSync(options.outfile);
163 }
164
165 if (!options.keepRunner) {
166 jasmine.cleanTemp(options.tempDir, cb);
167 } else {
168 cb();
169 }
170 }
171
172 async function setup(options, page) {
173 var indentLevel = 1,
174 tabstop = 2,
175 thisRun = {},
176 suites = {},
177 currentSuite;
178
179 status = {
180 failed: 0
181 };
182
183 function indent(times) {
184 return new Array(+times * tabstop).join(' ');
185 }
186
187 page.on('error', (error) => {
188 // page has crashed
189 grunt.log.error('Error caught from Headless Chrome. More info can be found by opening the Spec Runner in a browser.');
190 grunt.log.warn(error.stack);
191 });
192
193 await page.exposeFunction('jasmine.jasmineStarted', function() {
194 grunt.verbose.writeln('Jasmine Runner Starting...');
195 thisRun.startTime = (new Date()).getTime();
196 thisRun.executedSpecs = 0;
197 thisRun.passedSpecs = 0;
198 thisRun.failedSpecs = 0;
199 thisRun.skippedSpecs = 0;
200 thisRun.summary = [];
201 });
202
203 await page.exposeFunction('jasmine.suiteStarted', function suiteStarted(suiteMetadata) {
204 grunt.verbose.writeln('jasmine.suiteStarted');
205 currentSuite = suiteMetadata.id;
206 suites[currentSuite] = {
207 name: suiteMetadata.fullName,
208 timestamp: new Date(suiteMetadata.startTime),
209 errors: 0,
210 tests: 0,
211 failures: 0,
212 testcases: []
213 };
214 if (options.display === 'full') {
215 grunt.log.write(indent(indentLevel++));
216 grunt.log.writeln(chalk.bold(suiteMetadata.description));
217 }
218 });
219
220 await page.exposeFunction('jasmine.specStarted', function(specMetaData) {
221 grunt.verbose.writeln('jasmine.specStarted');
222 thisRun.executedSpecs++;
223 thisRun.cleanConsole = true;
224 if (options.display === 'full') {
225 grunt.log.write(indent(indentLevel) + '- ' + chalk.grey(specMetaData.description) + '...');
226 } else if (options.display === 'short') {
227 grunt.log.write(chalk.grey('.'));
228 }
229 });
230
231 await page.exposeFunction('jasmine.specDone', function(specMetaData) {
232 grunt.verbose.writeln('jasmine.specDone');
233 var specSummary = {
234 assertions: 0,
235 classname: suites[currentSuite].name,
236 name: specMetaData.description,
237 time: specMetaData.duration / 1000,
238 failureMessages: []
239 };
240
241 suites[currentSuite].tests++;
242
243 var color = 'yellow',
244 symbol = 'splat';
245 if (specMetaData.status === 'passed') {
246 thisRun.passedSpecs++;
247 color = 'green';
248 symbol = 'check';
249 } else if (specMetaData.status === 'failed') {
250 thisRun.failedSpecs++;
251 status.failed++;
252 color = 'red';
253 symbol = 'error';
254 suites[currentSuite].failures++;
255 suites[currentSuite].errors += specMetaData.failedExpectations.length;
256 specSummary.failureMessages = specMetaData.failedExpectations.map(function(error) {
257 return error.message;
258 });
259 thisRun.summary.push({
260 suite: suites[currentSuite].name,
261 name: specMetaData.description,
262 errors: specMetaData.failedExpectations.map(function(error) {
263 return {
264 message: error.message,
265 stack: error.stack
266 };
267 })
268 });
269 } else {
270 thisRun.skippedSpecs++;
271 }
272
273 suites[currentSuite].testcases.push(specSummary);
274
275 // If we're writing to a proper terminal, make it fancy.
276 if (process.stdout.clearLine) {
277 if (options.display === 'full') {
278 process.stdout.clearLine();
279 process.stdout.cursorTo(0);
280 grunt.log.writeln(
281 indent(indentLevel) +
282 chalk[color].bold(symbols.full[symbol]) + ' ' +
283 chalk.grey(specMetaData.description)
284 );
285 } else if (options.display === 'short') {
286 process.stdout.moveCursor(-1);
287 grunt.log.write(chalk[color].bold(symbols.short[symbol]));
288 }
289 } else {
290 // If we haven't written out since we've started
291 if (thisRun.cleanConsole) {
292 // then append to the current line.
293 if (options.display !== 'none') {
294 grunt.log.writeln('...' + symbols[options.display][symbol]);
295 }
296 } else {
297 // Otherwise reprint the current spec and status.
298 if (options.display !== 'none') {
299 grunt.log.writeln(
300 indent(indentLevel) + '...' +
301 chalk.grey(specMetaData.description) + '...' +
302 symbols[options.display][symbol]
303 );
304 }
305 }
306 }
307
308 specMetaData.failedExpectations.forEach(function(error, i) {
309 var specIndex = ' (' + (i + 1) + ')';
310 if (options.display === 'full') {
311 grunt.log.writeln(indent(indentLevel + 1) + chalk.red(error.message + specIndex));
312 }
313 grunt.log.error(error.message, error.stack);
314 });
315
316 });
317
318 await page.exposeFunction('jasmine.suiteDone', function suiteDone(suiteMetadata) {
319 grunt.verbose.writeln('jasmine.suiteDone');
320 suites[suiteMetadata.id].time = suiteMetadata.duration / 1000;
321
322 if (indentLevel > 1) {
323 indentLevel--;
324 }
325 });
326
327 await page.exposeFunction('jasmine.jasmineDone', function() {
328 grunt.verbose.writeln('jasmine.jasmineDone');
329 var dur = (new Date()).getTime() - thisRun.startTime;
330 var specQuantity = thisRun.executedSpecs + (thisRun.executedSpecs === 1 ? ' spec ' : ' specs ');
331
332 grunt.verbose.writeln('Jasmine runner finished');
333
334 if (thisRun.executedSpecs === 0) {
335 // log.error will print the message but not fail the task, warn will do both.
336 var log = options.ignoreEmpty ? grunt.log.error : grunt.warn;
337
338 log('No specs executed, is there a configuration error?');
339 }
340
341 if (options.display === 'short') {
342 grunt.log.writeln();
343 }
344
345 if (options.summary && thisRun.summary.length) {
346 grunt.log.writeln();
347 logSummary(thisRun.summary);
348 }
349
350 if (options.junit && options.junit.path) {
351 writeJunitXml(suites);
352 }
353
354 grunt.log.writeln('\n' + specQuantity + 'in ' + (dur / 1000) + 's.');
355
356 resolveJasmine();
357 });
358
359 await page.exposeFunction('jasmine.done_fail', function(url) {
360 grunt.log.error();
361 grunt.warn('Unable to load "' + url + '" URI.', 90);
362
363 resolveJasmine();
364 });
365
366 function logSummary(tests) {
367 grunt.log.writeln('Summary (' + tests.length + ' tests failed)');
368 _.forEach(tests, function(test) {
369 grunt.log.writeln(chalk.red(symbols[options.display].error) + ' ' + test.suite + ' ' + test.name);
370 _.forEach(test.errors, function(error) {
371 grunt.log.writeln(indent(2) + chalk.red(error.message));
372 logStack(error.stack, 2);
373 });
374 });
375 }
376
377 function logStack(stack, indentLevel) {
378 var lines = (stack || '').split('\n');
379 for (var i = 0; i < lines.length && i < 11; i++) {
380 grunt.log.writeln(indent(indentLevel) + lines[i]);
381 }
382 }
383
384 function writeJunitXml(testsuites) {
385 var template = grunt.file.read(options.junit.template || junitTemplate);
386 if (options.junit.consolidate) {
387 var xmlFile = path.join(options.junit.path, 'TEST-' + testsuites.suite1.name.replace(/[^\w]/g, '') + '.xml');
388 grunt.file.write(xmlFile, _.template(template, { testsuites: _.values(testsuites) }));
389 } else {
390 _.forEach(testsuites, function(suiteData) {
391 var xmlFile = path.join(options.junit.path, 'TEST-' + suiteData.name.replace(/[^\w]/g, '') + '.xml');
392 grunt.file.write(xmlFile, _.template(template, { testsuites: [suiteData] }));
393 });
394 }
395 }
396 }
397};