UNPKG

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
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