Coverage report generation, custom reporter support, and output formatting for coverage statistics.
Generate coverage reports using the configured reporter.
/**
* Generate coverage report using configured reporter
* Called automatically by onTestsDone() or manually for custom reporting
* @param coverage_data - Coverage statistics and file data
*/
function report(coverage_data);
/**
* Get the coverage variable name used for tracking
* @returns Coverage variable name ("window._$blanket" in browser, "_$jscoverage" in Node.js, or custom)
*/
function getCovVar();Usage Examples:
// Automatic reporting (called by onTestsDone)
blanket.onTestsDone(); // Triggers blanket.report(coverageData)
// Manual reporting
const coverageData = window._$blanket || global._$jscoverage;
blanket.report({
files: coverageData,
stats: blanket.coverageInfo.stats
});
// Get coverage variable name
const covVar = blanket.getCovVar(); // "_$jscoverage" or custom name
const rawCoverage = global[covVar];Set custom reporter functions or paths for specialized output formats.
/**
* Custom reporter property that can be assigned a reporting function
* @type {Function}
*/
customReporter;
/**
* Default reporter property containing built-in HTML reporter
* @type {Function}
*/
defaultReporter;Generates LCOV format output for integration with coverage tools.
Location: src/reporters/lcov_reporter.js
// LCOV reporter usage
blanket.options("reporter", "./reporters/lcov_reporter.js");
// Or use directly
blanket.customReporter = function(coverageData, options) {
var toHTML = options && options.toHTML !== undefined ? options.toHTML : true;
for (var filename in coverageData.files) {
var data = coverageData.files[filename];
var lcovStr = 'SF:' + filename + '\n';
data.source.forEach(function(line, num) {
num++; // LCOV uses 1-based line numbers
if (data[num] !== undefined) {
lcovStr += 'DA:' + num + ',' + data[num] + '\n';
}
});
lcovStr += 'end_of_record\n';
if (toHTML) {
// Display in browser
var div = document.createElement('div');
div.className = "blanket_lcov_reporter";
div.innerText = lcovStr;
document.body.appendChild(div);
} else {
// Store for programmatic access
window._$blanket_LCOV = (window._$blanket_LCOV || '') + lcovStr;
}
}
};LCOV Output Format:
SF:src/app.js
DA:1,5
DA:2,3
DA:3,0
DA:4,2
end_of_record
SF:src/utils.js
DA:1,1
DA:2,1
end_of_recordOutputs coverage data in JSON format.
Location: src/reporters/simple_json_reporter.js
// JSON reporter outputs coverage data as formatted JSON
blanket.customReporter = function(coverageData) {
console.log(JSON.stringify(coverageData, null, 2));
};Each instrumented file has coverage data in this structure:
interface FileCoverage {
source: string[]; // Original source code lines
[lineNumber: number]: number; // Line execution counts (1-based indexing)
branchData?: { // Branch coverage data (when branchTracking enabled)
[lineNumber: number]: {
[branchIndex: number]: {
consequent: BranchInfo;
alternate: BranchInfo;
};
};
};
}
interface BranchInfo {
start: { line: number; column: number; };
end: { line: number; column: number; };
}Test execution statistics collected during test runs:
interface CoverageStats {
suites: number; // Number of test suites executed
tests: number; // Total number of tests run
passes: number; // Number of tests that passed
pending: number; // Number of tests currently pending
failures: number; // Number of tests that failed
start: Date; // Test execution start timestamp
end: Date; // Test execution end timestamp
}The full coverage data object passed to reporters:
interface CoverageData {
files: {
[filename: string]: FileCoverage;
};
stats: CoverageStats;
}function myCustomReporter(coverageData, options) {
console.log("=== Coverage Report ===");
console.log("Tests run:", coverageData.stats.tests);
console.log("Pass rate:",
(coverageData.stats.passes / coverageData.stats.tests * 100).toFixed(2) + "%");
console.log("\n=== File Coverage ===");
for (const filename in coverageData.files) {
const fileData = coverageData.files[filename];
const totalLines = fileData.source.length;
let coveredLines = 0;
for (let i = 1; i <= totalLines; i++) {
if (fileData[i] > 0) coveredLines++;
}
const coverage = (coveredLines / totalLines * 100).toFixed(2);
console.log(`${filename}: ${coverage}% (${coveredLines}/${totalLines} lines)`);
}
}
// Use the custom reporter
blanket.options("reporter", myCustomReporter);function advancedReporter(coverageData, options) {
const results = {
summary: {
files: 0,
lines: { total: 0, covered: 0, percentage: 0 },
branches: { total: 0, covered: 0, percentage: 0 }
},
files: {}
};
for (const filename in coverageData.files) {
const fileData = coverageData.files[filename];
results.summary.files++;
// Line coverage analysis
const totalLines = fileData.source.length;
let coveredLines = 0;
for (let i = 1; i <= totalLines; i++) {
if (fileData[i] !== undefined) {
results.summary.lines.total++;
if (fileData[i] > 0) {
coveredLines++;
results.summary.lines.covered++;
}
}
}
// Branch coverage analysis
let totalBranches = 0;
let coveredBranches = 0;
if (fileData.branchData) {
for (const lineNum in fileData.branchData) {
for (const branchIndex in fileData.branchData[lineNum]) {
const branch = fileData.branchData[lineNum][branchIndex];
totalBranches += 2; // consequent + alternate
// Check if branches were executed
if (branch.consequent && branch.consequent.length > 0) coveredBranches++;
if (branch.alternate && branch.alternate.length > 0) coveredBranches++;
}
}
}
results.summary.branches.total += totalBranches;
results.summary.branches.covered += coveredBranches;
// Store file-specific results
results.files[filename] = {
lines: {
total: totalLines,
covered: coveredLines,
percentage: (coveredLines / totalLines * 100).toFixed(2)
},
branches: {
total: totalBranches,
covered: coveredBranches,
percentage: totalBranches > 0 ? (coveredBranches / totalBranches * 100).toFixed(2) : "N/A"
}
};
}
// Calculate summary percentages
results.summary.lines.percentage =
(results.summary.lines.covered / results.summary.lines.total * 100).toFixed(2);
results.summary.branches.percentage =
results.summary.branches.total > 0
? (results.summary.branches.covered / results.summary.branches.total * 100).toFixed(2)
: "N/A";
// Output results
console.log("Coverage Summary:");
console.log(`Files: ${results.summary.files}`);
console.log(`Lines: ${results.summary.lines.covered}/${results.summary.lines.total} (${results.summary.lines.percentage}%)`);
console.log(`Branches: ${results.summary.branches.covered}/${results.summary.branches.total} (${results.summary.branches.percentage}%)`);
return results;
}
blanket.options("reporter", advancedReporter);Reporters can receive options through the reporter_options configuration:
blanket.options("reporter_options", {
shortnames: true, // Use short filenames
relativepath: true, // Use relative paths
basepath: "/project", // Base path to remove from filenames
threshold: 80, // Minimum coverage threshold
outputFile: "coverage.json" // Output file path
});shortnames: Remove directory path, show only filenamerelativepath: Show paths relative to current working directorybasepath: Remove specified base path from all filenamesExamples:
// Original filename: "/project/src/utils/helper.js"
// With shortnames: true
// Result: "helper.js"
// With relativepath: true (cwd = "/project")
// Result: "src/utils/helper.js"
// With basepath: "/project/src"
// Result: "utils/helper.js"// Custom reporter for CI environments
function ciReporter(coverageData) {
const summary = calculateCoverage(coverageData);
// Exit with error code if coverage below threshold
const threshold = blanket.options("reporter_options").threshold || 80;
if (summary.percentage < threshold) {
console.error(`Coverage ${summary.percentage}% below threshold ${threshold}%`);
process.exit(1);
}
console.log(`Coverage: ${summary.percentage}% - PASSED`);
}
blanket.options("reporter", ciReporter);
blanket.options("reporter_options", { threshold: 85 });// Reporter that writes to file
function fileReporter(coverageData, options) {
const fs = require('fs');
const outputFile = options.outputFile || 'coverage.json';
const report = {
timestamp: new Date().toISOString(),
coverage: coverageData
};
fs.writeFileSync(outputFile, JSON.stringify(report, null, 2));
console.log(`Coverage report written to ${outputFile}`);
}
blanket.options("reporter", fileReporter);
blanket.options("reporter_options", { outputFile: "build/coverage.json" });In browsers, blanket.report() is called automatically by onTestsDone():
// Browser reporting flow
function onTestsDone() {
coverageInfo.stats.end = new Date();
this.report(coverageInfo); // Calls configured reporter
}In Node.js, the configured reporter function is called directly:
// Node.js reporting flow
function onTestsDone() {
coverageInfo.stats.end = new Date();
// Clean up branch tracking if disabled
if (!blanket.options("branchTracking")) {
delete global[blanket.getCovVar()].branchFcn;
}
// Call reporter function
blanket.options("reporter").call(blanket, coverageInfo);
}This allows for different reporting strategies in each environment while maintaining a consistent API.