jest-test-lineage-reporter
Version:
Comprehensive test analytics platform with line-by-line coverage, performance metrics, memory analysis, and test quality scoring
1,540 lines (1,400 loc) β’ 128 kB
JavaScript
const fs = require('fs');
const path = require('path');
const { loadConfig } = require('./config');
const MutationTester = require('./MutationTester');
class TestCoverageReporter {
constructor(globalConfig, options) {
this.globalConfig = globalConfig;
this.options = options || {};
this.coverageData = {};
this.testCoverageMap = new Map(); // Map to store individual test coverage
this.currentTestFile = null;
this.baselineCoverage = null;
// Validate configuration
this.validateConfig();
}
validateConfig() {
if (!this.globalConfig) {
throw new Error('TestCoverageReporter: globalConfig is required');
}
// Set default options
this.options = {
outputFile: this.options.outputFile || 'test-lineage-report.html',
memoryLeakThreshold: this.options.memoryLeakThreshold || 50 * 1024, // 50KB
gcPressureThreshold: this.options.gcPressureThreshold || 5,
qualityThreshold: this.options.qualityThreshold || 60,
enableDebugLogging: this.options.enableDebugLogging || false,
...this.options
};
}
// This method is called after a single test file (suite) has completed.
async onTestResult(test, testResult, _aggregatedResult) {
const testFilePath = test.path;
// Store test file path for later use
this.currentTestFile = testFilePath;
// Always process fallback coverage for now, but mark for potential precise data
const coverage = testResult.coverage;
if (!coverage) {
return;
}
// Process each individual test result (fallback mode)
testResult.testResults.forEach((testCase, index) => {
if (testCase.status === 'passed') {
this.processIndividualTestCoverage(testCase, coverage, testFilePath, index);
}
});
}
processLineageResults(lineageResults, testFilePath) {
console.log('π― Processing precise lineage tracking results...');
// lineageResults format: { filePath: { lineNumber: [testInfo, ...] } }
for (const filePath in lineageResults) {
// Skip test files - we only want to track coverage of source files
if (filePath.includes('__tests__') || filePath.includes('.test.') || filePath.includes('.spec.')) {
continue;
}
// Initialize the data structure for this file if it doesn't exist
if (!this.coverageData[filePath]) {
this.coverageData[filePath] = {};
}
for (const lineNumber in lineageResults[filePath]) {
const testInfos = lineageResults[filePath][lineNumber];
// Convert to our expected format
const processedTests = testInfos.map(testInfo => ({
name: testInfo.testName,
file: path.basename(testInfo.testFile || testFilePath),
fullPath: testInfo.testFile || testFilePath,
executionCount: testInfo.executionCount || 1,
timestamp: testInfo.timestamp || Date.now(),
type: 'precise' // Mark as precise tracking
}));
this.coverageData[filePath][lineNumber] = processedTests;
}
}
}
processIndividualTestCoverage(testCase, coverage, testFilePath, _testIndex) {
const testName = testCase.fullName;
// Since Jest doesn't provide per-test coverage, we'll use heuristics
// to estimate which tests likely covered which lines
for (const filePath in coverage) {
// Skip test files
if (filePath.includes('__tests__') || filePath.includes('.test.') || filePath.includes('.spec.')) {
continue;
}
const fileCoverage = coverage[filePath];
const statementMap = fileCoverage.statementMap;
const statements = fileCoverage.s;
// Initialize the data structure for this file if it doesn't exist
if (!this.coverageData[filePath]) {
this.coverageData[filePath] = {};
}
// Analyze which lines were covered
for (const statementId in statements) {
if (statements[statementId] > 0) {
const lineNumber = String(statementMap[statementId].start.line);
if (!this.coverageData[filePath][lineNumber]) {
this.coverageData[filePath][lineNumber] = [];
}
// Use heuristics to determine if this test likely covered this line
if (this.isTestLikelyCoveringLine(testCase, filePath, lineNumber, fileCoverage)) {
// Add test with more detailed information
const testInfo = {
name: testName,
file: path.basename(testFilePath),
fullPath: testFilePath,
duration: testCase.duration || 0,
confidence: this.calculateConfidence(testCase, filePath, lineNumber)
};
this.coverageData[filePath][lineNumber].push(testInfo);
}
}
}
}
}
isTestLikelyCoveringLine(testCase, _filePath, _lineNumber, _fileCoverage) {
// Heuristic 1: If test name mentions the function/file being tested
const _testName = testCase.fullName.toLowerCase();
// Simplified heuristic - for now, include all tests
// TODO: Implement more sophisticated heuristics
// Heuristic 3: If it's a simple file with few tests, assume all tests cover most lines
// This is a fallback for when we can't determine specific coverage
return true; // For now, include all tests (we'll refine this)
}
calculateConfidence(_testCase, _filePath, _lineNumber) {
// Simplified confidence calculation
// TODO: Implement more sophisticated confidence scoring
return 75; // Default confidence
}
extractCodeKeywords(sourceCode) {
// Extract function names, variable names, etc. from source code line
const keywords = [];
// Match function names: function name() or name: function() or const name =
const functionMatches = sourceCode.match(/(?:function\s+(\w+)|(\w+)\s*[:=]\s*(?:function|\()|(?:const|let|var)\s+(\w+))/g);
if (functionMatches) {
functionMatches.forEach(match => {
const nameMatch = match.match(/(\w+)/);
if (nameMatch) keywords.push(nameMatch[1]);
});
}
// Match method calls: object.method()
const methodMatches = sourceCode.match(/(\w+)\s*\(/g);
if (methodMatches) {
methodMatches.forEach(match => {
const nameMatch = match.match(/(\w+)/);
if (nameMatch) keywords.push(nameMatch[1]);
});
}
return keywords;
}
getSourceCodeLine(filePath, lineNumber) {
try {
const sourceCode = fs.readFileSync(filePath, 'utf8');
const lines = sourceCode.split('\n');
return lines[lineNumber - 1] || '';
} catch (error) {
return '';
}
}
// This method is called after all tests in the entire run have completed.
async onRunComplete(_contexts, _results) {
// Try to get precise tracking data before generating reports
this.tryGetPreciseTrackingData();
this.generateReport();
await this.generateHtmlReport();
// Run mutation testing if enabled
await this.runMutationTestingIfEnabled();
}
/**
* Run mutation testing if enabled in configuration
*/
async runMutationTestingIfEnabled() {
const config = loadConfig(this.options);
if (config.enableMutationTesting) {
console.log('\n𧬠Mutation testing enabled, starting mutation analysis...');
let mutationTester = null;
try {
mutationTester = new MutationTester(config);
// Pass the current coverage data directly instead of loading from file
const lineageData = this.convertCoverageDataToLineageFormat();
if (!lineageData || Object.keys(lineageData).length === 0) {
console.log('β οΈ No lineage data available for mutation testing');
return;
}
// Set the lineage data directly
mutationTester.setLineageData(lineageData);
// Run mutation testing
const results = await mutationTester.runMutationTesting();
// Store results for HTML report integration
this.mutationResults = results;
// Regenerate HTML report with mutation data
await this.generateHtmlReport();
} catch (error) {
console.error('β Mutation testing failed:', error.message);
if (this.options.enableDebugLogging) {
console.error(error.stack);
}
} finally {
// Always cleanup, even if there was an error
if (mutationTester) {
try {
await mutationTester.cleanup();
} catch (cleanupError) {
console.error('β Error during mutation testing cleanup:', cleanupError.message);
// Try emergency cleanup as last resort
mutationTester.emergencyCleanup();
}
}
}
}
}
tryGetPreciseTrackingData() {
// Try to get data from global persistent data first (most reliable)
if (global.__LINEAGE_PERSISTENT_DATA__ && global.__LINEAGE_PERSISTENT_DATA__.length > 0) {
console.log('π― Found precise lineage tracking data from global persistent data! Replacing estimated data...');
// Clear existing coverage data and replace with precise data
this.coverageData = {};
this.processFileTrackingData(global.__LINEAGE_PERSISTENT_DATA__);
return true;
}
// Fallback to global function
if (global.__GET_LINEAGE_RESULTS__) {
const lineageResults = global.__GET_LINEAGE_RESULTS__();
if (Object.keys(lineageResults).length > 0) {
console.log('π― Found precise lineage tracking data from global function! Replacing estimated data...');
// Clear existing coverage data and replace with precise data
this.coverageData = {};
this.processLineageResults(lineageResults, 'precise-tracking');
return true;
}
}
// Last resort: try to read tracking data from file
const fileData = this.readTrackingDataFromFile();
if (fileData) {
console.log('π― Found precise lineage tracking data from file! Replacing estimated data...');
// Clear existing coverage data and replace with precise data
this.coverageData = {};
this.processFileTrackingData(fileData);
return true;
}
console.log('β οΈ No precise tracking data found, using estimated coverage');
return false;
}
/**
* Convert the current coverage data to the format expected by MutationTester
*/
convertCoverageDataToLineageFormat() {
const lineageData = {};
// Iterate through all coverage data and convert to mutation testing format
// The actual structure is: this.coverageData[filePath][lineNumber] = [testInfo, ...]
for (const [filePath, fileData] of Object.entries(this.coverageData)) {
if (!fileData || typeof fileData !== 'object') continue;
for (const [lineNumber, tests] of Object.entries(fileData)) {
if (!Array.isArray(tests) || tests.length === 0) continue;
if (!lineageData[filePath]) {
lineageData[filePath] = {};
}
lineageData[filePath][lineNumber] = tests.map(test => ({
testName: test.name || test.testName || 'Unknown test',
testType: test.testType || test.type || 'it',
testFile: test.testFile || test.file || 'unknown',
executionCount: test.executionCount || 1,
}));
}
}
console.log(`π Converted coverage data to lineage format: ${Object.keys(lineageData).length} files`);
Object.keys(lineageData).forEach(filePath => {
const lineCount = Object.keys(lineageData[filePath]).length;
console.log(` ${filePath}: ${lineCount} lines`);
});
return lineageData;
}
readTrackingDataFromFile() {
try {
const filePath = path.join(process.cwd(), '.jest-lineage-data.json');
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
console.log(`π Read tracking data: ${data.tests.length} tests from file`);
return data.tests;
} else {
console.log(`β οΈ Tracking data file not found: ${filePath}`);
}
} catch (error) {
console.warn('Warning: Could not read tracking data from file:', error.message);
}
return null;
}
processFileTrackingData(testDataArray) {
if (!Array.isArray(testDataArray)) {
console.warn('β οΈ processFileTrackingData: testDataArray is not an array');
return;
}
console.log(`π Processing ${testDataArray.length} test data entries`);
let processedFiles = 0;
let processedLines = 0;
testDataArray.forEach((testData, index) => {
try {
if (!testData || typeof testData !== 'object') {
console.warn(`β οΈ Skipping invalid test data at index ${index}:`, testData);
return;
}
if (!testData.coverage || typeof testData.coverage !== 'object') {
console.warn(`β οΈ Skipping test data with invalid coverage at index ${index}:`, testData.name);
return;
}
// testData.coverage is now a plain object, not a Map
Object.entries(testData.coverage).forEach(([key, count]) => {
try {
// Skip metadata, depth, and performance entries for now (process them separately)
if (key.includes(':meta') || key.includes(':depth') || key.includes(':performance')) {
return;
}
const parts = key.split(':');
if (parts.length < 2) {
console.warn(`β οΈ Invalid key format: ${key}`);
return;
}
const lineNumber = parts.pop(); // Last part is line number
const filePath = parts.join(':'); // Rejoin in case path contains colons
// Validate line number
if (isNaN(parseInt(lineNumber))) {
console.warn(`β οΈ Invalid line number: ${lineNumber} for file: ${filePath}`);
return;
}
// Skip test files and node_modules
if (filePath.includes('__tests__') ||
filePath.includes('.test.') ||
filePath.includes('.spec.') ||
filePath.includes('node_modules')) {
console.log(`π DEBUG: Skipping test/node_modules file: ${filePath}`);
return;
}
//console.log(`π DEBUG: Processing coverage for ${filePath}:${lineNumber} (count: ${count})`);
processedLines++;
// Get depth data for this line
const depthKey = `${filePath}:${lineNumber}:depth`;
const depthData = testData.coverage[depthKey] || { 1: count };
// Get metadata for this line
const metaKey = `${filePath}:${lineNumber}:meta`;
const metaData = testData.coverage[metaKey] || {};
// Get performance data for this line
const performanceKey = `${filePath}:${lineNumber}:performance`;
const performanceData = testData.coverage[performanceKey] || {
totalExecutions: count,
totalCpuTime: 0,
totalWallTime: 0,
totalMemoryDelta: 0,
minExecutionTime: 0,
maxExecutionTime: 0,
executionTimes: [],
cpuCycles: []
};
// Initialize the data structure for this file if it doesn't exist
if (!this.coverageData[filePath]) {
this.coverageData[filePath] = {};
}
if (!this.coverageData[filePath][lineNumber]) {
this.coverageData[filePath][lineNumber] = [];
}
// Add test with precise tracking information including depth, performance, and quality
const testInfo = {
name: testData.name || 'Unknown test',
file: testData.testFile || 'unknown-test-file',
fullPath: testData.testFile || 'unknown-test-file',
executionCount: typeof count === 'number' ? count : 1,
duration: testData.duration || 0,
type: 'precise', // Mark as precise tracking
depthData: depthData, // Call depth information
minDepth: metaData.minDepth || 1,
maxDepth: metaData.maxDepth || 1,
nodeType: metaData.nodeType || 'unknown',
performance: {
totalCpuTime: performanceData.totalCpuTime || 0,
totalWallTime: performanceData.totalWallTime || 0,
avgCpuTime: performanceData.totalExecutions > 0 ? performanceData.totalCpuTime / performanceData.totalExecutions : 0,
avgWallTime: performanceData.totalExecutions > 0 ? performanceData.totalWallTime / performanceData.totalExecutions : 0,
totalMemoryDelta: performanceData.totalMemoryDelta || 0,
minExecutionTime: performanceData.minExecutionTime || 0,
maxExecutionTime: performanceData.maxExecutionTime || 0,
totalCpuCycles: performanceData.cpuCycles ? performanceData.cpuCycles.reduce((sum, cycles) => sum + cycles, 0) : 0,
avgCpuCycles: performanceData.cpuCycles && performanceData.cpuCycles.length > 0 ?
performanceData.cpuCycles.reduce((sum, cycles) => sum + cycles, 0) / performanceData.cpuCycles.length : 0,
performanceVariance: performanceData.performanceVariance || 0,
performanceStdDev: performanceData.performanceStdDev || 0,
performanceP95: performanceData.performanceP95 || 0,
performanceP99: performanceData.performanceP99 || 0,
slowExecutions: performanceData.slowExecutions || 0,
fastExecutions: performanceData.fastExecutions || 0,
memoryLeaks: performanceData.memoryLeaks || 0,
gcPressure: performanceData.gcPressure || 0
},
quality: testData.qualityMetrics || {
assertions: 0,
asyncOperations: 0,
mockUsage: 0,
errorHandling: 0,
edgeCases: 0,
complexity: 0,
maintainability: 50,
reliability: 50,
testSmells: [],
codePatterns: [],
isolationScore: 100,
testLength: 0
}
};
this.coverageData[filePath][lineNumber].push(testInfo);
// console.log(`π DEBUG: Added coverage for "${filePath}":${lineNumber} -> ${testData.name} (${count} executions)`);
} catch (entryError) {
console.warn(`β οΈ Error processing coverage entry ${key}:`, entryError.message);
}
});
} catch (testError) {
console.warn(`β οΈ Error processing test data at index ${index}:`, testError.message);
}
});
console.log(`β
Processed tracking data for ${Object.keys(this.coverageData).length} files (${processedLines} lines processed)`);
// Debug: Show what files were processed
Object.keys(this.coverageData).forEach(filePath => {
const lineCount = Object.keys(this.coverageData[filePath]).length;
console.log(` π ${filePath}: ${lineCount} lines`);
});
}
generateReport() {
console.log('\n--- Jest Test Lineage Reporter: Line-by-Line Test Coverage ---');
// Generate test quality summary first
this.generateTestQualitySummary();
for (const filePath in this.coverageData) {
const lineCoverage = this.coverageData[filePath];
const lineNumbers = Object.keys(lineCoverage).sort((a, b) => parseInt(a) - parseInt(b));
if (lineNumbers.length === 0) {
continue;
}
for (const line of lineNumbers) {
const testInfos = lineCoverage[line];
const uniqueTests = this.deduplicateTests(testInfos);
uniqueTests.forEach(testInfo => {
const testName = typeof testInfo === 'string' ? testInfo : testInfo.name;
const testFile = typeof testInfo === 'object' ? testInfo.file : 'Unknown';
const executionCount = typeof testInfo === 'object' ? testInfo.executionCount : 1;
const trackingType = typeof testInfo === 'object' && testInfo.type === 'precise' ? 'β
PRECISE' : 'β οΈ ESTIMATED';
// Add depth information for precise tracking
let depthInfo = '';
if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.depthData) {
const depths = Object.keys(testInfo.depthData).map(d => parseInt(d)).sort((a, b) => a - b);
if (depths.length === 1) {
depthInfo = `, depth ${depths[0]}`;
} else {
depthInfo = `, depths ${depths.join(',')}`;
}
}
// Add performance information for precise tracking
let performanceInfo = '';
if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.performance) {
const perf = testInfo.performance;
if (perf.avgCpuCycles > 0) {
const cycles = perf.avgCpuCycles > 1000000 ?
`${(perf.avgCpuCycles / 1000000).toFixed(1)}M` :
`${Math.round(perf.avgCpuCycles)}`;
const cpuTime = perf.avgCpuTime > 1000 ?
`${(perf.avgCpuTime / 1000).toFixed(2)}ms` :
`${perf.avgCpuTime.toFixed(1)}ΞΌs`;
// Add memory information
let memoryInfo = '';
if (perf.totalMemoryDelta !== 0) {
const memoryMB = Math.abs(perf.totalMemoryDelta) / (1024 * 1024);
const memorySign = perf.totalMemoryDelta > 0 ? '+' : '-';
if (memoryMB > 1) {
memoryInfo = `, ${memorySign}${memoryMB.toFixed(1)}MB`;
} else {
const memoryKB = Math.abs(perf.totalMemoryDelta) / 1024;
memoryInfo = `, ${memorySign}${memoryKB.toFixed(1)}KB`;
}
}
// Add performance alerts
let alerts = '';
const memoryMB = Math.abs(perf.totalMemoryDelta || 0) / (1024 * 1024);
if (memoryMB > 1) alerts += ` π¨LEAK`;
if (perf.gcPressure > 5) alerts += ` ποΈGC`;
if (perf.slowExecutions > perf.fastExecutions) alerts += ` πSLOW`;
performanceInfo = `, ${cycles} cycles, ${cpuTime}${memoryInfo}${alerts}`;
}
}
// Add quality information for precise tracking
let qualityInfo = '';
if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.quality) {
const quality = testInfo.quality;
const qualityBadges = [];
// Quality score with explanation
let qualityScore = '';
if (quality.maintainability >= 80) {
qualityScore = 'π High Quality';
} else if (quality.maintainability >= 60) {
qualityScore = 'β
Good Quality';
} else if (quality.maintainability >= 40) {
qualityScore = 'β οΈ Fair Quality';
} else {
qualityScore = 'β Poor Quality';
}
qualityBadges.push(`${qualityScore} (${Math.round(quality.maintainability)}%)`);
// Reliability score
if (quality.reliability >= 80) qualityBadges.push(`π‘οΈ Reliable (${Math.round(quality.reliability)}%)`);
else if (quality.reliability >= 60) qualityBadges.push(`π Stable (${Math.round(quality.reliability)}%)`);
else qualityBadges.push(`β οΈ Fragile (${Math.round(quality.reliability)}%)`);
// Test smells with details
if (quality.testSmells && quality.testSmells.length > 0) {
qualityBadges.push(`π¨ ${quality.testSmells.length} Smells: ${quality.testSmells.join(', ')}`);
}
// Positive indicators
if (quality.assertions > 5) qualityBadges.push(`π― ${quality.assertions} Assertions`);
if (quality.errorHandling > 0) qualityBadges.push(`π ${quality.errorHandling} Error Tests`);
if (quality.edgeCases > 3) qualityBadges.push(`πͺ ${quality.edgeCases} Edge Cases`);
if (qualityBadges.length > 0) {
qualityInfo = ` [${qualityBadges.join(', ')}]`;
}
}
// console.log(` - "${testName}" (${testFile}, ${executionCount} executions${depthInfo}${performanceInfo}) ${trackingType}${qualityInfo}`);
});
}
}
console.log('\n--- Report End ---');
}
async generateHtmlReport() {
console.log('\nπ Generating HTML coverage report...');
let html = this.generateHtmlTemplate();
// Validate coverage data before processing
const validFiles = this.validateCoverageData();
for (const filePath of validFiles) {
try {
html += await this.generateCodeTreeSection(filePath);
} catch (error) {
console.error(`β Error generating section for ${filePath}:`, error.message);
html += this.generateErrorSection(filePath, error.message);
}
}
html += this.generateHtmlFooter();
// Write HTML file
const outputPath = path.join(process.cwd(), 'test-lineage-report.html');
fs.writeFileSync(outputPath, html, 'utf8');
console.log(`β
HTML report generated: ${outputPath}`);
console.log('π Open the file in your browser to view the visual coverage report');
}
validateCoverageData() {
const validFiles = [];
for (const filePath in this.coverageData) {
const fileData = this.coverageData[filePath];
if (!fileData) {
console.warn(`β οΈ Skipping ${filePath}: No coverage data`);
continue;
}
if (typeof fileData !== 'object') {
console.warn(`β οΈ Skipping ${filePath}: Invalid data type (${typeof fileData})`);
continue;
}
if (Object.keys(fileData).length === 0) {
console.warn(`β οΈ Skipping ${filePath}: Empty coverage data`);
continue;
}
// Validate that the data structure is correct
let hasValidData = false;
for (const lineNumber in fileData) {
const lineData = fileData[lineNumber];
if (Array.isArray(lineData) && lineData.length > 0) {
hasValidData = true;
break;
}
}
if (!hasValidData) {
console.warn(`β οΈ Skipping ${filePath}: No valid line data found`);
continue;
}
validFiles.push(filePath);
}
// console.log(`β
Validated ${validFiles.length} files for HTML report`);
return validFiles;
}
findMatchingCoveragePath(targetPath) {
const targetBasename = path.basename(targetPath);
// Strategy 1: Find exact basename match
for (const coveragePath of Object.keys(this.coverageData)) {
if (path.basename(coveragePath) === targetBasename) {
return coveragePath;
}
}
// Strategy 2: Convert both paths to relative from package.json and compare
const projectRoot = this.findProjectRoot(process.cwd());
const targetRelative = this.makeRelativeToProject(targetPath, projectRoot);
for (const coveragePath of Object.keys(this.coverageData)) {
const coverageRelative = this.makeRelativeToProject(coveragePath, projectRoot);
if (targetRelative === coverageRelative) {
return coveragePath;
}
}
// Strategy 3: Find path that contains the target filename
for (const coveragePath of Object.keys(this.coverageData)) {
if (coveragePath.includes(targetBasename)) {
return coveragePath;
}
}
// Strategy 4: Find path where target contains the coverage filename
for (const coveragePath of Object.keys(this.coverageData)) {
const coverageBasename = path.basename(coveragePath);
if (targetPath.includes(coverageBasename)) {
return coveragePath;
}
}
// Strategy 5: Fuzzy matching - remove common path differences
const normalizedTarget = this.normalizePath(targetPath);
for (const coveragePath of Object.keys(this.coverageData)) {
const normalizedCoverage = this.normalizePath(coveragePath);
if (normalizedTarget === normalizedCoverage) {
return coveragePath;
}
}
return null;
}
findProjectRoot(startPath) {
let currentDir = startPath;
const root = path.parse(currentDir).root;
while (currentDir !== root) {
const packageJsonPath = path.join(currentDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}
// Fallback to current working directory if no package.json found
return process.cwd();
}
makeRelativeToProject(filePath, projectRoot) {
// If path is absolute and starts with project root, make it relative
if (path.isAbsolute(filePath) && filePath.startsWith(projectRoot)) {
return path.relative(projectRoot, filePath);
}
// If path is already relative, return as-is
if (!path.isAbsolute(filePath)) {
return filePath;
}
// Try to find common base with project root
const relativePath = path.relative(projectRoot, filePath);
if (!relativePath.startsWith('..')) {
return relativePath;
}
// Fallback to original path
return filePath;
}
normalizePath(filePath) {
// Remove common path variations that might cause mismatches
return filePath
.replace(/\/services\//g, '/') // Remove /services/ directory
.replace(/\/src\//g, '/') // Remove /src/ directory
.replace(/\/lib\//g, '/') // Remove /lib/ directory
.replace(/\/app\//g, '/') // Remove /app/ directory
.replace(/\/server\//g, '/') // Remove /server/ directory
.replace(/\/client\//g, '/') // Remove /client/ directory
.replace(/\/+/g, '/') // Replace multiple slashes with single
.toLowerCase(); // Case insensitive matching
}
generateErrorSection(filePath, errorMessage) {
return `<div class="file-section">
<div class="file-header">β Error processing ${path.basename(filePath)}</div>
<div class="error">
<p><strong>File:</strong> ${filePath}</p>
<p><strong>Error:</strong> ${errorMessage}</p>
<p><strong>Suggestion:</strong> Check if the file exists and has valid coverage data.</p>
</div>
</div>`;
}
generateHtmlTemplate() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jest Test Lineage Report</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
line-height: 1.6;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.navigation {
background: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.nav-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-btn, .action-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-btn {
background: #e9ecef;
color: #495057;
}
.nav-btn.active {
background: #007bff;
color: white;
}
.nav-btn:hover {
background: #007bff;
color: white;
}
.action-btn {
background: #28a745;
color: white;
}
.action-btn:hover {
background: #218838;
}
.sort-controls {
display: flex;
align-items: center;
gap: 10px;
}
.sort-controls label {
font-weight: 500;
color: #495057;
}
.sort-controls select {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
background: white;
font-size: 14px;
}
.header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
.header p {
margin: 10px 0 0 0;
opacity: 0.9;
font-size: 1.1em;
}
.file-section {
background: white;
margin-bottom: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.file-header {
background: #2c3e50;
color: white;
padding: 20px;
font-size: 1.2em;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-header:hover {
background: #34495e;
}
.file-header .expand-icon {
font-size: 18px;
transition: transform 0.3s ease;
}
.file-header.expanded .expand-icon {
transform: rotate(90deg);
}
.file-content {
display: none;
}
.file-content.expanded {
display: block;
}
.file-path {
font-family: 'Courier New', monospace;
opacity: 0.8;
font-size: 0.9em;
}
.code-container {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
background: #f8f9fa;
}
.code-line {
display: flex;
border-bottom: 1px solid #e9ecef;
transition: background-color 0.2s ease;
}
.code-line:hover {
background-color: #e3f2fd;
}
.line-number {
background: #6c757d;
color: white;
padding: 8px 12px;
min-width: 60px;
text-align: right;
font-weight: bold;
user-select: none;
}
.line-covered {
background: #28a745;
}
.line-uncovered {
background: #dc3545;
}
.line-content {
flex: 1;
padding: 8px 16px;
white-space: pre;
overflow-x: auto;
}
.coverage-indicator {
background: #17a2b8;
color: white;
padding: 8px 12px;
min-width: 80px;
text-align: center;
font-size: 12px;
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
}
.coverage-indicator:hover {
background: #138496;
}
.coverage-details {
display: none;
background: #fff3cd;
border-left: 4px solid #ffc107;
margin: 0;
padding: 15px;
border-bottom: 1px solid #e9ecef;
}
.coverage-details.expanded {
display: block;
}
.test-badge {
display: inline-block;
background: #007bff;
color: white;
padding: 4px 8px;
margin: 2px;
border-radius: 12px;
font-size: 11px;
transition: all 0.2s ease;
}
.test-badge:hover {
background: #0056b3;
transform: translateY(-1px);
}
.depth-badge {
display: inline-block;
background: #6c757d;
color: white;
padding: 2px 6px;
margin-left: 4px;
border-radius: 8px;
font-size: 9px;
font-weight: bold;
vertical-align: middle;
}
.depth-badge.depth-1 {
background: #28a745; /* Green for direct calls */
}
.depth-badge.depth-2 {
background: #ffc107; /* Yellow for one level deep */
color: #212529;
}
.depth-badge.depth-3 {
background: #fd7e14; /* Orange for two levels deep */
}
.depth-badge.depth-4,
.depth-badge.depth-5,
.depth-badge.depth-6,
.depth-badge.depth-7,
.depth-badge.depth-8,
.depth-badge.depth-9,
.depth-badge.depth-10 {
background: #dc3545; /* Red for very deep calls */
}
.lines-analysis {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.lines-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.lines-table th,
.lines-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.lines-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
position: sticky;
top: 0;
}
.lines-table tr:hover {
background: #f8f9fa;
}
.file-name {
font-family: 'Courier New', monospace;
font-weight: 500;
color: #007bff;
}
.line-number {
font-family: 'Courier New', monospace;
font-weight: bold;
color: #6c757d;
text-align: center;
}
.code-preview {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #495057;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.test-count,
.execution-count,
.cpu-cycles,
.cpu-time,
.wall-time {
text-align: center;
font-weight: 600;
}
.cpu-cycles {
color: #dc3545;
font-family: 'Courier New', monospace;
}
.cpu-time {
color: #fd7e14;
font-family: 'Courier New', monospace;
}
.wall-time {
color: #6f42c1;
font-family: 'Courier New', monospace;
}
.max-depth,
.depth-range,
.quality-score,
.reliability-score,
.test-smells {
text-align: center;
}
.quality-excellent {
color: #28a745;
font-weight: bold;
}
.quality-good {
color: #6f42c1;
font-weight: bold;
}
.quality-fair {
color: #ffc107;
font-weight: bold;
}
.quality-poor {
color: #dc3545;
font-weight: bold;
}
.smells-none {
color: #28a745;
font-weight: bold;
}
.smells-few {
color: #ffc107;
font-weight: bold;
cursor: help;
}
.smells-many {
color: #dc3545;
font-weight: bold;
cursor: help;
}
.smells-few[title]:hover,
.smells-many[title]:hover {
text-decoration: underline;
}
.performance-analysis,
.quality-analysis,
.mutations-analysis {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.performance-summary,
.quality-summary {
margin-bottom: 30px;
}
.perf-stats,
.quality-stats {
display: flex;
gap: 20px;
margin: 20px 0;
flex-wrap: wrap;
}
.perf-stat,
.quality-stat {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
min-width: 120px;
}
.perf-number,
.quality-number {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.perf-label,
.quality-label {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
.performance-tables,
.quality-tables {
display: flex;
gap: 30px;
flex-wrap: wrap;
}
.perf-table-container,
.quality-table-container {
flex: 1;
min-width: 400px;
}
.perf-table,
.quality-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.perf-table th,
.perf-table td,
.quality-table th,
.quality-table td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.perf-table th,
.quality-table th {
background: #f8f9fa;
font-weight: 600;
}
.memory-usage {
color: #6f42c1;
font-weight: bold;
}
.memory-leaks {
color: #dc3545;
font-weight: bold;
}
.quality-distribution {
margin: 20px 0;
}
.quality-bars {
margin: 15px 0;
}
.quality-bar {
margin: 10px 0;
}
.bar {
width: 100%;
height: 20px;
background: #e9ecef;
border-radius: 10px;
overflow: hidden;
margin: 5px 0;
}
.fill {
height: 100%;
transition: width 0.3s ease;
}
.fill.excellent {
background: #28a745;
}
.fill.good {
background: #6f42c1;
}
.fill.fair {
background: #ffc107;
}
.fill.poor {
background: #dc3545;
}
.quality-issues {
color: #dc3545;
font-size: 12px;
}
.recommendations {
color: #007bff;
font-size: 12px;
font-style: italic;
}
.performance-help {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
font-size: 14px;
}
.help-section {
margin: 10px 0;
padding: 8px;
background: white;
border-radius: 4px;
border-left: 4px solid #007bff;
}
.help-section strong {
color: #495057;
}
.help-section em {
color: #28a745;
font-weight: 600;
}
.test-file {
font-weight: bold;
color: #495057;
margin-top: 10px;
margin-bottom: 5px;
}
.test-file:first-child {
margin-top: 0;
}
.line-coverage {
padding: 20px;
}
.line-item {
margin-bottom: 20px;
padding: 15px;
border-left: 4px solid #3498db;
background: #f8f9fa;
border-radius: 0 5px 5px 0;
}
.line-number {
font-weight: bold;
color: #2c3e50;
font-size: 1.1em;
margin-bottom: 10px;
}
.test-count {
color: #7f8c8d;
font-size: 0.9em;
}
.test-list {
margin-top: 10px;
}
.test-item {
background: white;
margin: 5px 0;
padding: 8px 12px;
border-radius: 20px;
display: inline-block;
margin-right: 8px;
margin-bottom: 8px;
border: 1px solid #e1e8ed;
font-size: 0.85em;
transition: all 0.2s ease;
}
.test-item:hover {
background: #3498db;
color: white;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stats {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.stats h3 {
margin-top: 0;
color: #2c3e50;
}
.stat-item {
display: inline-block;
margin-right: 30px;
margin-bottom: 10px;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #3498db;
}
.stat-label {
display: block;
color: #7f8c8d;
font-size: 0.9em;
}
.footer {
text-align: center;
color: #7f8c8d;
margin-top: 40px;
padding: 20px;
}
/* Mutation Testing Styles */
.mutation-results {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
margin: 15px 0;
padding: 15px;
}
.mutation-results h4 {
margin: 0 0 15px 0;
color: #495057;
font-size: 16px;
}
.mutation-summary {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.mutation-score {
font-weight: bold;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
}
.mutation-score-good {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.mutation-score-fair {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.mutation-score-poor {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.mutation-stats {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.mutation-stat {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.mutation-stat.killed {
background: #d4edda;
color: #155724;
}
.mutation-stat.survived {
background: #f8d7da;
color: #721c24;
}
.mutation-stat.error {
background: #f8d7da;
color: #721c24;
}
.mutation-stat.timeout {
background: #fff3cd;
color: #856404;
}
.mutation-details {
margin-top: 15px;
}
.mutation-group {
margin-bottom: 15px;
}
.mutation-group h5 {
margin: 0 0 10px 0;
font-size: 14px;
color: #495057;
}
.mutation-group summary {
cursor: pointer;
font-weight: 500;
color: #495057;
margin-bottom: 10px;
}
.mutation-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
margin: 5px 0;
border-radius: 4px;
font-size: 13px;
flex-wrap: wrap;
}
.mutation-item.survived {
background: #f8d7da;
border-left: 4px solid #dc3545;
}
.mutation-item.killed {
background: #d4edda;
border-left: 4px solid #28a745;
}
.mutation-item.error {
background: #f8d7da;
border-left: 4px solid #dc3545;
}
.mutation-type {
background: #6c757d;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
min-width: 80px;
text-align: center;
}
.mutation-description {
flex: 1;
color: #495057;
}
.mutation-tests {
color: #6c757d;
font-size: 12px;
}
.mutation-error {
color: #721c24;
font-style: italic;
flex: 1;
}
/* Mutation Dashboard Styles */
.mutations-overview {
margin-bottom: 30px;
}
.mutations-summary-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
border: 2px solid #e9ecef;
}
.summary-card.excellent {
border-color: #28a745;
background: #f8fff9;
}
.summary-card.good {
border-color: #6f42c1;
background: #f8f7ff;
}
.summary-card.fair {
border-color: #ffc107;
background: #fffef8;
}
.summary-card.poor {
b