/* Copyright (c) 2012, Yahoo! Inc. All rights reserved. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /*jshint maxlen: 300 */ var handlebars = require('handlebars'), defaults = require('./common/defaults'), path = require('path'), SEP = path.sep || '/', fs = require('fs'), util = require('util'), FileWriter = require('../util/file-writer'), Report = require('./index'), Store = require('../store'), InsertionText = require('../util/insertion-text'), TreeSummarizer = require('../util/tree-summarizer'), utils = require('../object-utils'), templateFor = function (name) { return handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'templates', name + '.txt'), 'utf8')); }, headerTemplate = templateFor('head'), footerTemplate = templateFor('foot'), pathTemplate = handlebars.compile('
{{{html}}}
'), detailTemplate = handlebars.compile([ '', '{{#show_lines}}{{maxLines}}{{/show_lines}}', '{{#show_line_execution_counts fileCoverage}}{{maxLines}}{{/show_line_execution_counts}}', '
{{#show_code structured}}{{/show_code}}
', '\n' ].join('')), summaryTableHeader = [ '
', '', '', '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', '', '' ].join('\n'), summaryLineTemplate = handlebars.compile([ '', '', '', '', '', '', '', '', '', '', '', '\n' ].join('\n\t')), summaryTableFooter = [ '', '
FileStatementsBranchesFunctionsLines
{{file}}{{#show_picture}}{{metrics.statements.pct}}{{/show_picture}}{{metrics.statements.pct}}%({{metrics.statements.covered}} / {{metrics.statements.total}}){{metrics.branches.pct}}%({{metrics.branches.covered}} / {{metrics.branches.total}}){{metrics.functions.pct}}%({{metrics.functions.covered}} / {{metrics.functions.total}}){{metrics.lines.pct}}%({{metrics.lines.covered}} / {{metrics.lines.total}})
', '
' ].join('\n'), lt = '\u0001', gt = '\u0002', RE_LT = //g, RE_AMP = /&/g, RE_lt = /\u0001/g, RE_gt = /\u0002/g; handlebars.registerHelper('show_picture', function (opts) { var num = Number(opts.fn(this)), rest, cls = ''; if (isFinite(num)) { if (num === 100) { cls = ' cover-full'; } num = Math.floor(num); rest = 100 - num; return '' + ''; } else { return ''; } }); handlebars.registerHelper('show_ignores', function (metrics) { var statements = metrics.statements.skipped, functions = metrics.functions.skipped, branches = metrics.branches.skipped, result; if (statements === 0 && functions === 0 && branches === 0) { return 'none'; } result = []; if (statements >0) { result.push(statements === 1 ? '1 statement': statements + ' statements'); } if (functions >0) { result.push(functions === 1 ? '1 function' : functions + ' functions'); } if (branches >0) { result.push(branches === 1 ? '1 branch' : branches + ' branches'); } return result.join(', '); }); handlebars.registerHelper('show_lines', function (opts) { var maxLines = Number(opts.fn(this)), i, array = []; for (i = 0; i < maxLines; i += 1) { array[i] = i + 1; } return array.join('\n'); }); handlebars.registerHelper('show_line_execution_counts', function (context, opts) { var lines = context.l, maxLines = Number(opts.fn(this)), i, lineNumber, array = [], covered, value = ''; for (i = 0; i < maxLines; i += 1) { lineNumber = i + 1; value = ' '; covered = 'neutral'; if (lines.hasOwnProperty(lineNumber)) { if (lines[lineNumber] > 0) { covered = 'yes'; value = lines[lineNumber]; } else { covered = 'no'; } } array.push('' + value + ''); } return array.join('\n'); }); function customEscape(text) { text = text.toString(); return text.replace(RE_AMP, '&') .replace(RE_LT, '<') .replace(RE_GT, '>') .replace(RE_lt, '<') .replace(RE_gt, '>'); } handlebars.registerHelper('show_code', function (context /*, opts */) { var array = []; context.forEach(function (item) { array.push(customEscape(item.text) || ' '); }); return array.join('\n'); }); function title(str) { return ' title="' + str + '" '; } function annotateLines(fileCoverage, structuredText) { var lineStats = fileCoverage.l; if (!lineStats) { return; } Object.keys(lineStats).forEach(function (lineNumber) { var count = lineStats[lineNumber]; structuredText[lineNumber].covered = count > 0 ? 'yes' : 'no'; }); structuredText.forEach(function (item) { if (item.covered === null) { item.covered = 'neutral'; } }); } function annotateStatements(fileCoverage, structuredText) { var statementStats = fileCoverage.s, statementMeta = fileCoverage.statementMap; Object.keys(statementStats).forEach(function (stName) { var count = statementStats[stName], meta = statementMeta[stName], type = count > 0 ? 'yes' : 'no', startCol = meta.start.column, endCol = meta.end.column + 1, startLine = meta.start.line, endLine = meta.end.line, openSpan = lt + 'span class="' + (meta.skip ? 'cstat-skip' : 'cstat-no') + '"' + title('statement not covered') + gt, closeSpan = lt + '/span' + gt, text; if (type === 'no') { if (endLine !== startLine) { endLine = startLine; endCol = structuredText[startLine].text.originalLength(); } text = structuredText[startLine].text; text.wrap(startCol, openSpan, startLine === endLine ? endCol : text.originalLength(), closeSpan); } }); } function annotateFunctions(fileCoverage, structuredText) { var fnStats = fileCoverage.f, fnMeta = fileCoverage.fnMap; if (!fnStats) { return; } Object.keys(fnStats).forEach(function (fName) { var count = fnStats[fName], meta = fnMeta[fName], type = count > 0 ? 'yes' : 'no', startCol = meta.loc.start.column, endCol = meta.loc.end.column + 1, startLine = meta.loc.start.line, endLine = meta.loc.end.line, openSpan = lt + 'span class="' + (meta.skip ? 'fstat-skip' : 'fstat-no') + '"' + title('function not covered') + gt, closeSpan = lt + '/span' + gt, text; if (type === 'no') { if (endLine !== startLine) { endLine = startLine; endCol = structuredText[startLine].text.originalLength(); } text = structuredText[startLine].text; text.wrap(startCol, openSpan, startLine === endLine ? endCol : text.originalLength(), closeSpan); } }); } function annotateBranches(fileCoverage, structuredText) { var branchStats = fileCoverage.b, branchMeta = fileCoverage.branchMap; if (!branchStats) { return; } Object.keys(branchStats).forEach(function (branchName) { var branchArray = branchStats[branchName], sumCount = branchArray.reduce(function (p, n) { return p + n; }, 0), metaArray = branchMeta[branchName].locations, i, count, meta, type, startCol, endCol, startLine, endLine, openSpan, closeSpan, text; if (sumCount > 0) { //only highlight if partial branches are missing for (i = 0; i < branchArray.length; i += 1) { count = branchArray[i]; meta = metaArray[i]; type = count > 0 ? 'yes' : 'no'; startCol = meta.start.column; endCol = meta.end.column + 1; startLine = meta.start.line; endLine = meta.end.line; openSpan = lt + 'span class="branch-' + i + ' ' + (meta.skip ? 'cbranch-skip' : 'cbranch-no') + '"' + title('branch not covered') + gt; closeSpan = lt + '/span' + gt; if (count === 0) { //skip branches taken if (endLine !== startLine) { endLine = startLine; endCol = structuredText[startLine].text.originalLength(); } text = structuredText[startLine].text; if (branchMeta[branchName].type === 'if') { // and 'if' is a special case since the else branch might not be visible, being non-existent text.insertAt(startCol, lt + 'span class="' + (meta.skip ? 'skip-if-branch' : 'missing-if-branch') + '"' + title((i === 0 ? 'if' : 'else') + ' path not taken') + gt + (i === 0 ? 'I' : 'E') + lt + '/span' + gt, true, false); } else { text.wrap(startCol, openSpan, startLine === endLine ? endCol : text.originalLength(), closeSpan); } } } } }); } function getReportClass(stats, watermark) { var coveragePct = stats.pct, identity = 1; if (coveragePct * identity === coveragePct) { return coveragePct >= watermark[1] ? 'high' : coveragePct >= watermark[0] ? 'medium' : 'low'; } else { return ''; } } /** * a `Report` implementation that produces HTML coverage reports. * * Usage * ----- * * var report = require('istanbul').Report.create('html'); * * * @class HtmlReport * @extends Report * @constructor * @param {Object} opts optional * @param {String} [opts.dir] the directory in which to generate reports. Defaults to `./html-report` */ function HtmlReport(opts) { Report.call(this); this.opts = opts || {}; this.opts.dir = this.opts.dir || path.resolve(process.cwd(), 'html-report'); this.opts.sourceStore = this.opts.sourceStore || Store.create('fslookup'); this.opts.linkMapper = this.opts.linkMapper || this.standardLinkMapper(); this.opts.writer = this.opts.writer || null; this.opts.templateData = { datetime: Date() }; this.opts.watermarks = this.opts.watermarks || defaults.watermarks(); } HtmlReport.TYPE = 'html'; util.inherits(HtmlReport, Report); Report.mix(HtmlReport, { getPathHtml: function (node, linkMapper) { var parent = node.parent, nodePath = [], linkPath = [], i; while (parent) { nodePath.push(parent); parent = parent.parent; } for (i = 0; i < nodePath.length; i += 1) { linkPath.push('' + (nodePath[i].relativeName || 'All files') + ''); } linkPath.reverse(); return linkPath.length > 0 ? linkPath.join(' » ') + ' » ' + node.displayShortName() : ''; }, fillTemplate: function (node, templateData) { var opts = this.opts, linkMapper = opts.linkMapper; templateData.entity = node.name || 'All files'; templateData.metrics = node.metrics; templateData.reportClass = getReportClass(node.metrics.statements, opts.watermarks.statements); templateData.pathHtml = pathTemplate({ html: this.getPathHtml(node, linkMapper) }); templateData.prettify = { js: linkMapper.asset(node, 'prettify.js'), css: linkMapper.asset(node, 'prettify.css') }; }, writeDetailPage: function (writer, node, fileCoverage) { var opts = this.opts, sourceStore = opts.sourceStore, templateData = opts.templateData, sourceText = fileCoverage.code && Array.isArray(fileCoverage.code) ? fileCoverage.code.join('\n') + '\n' : sourceStore.get(fileCoverage.path), code = sourceText.split(/(?:\r?\n)|\r/), count = 0, structured = code.map(function (str) { count += 1; return { line: count, covered: null, text: new InsertionText(str, true) }; }), context; structured.unshift({ line: 0, covered: null, text: new InsertionText("") }); this.fillTemplate(node, templateData); writer.write(headerTemplate(templateData)); writer.write('
\n');

        annotateLines(fileCoverage, structured);
        //note: order is important, since statements typically result in spanning the whole line and doing branches late
        //causes mismatched tags
        annotateBranches(fileCoverage, structured);
        annotateFunctions(fileCoverage, structured);
        annotateStatements(fileCoverage, structured);

        structured.shift();
        context = {
            structured: structured,
            maxLines: structured.length,
            fileCoverage: fileCoverage
        };
        writer.write(detailTemplate(context));
        writer.write('
\n'); writer.write(footerTemplate(templateData)); }, writeIndexPage: function (writer, node) { var linkMapper = this.opts.linkMapper, templateData = this.opts.templateData, children = Array.prototype.slice.apply(node.children), watermarks = this.opts.watermarks; children.sort(function (a, b) { return a.name < b.name ? -1 : 1; }); this.fillTemplate(node, templateData); writer.write(headerTemplate(templateData)); writer.write(summaryTableHeader); children.forEach(function (child) { var metrics = child.metrics, reportClasses = { statements: getReportClass(metrics.statements, watermarks.statements), lines: getReportClass(metrics.lines, watermarks.lines), functions: getReportClass(metrics.functions, watermarks.functions), branches: getReportClass(metrics.branches, watermarks.branches) }, data = { metrics: metrics, reportClasses: reportClasses, file: child.displayShortName(), output: linkMapper.fromParent(child) }; writer.write(summaryLineTemplate(data) + '\n'); }); writer.write(summaryTableFooter); writer.write(footerTemplate(templateData)); }, writeFiles: function (writer, node, dir, collector) { var that = this, indexFile = path.resolve(dir, 'index.html'), childFile; if (this.opts.verbose) { console.error('Writing ' + indexFile); } writer.writeFile(indexFile, function (contentWriter) { that.writeIndexPage(contentWriter, node); }); node.children.forEach(function (child) { if (child.kind === 'dir') { that.writeFiles(writer, child, path.resolve(dir, child.relativeName), collector); } else { childFile = path.resolve(dir, child.relativeName + '.html'); if (that.opts.verbose) { console.error('Writing ' + childFile); } writer.writeFile(childFile, function (contentWriter) { that.writeDetailPage(contentWriter, child, collector.fileCoverageFor(child.fullPath())); }); } }); }, standardLinkMapper: function () { return { fromParent: function (node) { var i = 0, relativeName = node.relativeName, ch; if (SEP !== '/') { relativeName = ''; for (i = 0; i < node.relativeName.length; i += 1) { ch = node.relativeName.charAt(i); if (ch === SEP) { relativeName += '/'; } else { relativeName += ch; } } } return node.kind === 'dir' ? relativeName + 'index.html' : relativeName + '.html'; }, ancestorHref: function (node, num) { var href = '', separated, levels, i, j; for (i = 0; i < num; i += 1) { separated = node.relativeName.split(SEP); levels = separated.length - 1; for (j = 0; j < levels; j += 1) { href += '../'; } node = node.parent; } return href; }, ancestor: function (node, num) { return this.ancestorHref(node, num) + 'index.html'; }, asset: function (node, name) { var i = 0, parent = node.parent; while (parent) { i += 1; parent = parent.parent; } return this.ancestorHref(node, i) + name; } }; }, writeReport: function (collector, sync) { var opts = this.opts, dir = opts.dir, summarizer = new TreeSummarizer(), writer = opts.writer || new FileWriter(sync), tree; collector.files().forEach(function (key) { summarizer.addFileCoverageSummary(key, utils.summarizeFileCoverage(collector.fileCoverageFor(key))); }); tree = summarizer.getTreeSummary(); fs.readdirSync(path.resolve(__dirname, '..', 'vendor')).forEach(function (f) { var resolvedSource = path.resolve(__dirname, '..', 'vendor', f), resolvedDestination = path.resolve(dir, f), stat = fs.statSync(resolvedSource); if (stat.isFile()) { if (opts.verbose) { console.log('Write asset: ' + resolvedDestination); } writer.copyFile(resolvedSource, resolvedDestination); } }); //console.log(JSON.stringify(tree.root, undefined, 4)); this.writeFiles(writer, tree.root, dir, collector); } }); module.exports = HtmlReport;