Seamless JavaScript code coverage library for both browser and Node.js environments
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.