Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80680 views
1
/*
2
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
3
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4
*/
5
6
/*jshint maxlen: 300 */
7
var handlebars = require('handlebars'),
8
defaults = require('./common/defaults'),
9
path = require('path'),
10
fs = require('fs'),
11
util = require('util'),
12
FileWriter = require('../util/file-writer'),
13
Report = require('./index'),
14
Store = require('../store'),
15
InsertionText = require('../util/insertion-text'),
16
TreeSummarizer = require('../util/tree-summarizer'),
17
utils = require('../object-utils'),
18
templateFor = function (name) { return handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'templates', name + '.txt'), 'utf8')); },
19
headerTemplate = templateFor('head'),
20
footerTemplate = templateFor('foot'),
21
pathTemplate = handlebars.compile('<div class="path">{{{html}}}</div>'),
22
detailTemplate = handlebars.compile([
23
'<tr>',
24
'<td class="line-count">{{#show_lines}}{{maxLines}}{{/show_lines}}</td>',
25
'<td class="line-coverage">{{#show_line_execution_counts fileCoverage}}{{maxLines}}{{/show_line_execution_counts}}</td>',
26
'<td class="text"><pre class="prettyprint lang-js">{{#show_code structured}}{{/show_code}}</pre></td>',
27
'</tr>\n'
28
].join('')),
29
summaryTableHeader = [
30
'<div class="coverage-summary">',
31
'<table>',
32
'<thead>',
33
'<tr>',
34
' <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>',
35
' <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>',
36
' <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>',
37
' <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>',
38
' <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>',
39
' <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>',
40
' <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>',
41
' <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>',
42
' <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>',
43
' <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>',
44
'</tr>',
45
'</thead>',
46
'<tbody>'
47
].join('\n'),
48
summaryLineTemplate = handlebars.compile([
49
'<tr>',
50
'<td class="file {{reportClasses.statements}}" data-value="{{file}}"><a href="{{output}}">{{file}}</a></td>',
51
'<td data-value="{{metrics.statements.pct}}" class="pic {{reportClasses.statements}}">{{#show_picture}}{{metrics.statements.pct}}{{/show_picture}}</td>',
52
'<td data-value="{{metrics.statements.pct}}" class="pct {{reportClasses.statements}}">{{metrics.statements.pct}}%</td>',
53
'<td data-value="{{metrics.statements.total}}" class="abs {{reportClasses.statements}}">({{metrics.statements.covered}}&nbsp;/&nbsp;{{metrics.statements.total}})</td>',
54
'<td data-value="{{metrics.branches.pct}}" class="pct {{reportClasses.branches}}">{{metrics.branches.pct}}%</td>',
55
'<td data-value="{{metrics.branches.total}}" class="abs {{reportClasses.branches}}">({{metrics.branches.covered}}&nbsp;/&nbsp;{{metrics.branches.total}})</td>',
56
'<td data-value="{{metrics.functions.pct}}" class="pct {{reportClasses.functions}}">{{metrics.functions.pct}}%</td>',
57
'<td data-value="{{metrics.functions.total}}" class="abs {{reportClasses.functions}}">({{metrics.functions.covered}}&nbsp;/&nbsp;{{metrics.functions.total}})</td>',
58
'<td data-value="{{metrics.lines.pct}}" class="pct {{reportClasses.lines}}">{{metrics.lines.pct}}%</td>',
59
'<td data-value="{{metrics.lines.total}}" class="abs {{reportClasses.lines}}">({{metrics.lines.covered}}&nbsp;/&nbsp;{{metrics.lines.total}})</td>',
60
'</tr>\n'
61
].join('\n\t')),
62
summaryTableFooter = [
63
'</tbody>',
64
'</table>',
65
'</div>'
66
].join('\n'),
67
lt = '\u0001',
68
gt = '\u0002',
69
RE_LT = /</g,
70
RE_GT = />/g,
71
RE_AMP = /&/g,
72
RE_lt = /\u0001/g,
73
RE_gt = /\u0002/g;
74
75
handlebars.registerHelper('show_picture', function (opts) {
76
var num = Number(opts.fn(this)),
77
rest,
78
cls = '';
79
if (isFinite(num)) {
80
if (num === 100) {
81
cls = ' cover-full';
82
}
83
num = Math.floor(num);
84
rest = 100 - num;
85
return '<span class="cover-fill' + cls + '" style="width: ' + num + 'px;"></span>' +
86
'<span class="cover-empty" style="width:' + rest + 'px;"></span>';
87
} else {
88
return '';
89
}
90
});
91
92
handlebars.registerHelper('show_ignores', function (metrics) {
93
var statements = metrics.statements.skipped,
94
functions = metrics.functions.skipped,
95
branches = metrics.branches.skipped,
96
result;
97
98
if (statements === 0 && functions === 0 && branches === 0) {
99
return '<span class="ignore-none">none</span>';
100
}
101
102
result = [];
103
if (statements >0) { result.push(statements === 1 ? '1 statement': statements + ' statements'); }
104
if (functions >0) { result.push(functions === 1 ? '1 function' : functions + ' functions'); }
105
if (branches >0) { result.push(branches === 1 ? '1 branch' : branches + ' branches'); }
106
107
return result.join(', ');
108
});
109
110
handlebars.registerHelper('show_lines', function (opts) {
111
var maxLines = Number(opts.fn(this)),
112
i,
113
array = [];
114
115
for (i = 0; i < maxLines; i += 1) {
116
array[i] = i + 1;
117
}
118
return array.join('\n');
119
});
120
121
handlebars.registerHelper('show_line_execution_counts', function (context, opts) {
122
var lines = context.l,
123
maxLines = Number(opts.fn(this)),
124
i,
125
lineNumber,
126
array = [],
127
covered,
128
value = '';
129
130
for (i = 0; i < maxLines; i += 1) {
131
lineNumber = i + 1;
132
value = '&nbsp;';
133
covered = 'neutral';
134
if (lines.hasOwnProperty(lineNumber)) {
135
if (lines[lineNumber] > 0) {
136
covered = 'yes';
137
value = lines[lineNumber];
138
} else {
139
covered = 'no';
140
}
141
}
142
array.push('<span class="cline-any cline-' + covered + '">' + value + '</span>');
143
}
144
return array.join('\n');
145
});
146
147
function customEscape(text) {
148
text = text.toString();
149
return text.replace(RE_AMP, '&amp;')
150
.replace(RE_LT, '&lt;')
151
.replace(RE_GT, '&gt;')
152
.replace(RE_lt, '<')
153
.replace(RE_gt, '>');
154
}
155
156
handlebars.registerHelper('show_code', function (context /*, opts */) {
157
var array = [];
158
159
context.forEach(function (item) {
160
array.push(customEscape(item.text) || '&nbsp;');
161
});
162
return array.join('\n');
163
});
164
165
function title(str) {
166
return ' title="' + str + '" ';
167
}
168
169
function annotateLines(fileCoverage, structuredText) {
170
var lineStats = fileCoverage.l;
171
if (!lineStats) { return; }
172
Object.keys(lineStats).forEach(function (lineNumber) {
173
var count = lineStats[lineNumber];
174
structuredText[lineNumber].covered = count > 0 ? 'yes' : 'no';
175
});
176
structuredText.forEach(function (item) {
177
if (item.covered === null) {
178
item.covered = 'neutral';
179
}
180
});
181
}
182
183
function annotateStatements(fileCoverage, structuredText) {
184
var statementStats = fileCoverage.s,
185
statementMeta = fileCoverage.statementMap;
186
Object.keys(statementStats).forEach(function (stName) {
187
var count = statementStats[stName],
188
meta = statementMeta[stName],
189
type = count > 0 ? 'yes' : 'no',
190
startCol = meta.start.column,
191
endCol = meta.end.column + 1,
192
startLine = meta.start.line,
193
endLine = meta.end.line,
194
openSpan = lt + 'span class="' + (meta.skip ? 'cstat-skip' : 'cstat-no') + '"' + title('statement not covered') + gt,
195
closeSpan = lt + '/span' + gt,
196
text;
197
198
if (type === 'no') {
199
if (endLine !== startLine) {
200
endLine = startLine;
201
endCol = structuredText[startLine].text.originalLength();
202
}
203
text = structuredText[startLine].text;
204
text.wrap(startCol,
205
openSpan,
206
startLine === endLine ? endCol : text.originalLength(),
207
closeSpan);
208
}
209
});
210
}
211
212
function annotateFunctions(fileCoverage, structuredText) {
213
214
var fnStats = fileCoverage.f,
215
fnMeta = fileCoverage.fnMap;
216
if (!fnStats) { return; }
217
Object.keys(fnStats).forEach(function (fName) {
218
var count = fnStats[fName],
219
meta = fnMeta[fName],
220
type = count > 0 ? 'yes' : 'no',
221
startCol = meta.loc.start.column,
222
endCol = meta.loc.end.column + 1,
223
startLine = meta.loc.start.line,
224
endLine = meta.loc.end.line,
225
openSpan = lt + 'span class="' + (meta.skip ? 'fstat-skip' : 'fstat-no') + '"' + title('function not covered') + gt,
226
closeSpan = lt + '/span' + gt,
227
text;
228
229
if (type === 'no') {
230
if (endLine !== startLine) {
231
endLine = startLine;
232
endCol = structuredText[startLine].text.originalLength();
233
}
234
text = structuredText[startLine].text;
235
text.wrap(startCol,
236
openSpan,
237
startLine === endLine ? endCol : text.originalLength(),
238
closeSpan);
239
}
240
});
241
}
242
243
function annotateBranches(fileCoverage, structuredText) {
244
var branchStats = fileCoverage.b,
245
branchMeta = fileCoverage.branchMap;
246
if (!branchStats) { return; }
247
248
Object.keys(branchStats).forEach(function (branchName) {
249
var branchArray = branchStats[branchName],
250
sumCount = branchArray.reduce(function (p, n) { return p + n; }, 0),
251
metaArray = branchMeta[branchName].locations,
252
i,
253
count,
254
meta,
255
type,
256
startCol,
257
endCol,
258
startLine,
259
endLine,
260
openSpan,
261
closeSpan,
262
text;
263
264
if (sumCount > 0) { //only highlight if partial branches are missing
265
for (i = 0; i < branchArray.length; i += 1) {
266
count = branchArray[i];
267
meta = metaArray[i];
268
type = count > 0 ? 'yes' : 'no';
269
startCol = meta.start.column;
270
endCol = meta.end.column + 1;
271
startLine = meta.start.line;
272
endLine = meta.end.line;
273
openSpan = lt + 'span class="branch-' + i + ' ' + (meta.skip ? 'cbranch-skip' : 'cbranch-no') + '"' + title('branch not covered') + gt;
274
closeSpan = lt + '/span' + gt;
275
276
if (count === 0) { //skip branches taken
277
if (endLine !== startLine) {
278
endLine = startLine;
279
endCol = structuredText[startLine].text.originalLength();
280
}
281
text = structuredText[startLine].text;
282
if (branchMeta[branchName].type === 'if') { // and 'if' is a special case since the else branch might not be visible, being non-existent
283
text.insertAt(startCol, lt + 'span class="' + (meta.skip ? 'skip-if-branch' : 'missing-if-branch') + '"' +
284
title((i === 0 ? 'if' : 'else') + ' path not taken') + gt +
285
(i === 0 ? 'I' : 'E') + lt + '/span' + gt, true, false);
286
} else {
287
text.wrap(startCol,
288
openSpan,
289
startLine === endLine ? endCol : text.originalLength(),
290
closeSpan);
291
}
292
}
293
}
294
}
295
});
296
}
297
298
function getReportClass(stats, watermark) {
299
var coveragePct = stats.pct,
300
identity = 1;
301
if (coveragePct * identity === coveragePct) {
302
return coveragePct >= watermark[1] ? 'high' : coveragePct >= watermark[0] ? 'medium' : 'low';
303
} else {
304
return '';
305
}
306
}
307
308
function cleanPath(name){
309
var SEP = path.sep || '/';
310
return (SEP !== '/') ? name.split(SEP).join('/') : name;
311
}
312
313
/**
314
* a `Report` implementation that produces HTML coverage reports.
315
*
316
* Usage
317
* -----
318
*
319
* var report = require('istanbul').Report.create('html');
320
*
321
*
322
* @class HtmlReport
323
* @extends Report
324
* @module report
325
* @constructor
326
* @param {Object} opts optional
327
* @param {String} [opts.dir] the directory in which to generate reports. Defaults to `./html-report`
328
*/
329
function HtmlReport(opts) {
330
Report.call(this);
331
this.opts = opts || {};
332
this.opts.dir = this.opts.dir || path.resolve(process.cwd(), 'html-report');
333
this.opts.sourceStore = this.opts.sourceStore || Store.create('fslookup');
334
this.opts.linkMapper = this.opts.linkMapper || this.standardLinkMapper();
335
this.opts.writer = this.opts.writer || null;
336
this.opts.templateData = { datetime: Date() };
337
this.opts.watermarks = this.opts.watermarks || defaults.watermarks();
338
}
339
340
HtmlReport.TYPE = 'html';
341
util.inherits(HtmlReport, Report);
342
343
Report.mix(HtmlReport, {
344
345
synopsis: function () {
346
return 'Navigable HTML coverage report for every file and directory';
347
},
348
349
getPathHtml: function (node, linkMapper) {
350
var parent = node.parent,
351
nodePath = [],
352
linkPath = [],
353
i;
354
355
while (parent) {
356
nodePath.push(parent);
357
parent = parent.parent;
358
}
359
360
for (i = 0; i < nodePath.length; i += 1) {
361
linkPath.push('<a href="' + linkMapper.ancestor(node, i + 1) + '">' +
362
(cleanPath(nodePath[i].relativeName) || 'All files') + '</a>');
363
}
364
linkPath.reverse();
365
return linkPath.length > 0 ? linkPath.join(' &#187; ') + ' &#187; ' +
366
cleanPath(node.displayShortName()) : '';
367
},
368
369
fillTemplate: function (node, templateData) {
370
var opts = this.opts,
371
linkMapper = opts.linkMapper;
372
373
templateData.entity = node.name || 'All files';
374
templateData.metrics = node.metrics;
375
templateData.reportClass = getReportClass(node.metrics.statements, opts.watermarks.statements);
376
templateData.pathHtml = pathTemplate({ html: this.getPathHtml(node, linkMapper) });
377
templateData.base = {
378
css: linkMapper.asset(node, 'base.css')
379
};
380
templateData.sorter = {
381
js: linkMapper.asset(node, 'sorter.js'),
382
image: linkMapper.asset(node, 'sort-arrow-sprite.png')
383
};
384
templateData.prettify = {
385
js: linkMapper.asset(node, 'prettify.js'),
386
css: linkMapper.asset(node, 'prettify.css')
387
};
388
},
389
writeDetailPage: function (writer, node, fileCoverage) {
390
var opts = this.opts,
391
sourceStore = opts.sourceStore,
392
templateData = opts.templateData,
393
sourceText = fileCoverage.code && Array.isArray(fileCoverage.code) ?
394
fileCoverage.code.join('\n') + '\n' : sourceStore.get(fileCoverage.path),
395
code = sourceText.split(/(?:\r?\n)|\r/),
396
count = 0,
397
structured = code.map(function (str) { count += 1; return { line: count, covered: null, text: new InsertionText(str, true) }; }),
398
context;
399
400
structured.unshift({ line: 0, covered: null, text: new InsertionText("") });
401
402
this.fillTemplate(node, templateData);
403
writer.write(headerTemplate(templateData));
404
writer.write('<pre><table class="coverage">\n');
405
406
annotateLines(fileCoverage, structured);
407
//note: order is important, since statements typically result in spanning the whole line and doing branches late
408
//causes mismatched tags
409
annotateBranches(fileCoverage, structured);
410
annotateFunctions(fileCoverage, structured);
411
annotateStatements(fileCoverage, structured);
412
413
structured.shift();
414
context = {
415
structured: structured,
416
maxLines: structured.length,
417
fileCoverage: fileCoverage
418
};
419
writer.write(detailTemplate(context));
420
writer.write('</table></pre>\n');
421
writer.write(footerTemplate(templateData));
422
},
423
424
writeIndexPage: function (writer, node) {
425
var linkMapper = this.opts.linkMapper,
426
templateData = this.opts.templateData,
427
children = Array.prototype.slice.apply(node.children),
428
watermarks = this.opts.watermarks;
429
430
children.sort(function (a, b) {
431
return a.name < b.name ? -1 : 1;
432
});
433
434
this.fillTemplate(node, templateData);
435
writer.write(headerTemplate(templateData));
436
writer.write(summaryTableHeader);
437
children.forEach(function (child) {
438
var metrics = child.metrics,
439
reportClasses = {
440
statements: getReportClass(metrics.statements, watermarks.statements),
441
lines: getReportClass(metrics.lines, watermarks.lines),
442
functions: getReportClass(metrics.functions, watermarks.functions),
443
branches: getReportClass(metrics.branches, watermarks.branches)
444
},
445
data = {
446
metrics: metrics,
447
reportClasses: reportClasses,
448
file: cleanPath(child.displayShortName()),
449
output: linkMapper.fromParent(child)
450
};
451
writer.write(summaryLineTemplate(data) + '\n');
452
});
453
writer.write(summaryTableFooter);
454
writer.write(footerTemplate(templateData));
455
},
456
457
writeFiles: function (writer, node, dir, collector) {
458
var that = this,
459
indexFile = path.resolve(dir, 'index.html'),
460
childFile;
461
if (this.opts.verbose) { console.error('Writing ' + indexFile); }
462
writer.writeFile(indexFile, function (contentWriter) {
463
that.writeIndexPage(contentWriter, node);
464
});
465
node.children.forEach(function (child) {
466
if (child.kind === 'dir') {
467
that.writeFiles(writer, child, path.resolve(dir, child.relativeName), collector);
468
} else {
469
childFile = path.resolve(dir, child.relativeName + '.html');
470
if (that.opts.verbose) { console.error('Writing ' + childFile); }
471
writer.writeFile(childFile, function (contentWriter) {
472
that.writeDetailPage(contentWriter, child, collector.fileCoverageFor(child.fullPath()));
473
});
474
}
475
});
476
},
477
478
standardLinkMapper: function () {
479
return {
480
fromParent: function (node) {
481
var relativeName = cleanPath(node.relativeName);
482
483
return node.kind === 'dir' ? relativeName + 'index.html' : relativeName + '.html';
484
},
485
ancestorHref: function (node, num) {
486
var href = '',
487
notDot = function(part) {
488
return part !== '.';
489
},
490
separated,
491
levels,
492
i,
493
j;
494
495
for (i = 0; i < num; i += 1) {
496
separated = cleanPath(node.relativeName).split('/').filter(notDot);
497
levels = separated.length - 1;
498
for (j = 0; j < levels; j += 1) {
499
href += '../';
500
}
501
node = node.parent;
502
}
503
return href;
504
},
505
ancestor: function (node, num) {
506
return this.ancestorHref(node, num) + 'index.html';
507
},
508
asset: function (node, name) {
509
var i = 0,
510
parent = node.parent;
511
while (parent) { i += 1; parent = parent.parent; }
512
return this.ancestorHref(node, i) + name;
513
}
514
};
515
},
516
517
writeReport: function (collector, sync) {
518
var opts = this.opts,
519
dir = opts.dir,
520
summarizer = new TreeSummarizer(),
521
writer = opts.writer || new FileWriter(sync),
522
that = this,
523
tree,
524
copyAssets = function (subdir) {
525
var srcDir = path.resolve(__dirname, '..', 'assets', subdir);
526
fs.readdirSync(srcDir).forEach(function (f) {
527
var resolvedSource = path.resolve(srcDir, f),
528
resolvedDestination = path.resolve(dir, f),
529
stat = fs.statSync(resolvedSource);
530
531
if (stat.isFile()) {
532
if (opts.verbose) {
533
console.log('Write asset: ' + resolvedDestination);
534
}
535
writer.copyFile(resolvedSource, resolvedDestination);
536
}
537
});
538
};
539
540
collector.files().forEach(function (key) {
541
summarizer.addFileCoverageSummary(key, utils.summarizeFileCoverage(collector.fileCoverageFor(key)));
542
});
543
tree = summarizer.getTreeSummary();
544
[ '.', 'vendor'].forEach(function (subdir) {
545
copyAssets(subdir);
546
});
547
writer.on('done', function () { that.emit('done'); });
548
//console.log(JSON.stringify(tree.root, undefined, 4));
549
this.writeFiles(writer, tree.root, dir, collector);
550
writer.done();
551
}
552
});
553
554
module.exports = HtmlReport;
555
556
557