1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | 'use strict';
|
10 |
|
11 | module.exports = function(grunt) {
|
12 |
|
13 |
|
14 | var fs = require('fs'),
|
15 | path = require('path');
|
16 |
|
17 |
|
18 | var puppeteer = require('puppeteer'),
|
19 | chalk = require('chalk'),
|
20 | _ = require('lodash');
|
21 |
|
22 |
|
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 |
|
53 |
|
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 |
|
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 |
|
101 | if (!jasmine.buildSpecrunner(this.filesSrc, options)) {
|
102 | return;
|
103 | }
|
104 |
|
105 |
|
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 |
|
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 |
|
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 |
|
291 | if (thisRun.cleanConsole) {
|
292 |
|
293 | if (options.display !== 'none') {
|
294 | grunt.log.writeln('...' + symbols[options.display][symbol]);
|
295 | }
|
296 | } else {
|
297 |
|
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 |
|
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 | };
|