jest-test-lineage-reporter 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3307 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { loadConfig } = require('./config');
4
+ const MutationTester = require('./MutationTester');
5
+
6
+ class TestCoverageReporter {
7
+ constructor(globalConfig, options) {
8
+ this.globalConfig = globalConfig;
9
+ this.options = options || {};
10
+ this.coverageData = {};
11
+ this.testCoverageMap = new Map(); // Map to store individual test coverage
12
+ this.currentTestFile = null;
13
+ this.baselineCoverage = null;
14
+
15
+ // Validate configuration
16
+ this.validateConfig();
17
+ }
18
+
19
+ validateConfig() {
20
+ if (!this.globalConfig) {
21
+ throw new Error('TestCoverageReporter: globalConfig is required');
22
+ }
23
+
24
+ // Set default options
25
+ this.options = {
26
+ outputFile: this.options.outputFile || 'test-lineage-report.html',
27
+ memoryLeakThreshold: this.options.memoryLeakThreshold || 50 * 1024, // 50KB
28
+ gcPressureThreshold: this.options.gcPressureThreshold || 5,
29
+ qualityThreshold: this.options.qualityThreshold || 60,
30
+ enableDebugLogging: this.options.enableDebugLogging || false,
31
+ ...this.options
32
+ };
33
+ }
34
+
35
+ // This method is called after a single test file (suite) has completed.
36
+ async onTestResult(test, testResult, _aggregatedResult) {
37
+ const testFilePath = test.path;
38
+
39
+ // Store test file path for later use
40
+ this.currentTestFile = testFilePath;
41
+
42
+ // Always process fallback coverage for now, but mark for potential precise data
43
+ const coverage = testResult.coverage;
44
+ if (!coverage) {
45
+ return;
46
+ }
47
+
48
+ // Process each individual test result (fallback mode)
49
+ testResult.testResults.forEach((testCase, index) => {
50
+ if (testCase.status === 'passed') {
51
+ this.processIndividualTestCoverage(testCase, coverage, testFilePath, index);
52
+ }
53
+ });
54
+ }
55
+
56
+ processLineageResults(lineageResults, testFilePath) {
57
+ console.log('🎯 Processing precise lineage tracking results...');
58
+
59
+ // lineageResults format: { filePath: { lineNumber: [testInfo, ...] } }
60
+ for (const filePath in lineageResults) {
61
+ // Skip test files - we only want to track coverage of source files
62
+ if (filePath.includes('__tests__') || filePath.includes('.test.') || filePath.includes('.spec.')) {
63
+ continue;
64
+ }
65
+
66
+ // Initialize the data structure for this file if it doesn't exist
67
+ if (!this.coverageData[filePath]) {
68
+ this.coverageData[filePath] = {};
69
+ }
70
+
71
+ for (const lineNumber in lineageResults[filePath]) {
72
+ const testInfos = lineageResults[filePath][lineNumber];
73
+
74
+ // Convert to our expected format
75
+ const processedTests = testInfos.map(testInfo => ({
76
+ name: testInfo.testName,
77
+ file: path.basename(testInfo.testFile || testFilePath),
78
+ fullPath: testInfo.testFile || testFilePath,
79
+ executionCount: testInfo.executionCount || 1,
80
+ timestamp: testInfo.timestamp || Date.now(),
81
+ type: 'precise' // Mark as precise tracking
82
+ }));
83
+
84
+ this.coverageData[filePath][lineNumber] = processedTests;
85
+ }
86
+ }
87
+ }
88
+
89
+ processIndividualTestCoverage(testCase, coverage, testFilePath, _testIndex) {
90
+ const testName = testCase.fullName;
91
+
92
+ // Since Jest doesn't provide per-test coverage, we'll use heuristics
93
+ // to estimate which tests likely covered which lines
94
+
95
+ for (const filePath in coverage) {
96
+ // Skip test files
97
+ if (filePath.includes('__tests__') || filePath.includes('.test.') || filePath.includes('.spec.')) {
98
+ continue;
99
+ }
100
+
101
+ const fileCoverage = coverage[filePath];
102
+ const statementMap = fileCoverage.statementMap;
103
+ const statements = fileCoverage.s;
104
+
105
+ // Initialize the data structure for this file if it doesn't exist
106
+ if (!this.coverageData[filePath]) {
107
+ this.coverageData[filePath] = {};
108
+ }
109
+
110
+ // Analyze which lines were covered
111
+ for (const statementId in statements) {
112
+ if (statements[statementId] > 0) {
113
+ const lineNumber = String(statementMap[statementId].start.line);
114
+
115
+ if (!this.coverageData[filePath][lineNumber]) {
116
+ this.coverageData[filePath][lineNumber] = [];
117
+ }
118
+
119
+ // Use heuristics to determine if this test likely covered this line
120
+ if (this.isTestLikelyCoveringLine(testCase, filePath, lineNumber, fileCoverage)) {
121
+ // Add test with more detailed information
122
+ const testInfo = {
123
+ name: testName,
124
+ file: path.basename(testFilePath),
125
+ fullPath: testFilePath,
126
+ duration: testCase.duration || 0,
127
+ confidence: this.calculateConfidence(testCase, filePath, lineNumber)
128
+ };
129
+
130
+ this.coverageData[filePath][lineNumber].push(testInfo);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ isTestLikelyCoveringLine(testCase, _filePath, _lineNumber, _fileCoverage) {
138
+ // Heuristic 1: If test name mentions the function/file being tested
139
+ const _testName = testCase.fullName.toLowerCase();
140
+
141
+ // Simplified heuristic - for now, include all tests
142
+ // TODO: Implement more sophisticated heuristics
143
+
144
+ // Heuristic 3: If it's a simple file with few tests, assume all tests cover most lines
145
+ // This is a fallback for when we can't determine specific coverage
146
+ return true; // For now, include all tests (we'll refine this)
147
+ }
148
+
149
+ calculateConfidence(_testCase, _filePath, _lineNumber) {
150
+ // Simplified confidence calculation
151
+ // TODO: Implement more sophisticated confidence scoring
152
+ return 75; // Default confidence
153
+ }
154
+
155
+ extractCodeKeywords(sourceCode) {
156
+ // Extract function names, variable names, etc. from source code line
157
+ const keywords = [];
158
+
159
+ // Match function names: function name() or name: function() or const name =
160
+ const functionMatches = sourceCode.match(/(?:function\s+(\w+)|(\w+)\s*[:=]\s*(?:function|\()|(?:const|let|var)\s+(\w+))/g);
161
+ if (functionMatches) {
162
+ functionMatches.forEach(match => {
163
+ const nameMatch = match.match(/(\w+)/);
164
+ if (nameMatch) keywords.push(nameMatch[1]);
165
+ });
166
+ }
167
+
168
+ // Match method calls: object.method()
169
+ const methodMatches = sourceCode.match(/(\w+)\s*\(/g);
170
+ if (methodMatches) {
171
+ methodMatches.forEach(match => {
172
+ const nameMatch = match.match(/(\w+)/);
173
+ if (nameMatch) keywords.push(nameMatch[1]);
174
+ });
175
+ }
176
+
177
+ return keywords;
178
+ }
179
+
180
+ getSourceCodeLine(filePath, lineNumber) {
181
+ try {
182
+ const sourceCode = fs.readFileSync(filePath, 'utf8');
183
+ const lines = sourceCode.split('\n');
184
+ return lines[lineNumber - 1] || '';
185
+ } catch (error) {
186
+ return '';
187
+ }
188
+ }
189
+
190
+ // This method is called after all tests in the entire run have completed.
191
+ async onRunComplete(_contexts, _results) {
192
+ // Try to get precise tracking data before generating reports
193
+ this.tryGetPreciseTrackingData();
194
+
195
+ this.generateReport();
196
+ await this.generateHtmlReport();
197
+
198
+ // Run mutation testing if enabled
199
+ await this.runMutationTestingIfEnabled();
200
+ }
201
+
202
+ /**
203
+ * Run mutation testing if enabled in configuration
204
+ */
205
+ async runMutationTestingIfEnabled() {
206
+ const config = loadConfig(this.options);
207
+
208
+ if (config.enableMutationTesting) {
209
+ console.log('\n🧬 Mutation testing enabled, starting mutation analysis...');
210
+
211
+ let mutationTester = null;
212
+ try {
213
+ mutationTester = new MutationTester(config);
214
+
215
+ // Pass the current coverage data directly instead of loading from file
216
+ const lineageData = this.convertCoverageDataToLineageFormat();
217
+ if (!lineageData || Object.keys(lineageData).length === 0) {
218
+ console.log('⚠️ No lineage data available for mutation testing');
219
+ return;
220
+ }
221
+
222
+ // Set the lineage data directly
223
+ mutationTester.setLineageData(lineageData);
224
+
225
+ // Run mutation testing
226
+ const results = await mutationTester.runMutationTesting();
227
+
228
+ // Store results for HTML report integration
229
+ this.mutationResults = results;
230
+
231
+ // Regenerate HTML report with mutation data
232
+ await this.generateHtmlReport();
233
+
234
+ } catch (error) {
235
+ console.error('❌ Mutation testing failed:', error.message);
236
+ if (this.options.enableDebugLogging) {
237
+ console.error(error.stack);
238
+ }
239
+ } finally {
240
+ // Always cleanup, even if there was an error
241
+ if (mutationTester) {
242
+ try {
243
+ await mutationTester.cleanup();
244
+ } catch (cleanupError) {
245
+ console.error('❌ Error during mutation testing cleanup:', cleanupError.message);
246
+ // Try emergency cleanup as last resort
247
+ mutationTester.emergencyCleanup();
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ tryGetPreciseTrackingData() {
255
+ // Try to get data from global persistent data first (most reliable)
256
+ if (global.__LINEAGE_PERSISTENT_DATA__ && global.__LINEAGE_PERSISTENT_DATA__.length > 0) {
257
+ console.log('🎯 Found precise lineage tracking data from global persistent data! Replacing estimated data...');
258
+
259
+ // Clear existing coverage data and replace with precise data
260
+ this.coverageData = {};
261
+ this.processFileTrackingData(global.__LINEAGE_PERSISTENT_DATA__);
262
+ return true;
263
+ }
264
+
265
+ // Fallback to global function
266
+ if (global.__GET_LINEAGE_RESULTS__) {
267
+ const lineageResults = global.__GET_LINEAGE_RESULTS__();
268
+ if (Object.keys(lineageResults).length > 0) {
269
+ console.log('🎯 Found precise lineage tracking data from global function! Replacing estimated data...');
270
+
271
+ // Clear existing coverage data and replace with precise data
272
+ this.coverageData = {};
273
+ this.processLineageResults(lineageResults, 'precise-tracking');
274
+ return true;
275
+ }
276
+ }
277
+
278
+ // Last resort: try to read tracking data from file
279
+ const fileData = this.readTrackingDataFromFile();
280
+ if (fileData) {
281
+ console.log('🎯 Found precise lineage tracking data from file! Replacing estimated data...');
282
+
283
+ // Clear existing coverage data and replace with precise data
284
+ this.coverageData = {};
285
+ this.processFileTrackingData(fileData);
286
+ return true;
287
+ }
288
+
289
+ console.log('⚠️ No precise tracking data found, using estimated coverage');
290
+ return false;
291
+ }
292
+
293
+ /**
294
+ * Convert the current coverage data to the format expected by MutationTester
295
+ */
296
+ convertCoverageDataToLineageFormat() {
297
+ const lineageData = {};
298
+
299
+ // Iterate through all coverage data and convert to mutation testing format
300
+ // The actual structure is: this.coverageData[filePath][lineNumber] = [testInfo, ...]
301
+ for (const [filePath, fileData] of Object.entries(this.coverageData)) {
302
+ if (!fileData || typeof fileData !== 'object') continue;
303
+
304
+ for (const [lineNumber, tests] of Object.entries(fileData)) {
305
+ if (!Array.isArray(tests) || tests.length === 0) continue;
306
+
307
+ if (!lineageData[filePath]) {
308
+ lineageData[filePath] = {};
309
+ }
310
+
311
+ lineageData[filePath][lineNumber] = tests.map(test => ({
312
+ testName: test.name || test.testName || 'Unknown test',
313
+ testType: test.testType || test.type || 'it',
314
+ testFile: test.testFile || test.file || 'unknown',
315
+ executionCount: test.executionCount || 1,
316
+ }));
317
+ }
318
+ }
319
+
320
+ console.log(`🔄 Converted coverage data to lineage format: ${Object.keys(lineageData).length} files`);
321
+ Object.keys(lineageData).forEach(filePath => {
322
+ const lineCount = Object.keys(lineageData[filePath]).length;
323
+ console.log(` ${filePath}: ${lineCount} lines`);
324
+ });
325
+
326
+ return lineageData;
327
+ }
328
+
329
+ readTrackingDataFromFile() {
330
+ try {
331
+ const filePath = path.join(process.cwd(), '.jest-lineage-data.json');
332
+ if (fs.existsSync(filePath)) {
333
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
334
+ console.log(`📖 Read tracking data: ${data.tests.length} tests from file`);
335
+
336
+
337
+
338
+ return data.tests;
339
+ } else {
340
+ console.log(`⚠️ Tracking data file not found: ${filePath}`);
341
+ }
342
+ } catch (error) {
343
+ console.warn('Warning: Could not read tracking data from file:', error.message);
344
+ }
345
+ return null;
346
+ }
347
+
348
+ processFileTrackingData(testDataArray) {
349
+ if (!Array.isArray(testDataArray)) {
350
+ console.warn('⚠️ processFileTrackingData: testDataArray is not an array');
351
+ return;
352
+ }
353
+
354
+ console.log(`🔍 Processing ${testDataArray.length} test data entries`);
355
+
356
+ let processedFiles = 0;
357
+ let processedLines = 0;
358
+
359
+
360
+ testDataArray.forEach((testData, index) => {
361
+ try {
362
+ if (!testData || typeof testData !== 'object') {
363
+ console.warn(`⚠️ Skipping invalid test data at index ${index}:`, testData);
364
+ return;
365
+ }
366
+
367
+ if (!testData.coverage || typeof testData.coverage !== 'object') {
368
+ console.warn(`⚠️ Skipping test data with invalid coverage at index ${index}:`, testData.name);
369
+ return;
370
+ }
371
+
372
+ // testData.coverage is now a plain object, not a Map
373
+ Object.entries(testData.coverage).forEach(([key, count]) => {
374
+ try {
375
+ // Skip metadata, depth, and performance entries for now (process them separately)
376
+ if (key.includes(':meta') || key.includes(':depth') || key.includes(':performance')) {
377
+ return;
378
+ }
379
+
380
+ const parts = key.split(':');
381
+ if (parts.length < 2) {
382
+ console.warn(`⚠️ Invalid key format: ${key}`);
383
+ return;
384
+ }
385
+
386
+ const lineNumber = parts.pop(); // Last part is line number
387
+ const filePath = parts.join(':'); // Rejoin in case path contains colons
388
+
389
+ // Validate line number
390
+ if (isNaN(parseInt(lineNumber))) {
391
+ console.warn(`⚠️ Invalid line number: ${lineNumber} for file: ${filePath}`);
392
+ return;
393
+ }
394
+
395
+ // Skip test files and node_modules
396
+ if (filePath.includes('__tests__') ||
397
+ filePath.includes('.test.') ||
398
+ filePath.includes('.spec.') ||
399
+ filePath.includes('node_modules')) {
400
+ console.log(`🔍 DEBUG: Skipping test/node_modules file: ${filePath}`);
401
+ return;
402
+ }
403
+
404
+ //console.log(`🔍 DEBUG: Processing coverage for ${filePath}:${lineNumber} (count: ${count})`);
405
+ processedLines++;
406
+
407
+ // Get depth data for this line
408
+ const depthKey = `${filePath}:${lineNumber}:depth`;
409
+ const depthData = testData.coverage[depthKey] || { 1: count };
410
+
411
+ // Get metadata for this line
412
+ const metaKey = `${filePath}:${lineNumber}:meta`;
413
+ const metaData = testData.coverage[metaKey] || {};
414
+
415
+ // Get performance data for this line
416
+ const performanceKey = `${filePath}:${lineNumber}:performance`;
417
+ const performanceData = testData.coverage[performanceKey] || {
418
+ totalExecutions: count,
419
+ totalCpuTime: 0,
420
+ totalWallTime: 0,
421
+ totalMemoryDelta: 0,
422
+ minExecutionTime: 0,
423
+ maxExecutionTime: 0,
424
+ executionTimes: [],
425
+ cpuCycles: []
426
+ };
427
+
428
+ // Initialize the data structure for this file if it doesn't exist
429
+ if (!this.coverageData[filePath]) {
430
+ this.coverageData[filePath] = {};
431
+ }
432
+
433
+ if (!this.coverageData[filePath][lineNumber]) {
434
+ this.coverageData[filePath][lineNumber] = [];
435
+ }
436
+
437
+ // Add test with precise tracking information including depth, performance, and quality
438
+ const testInfo = {
439
+ name: testData.name || 'Unknown test',
440
+ file: testData.testFile || 'unknown-test-file',
441
+ fullPath: testData.testFile || 'unknown-test-file',
442
+ executionCount: typeof count === 'number' ? count : 1,
443
+ duration: testData.duration || 0,
444
+ type: 'precise', // Mark as precise tracking
445
+ depthData: depthData, // Call depth information
446
+ minDepth: metaData.minDepth || 1,
447
+ maxDepth: metaData.maxDepth || 1,
448
+ nodeType: metaData.nodeType || 'unknown',
449
+ performance: {
450
+ totalCpuTime: performanceData.totalCpuTime || 0,
451
+ totalWallTime: performanceData.totalWallTime || 0,
452
+ avgCpuTime: performanceData.totalExecutions > 0 ? performanceData.totalCpuTime / performanceData.totalExecutions : 0,
453
+ avgWallTime: performanceData.totalExecutions > 0 ? performanceData.totalWallTime / performanceData.totalExecutions : 0,
454
+ totalMemoryDelta: performanceData.totalMemoryDelta || 0,
455
+ minExecutionTime: performanceData.minExecutionTime || 0,
456
+ maxExecutionTime: performanceData.maxExecutionTime || 0,
457
+ totalCpuCycles: performanceData.cpuCycles ? performanceData.cpuCycles.reduce((sum, cycles) => sum + cycles, 0) : 0,
458
+ avgCpuCycles: performanceData.cpuCycles && performanceData.cpuCycles.length > 0 ?
459
+ performanceData.cpuCycles.reduce((sum, cycles) => sum + cycles, 0) / performanceData.cpuCycles.length : 0,
460
+ performanceVariance: performanceData.performanceVariance || 0,
461
+ performanceStdDev: performanceData.performanceStdDev || 0,
462
+ performanceP95: performanceData.performanceP95 || 0,
463
+ performanceP99: performanceData.performanceP99 || 0,
464
+ slowExecutions: performanceData.slowExecutions || 0,
465
+ fastExecutions: performanceData.fastExecutions || 0,
466
+ memoryLeaks: performanceData.memoryLeaks || 0,
467
+ gcPressure: performanceData.gcPressure || 0
468
+ },
469
+ quality: testData.qualityMetrics || {
470
+ assertions: 0,
471
+ asyncOperations: 0,
472
+ mockUsage: 0,
473
+ errorHandling: 0,
474
+ edgeCases: 0,
475
+ complexity: 0,
476
+ maintainability: 50,
477
+ reliability: 50,
478
+ testSmells: [],
479
+ codePatterns: [],
480
+ isolationScore: 100,
481
+ testLength: 0
482
+ }
483
+ };
484
+
485
+ this.coverageData[filePath][lineNumber].push(testInfo);
486
+
487
+ // console.log(`🔍 DEBUG: Added coverage for "${filePath}":${lineNumber} -> ${testData.name} (${count} executions)`);
488
+
489
+ } catch (entryError) {
490
+ console.warn(`⚠️ Error processing coverage entry ${key}:`, entryError.message);
491
+ }
492
+ });
493
+ } catch (testError) {
494
+ console.warn(`⚠️ Error processing test data at index ${index}:`, testError.message);
495
+ }
496
+ });
497
+
498
+ console.log(`✅ Processed tracking data for ${Object.keys(this.coverageData).length} files (${processedLines} lines processed)`);
499
+
500
+ // Debug: Show what files were processed
501
+ Object.keys(this.coverageData).forEach(filePath => {
502
+ const lineCount = Object.keys(this.coverageData[filePath]).length;
503
+ console.log(` 📁 ${filePath}: ${lineCount} lines`);
504
+ });
505
+ }
506
+
507
+ generateReport() {
508
+ console.log('\n--- Jest Test Lineage Reporter: Line-by-Line Test Coverage ---');
509
+
510
+ // Generate test quality summary first
511
+ this.generateTestQualitySummary();
512
+
513
+ for (const filePath in this.coverageData) {
514
+ const lineCoverage = this.coverageData[filePath];
515
+
516
+ const lineNumbers = Object.keys(lineCoverage).sort((a, b) => parseInt(a) - parseInt(b));
517
+
518
+ if (lineNumbers.length === 0) {
519
+ continue;
520
+ }
521
+
522
+ for (const line of lineNumbers) {
523
+ const testInfos = lineCoverage[line];
524
+ const uniqueTests = this.deduplicateTests(testInfos);
525
+ uniqueTests.forEach(testInfo => {
526
+ const testName = typeof testInfo === 'string' ? testInfo : testInfo.name;
527
+ const testFile = typeof testInfo === 'object' ? testInfo.file : 'Unknown';
528
+ const executionCount = typeof testInfo === 'object' ? testInfo.executionCount : 1;
529
+ const trackingType = typeof testInfo === 'object' && testInfo.type === 'precise' ? '✅ PRECISE' : '⚠️ ESTIMATED';
530
+
531
+ // Add depth information for precise tracking
532
+ let depthInfo = '';
533
+ if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.depthData) {
534
+ const depths = Object.keys(testInfo.depthData).map(d => parseInt(d)).sort((a, b) => a - b);
535
+ if (depths.length === 1) {
536
+ depthInfo = `, depth ${depths[0]}`;
537
+ } else {
538
+ depthInfo = `, depths ${depths.join(',')}`;
539
+ }
540
+ }
541
+
542
+ // Add performance information for precise tracking
543
+ let performanceInfo = '';
544
+ if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.performance) {
545
+ const perf = testInfo.performance;
546
+ if (perf.avgCpuCycles > 0) {
547
+ const cycles = perf.avgCpuCycles > 1000000 ?
548
+ `${(perf.avgCpuCycles / 1000000).toFixed(1)}M` :
549
+ `${Math.round(perf.avgCpuCycles)}`;
550
+ const cpuTime = perf.avgCpuTime > 1000 ?
551
+ `${(perf.avgCpuTime / 1000).toFixed(2)}ms` :
552
+ `${perf.avgCpuTime.toFixed(1)}μs`;
553
+
554
+ // Add memory information
555
+ let memoryInfo = '';
556
+ if (perf.totalMemoryDelta !== 0) {
557
+ const memoryMB = Math.abs(perf.totalMemoryDelta) / (1024 * 1024);
558
+ const memorySign = perf.totalMemoryDelta > 0 ? '+' : '-';
559
+ if (memoryMB > 1) {
560
+ memoryInfo = `, ${memorySign}${memoryMB.toFixed(1)}MB`;
561
+ } else {
562
+ const memoryKB = Math.abs(perf.totalMemoryDelta) / 1024;
563
+ memoryInfo = `, ${memorySign}${memoryKB.toFixed(1)}KB`;
564
+ }
565
+ }
566
+
567
+ // Add performance alerts
568
+ let alerts = '';
569
+ const memoryMB = Math.abs(perf.totalMemoryDelta || 0) / (1024 * 1024);
570
+ if (memoryMB > 1) alerts += ` 🚨LEAK`;
571
+ if (perf.gcPressure > 5) alerts += ` 🗑️GC`;
572
+ if (perf.slowExecutions > perf.fastExecutions) alerts += ` 🐌SLOW`;
573
+
574
+ performanceInfo = `, ${cycles} cycles, ${cpuTime}${memoryInfo}${alerts}`;
575
+ }
576
+ }
577
+
578
+ // Add quality information for precise tracking
579
+ let qualityInfo = '';
580
+ if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.quality) {
581
+ const quality = testInfo.quality;
582
+ const qualityBadges = [];
583
+
584
+ // Quality score with explanation
585
+ let qualityScore = '';
586
+ if (quality.maintainability >= 80) {
587
+ qualityScore = '🏆 High Quality';
588
+ } else if (quality.maintainability >= 60) {
589
+ qualityScore = '✅ Good Quality';
590
+ } else if (quality.maintainability >= 40) {
591
+ qualityScore = '⚠️ Fair Quality';
592
+ } else {
593
+ qualityScore = '❌ Poor Quality';
594
+ }
595
+ qualityBadges.push(`${qualityScore} (${Math.round(quality.maintainability)}%)`);
596
+
597
+ // Reliability score
598
+ if (quality.reliability >= 80) qualityBadges.push(`🛡️ Reliable (${Math.round(quality.reliability)}%)`);
599
+ else if (quality.reliability >= 60) qualityBadges.push(`🔒 Stable (${Math.round(quality.reliability)}%)`);
600
+ else qualityBadges.push(`⚠️ Fragile (${Math.round(quality.reliability)}%)`);
601
+
602
+ // Test smells with details
603
+ if (quality.testSmells && quality.testSmells.length > 0) {
604
+ qualityBadges.push(`🚨 ${quality.testSmells.length} Smells: ${quality.testSmells.join(', ')}`);
605
+ }
606
+
607
+ // Positive indicators
608
+ if (quality.assertions > 5) qualityBadges.push(`🎯 ${quality.assertions} Assertions`);
609
+ if (quality.errorHandling > 0) qualityBadges.push(`🔒 ${quality.errorHandling} Error Tests`);
610
+ if (quality.edgeCases > 3) qualityBadges.push(`🎪 ${quality.edgeCases} Edge Cases`);
611
+
612
+ if (qualityBadges.length > 0) {
613
+ qualityInfo = ` [${qualityBadges.join(', ')}]`;
614
+ }
615
+ }
616
+
617
+ // console.log(` - "${testName}" (${testFile}, ${executionCount} executions${depthInfo}${performanceInfo}) ${trackingType}${qualityInfo}`);
618
+ });
619
+ }
620
+ }
621
+
622
+ console.log('\n--- Report End ---');
623
+ }
624
+
625
+ async generateHtmlReport() {
626
+ console.log('\n📄 Generating HTML coverage report...');
627
+
628
+ let html = this.generateHtmlTemplate();
629
+
630
+ // Validate coverage data before processing
631
+ const validFiles = this.validateCoverageData();
632
+
633
+ for (const filePath of validFiles) {
634
+ try {
635
+ html += await this.generateCodeTreeSection(filePath);
636
+ } catch (error) {
637
+ console.error(`❌ Error generating section for ${filePath}:`, error.message);
638
+ html += this.generateErrorSection(filePath, error.message);
639
+ }
640
+ }
641
+
642
+ html += this.generateHtmlFooter();
643
+
644
+ // Write HTML file
645
+ const outputPath = path.join(process.cwd(), 'test-lineage-report.html');
646
+ fs.writeFileSync(outputPath, html, 'utf8');
647
+
648
+ console.log(`✅ HTML report generated: ${outputPath}`);
649
+ console.log('🌐 Open the file in your browser to view the visual coverage report');
650
+ }
651
+
652
+ validateCoverageData() {
653
+ const validFiles = [];
654
+
655
+ for (const filePath in this.coverageData) {
656
+ const fileData = this.coverageData[filePath];
657
+
658
+ if (!fileData) {
659
+ console.warn(`⚠️ Skipping ${filePath}: No coverage data`);
660
+ continue;
661
+ }
662
+
663
+ if (typeof fileData !== 'object') {
664
+ console.warn(`⚠️ Skipping ${filePath}: Invalid data type (${typeof fileData})`);
665
+ continue;
666
+ }
667
+
668
+ if (Object.keys(fileData).length === 0) {
669
+ console.warn(`⚠️ Skipping ${filePath}: Empty coverage data`);
670
+ continue;
671
+ }
672
+
673
+ // Validate that the data structure is correct
674
+ let hasValidData = false;
675
+ for (const lineNumber in fileData) {
676
+ const lineData = fileData[lineNumber];
677
+ if (Array.isArray(lineData) && lineData.length > 0) {
678
+ hasValidData = true;
679
+ break;
680
+ }
681
+ }
682
+
683
+ if (!hasValidData) {
684
+ console.warn(`⚠️ Skipping ${filePath}: No valid line data found`);
685
+ continue;
686
+ }
687
+
688
+ validFiles.push(filePath);
689
+ }
690
+
691
+ // console.log(`✅ Validated ${validFiles.length} files for HTML report`);
692
+ return validFiles;
693
+ }
694
+
695
+ findMatchingCoveragePath(targetPath) {
696
+ const targetBasename = path.basename(targetPath);
697
+
698
+ // Strategy 1: Find exact basename match
699
+ for (const coveragePath of Object.keys(this.coverageData)) {
700
+ if (path.basename(coveragePath) === targetBasename) {
701
+ return coveragePath;
702
+ }
703
+ }
704
+
705
+ // Strategy 2: Convert both paths to relative from package.json and compare
706
+ const projectRoot = this.findProjectRoot(process.cwd());
707
+ const targetRelative = this.makeRelativeToProject(targetPath, projectRoot);
708
+
709
+ for (const coveragePath of Object.keys(this.coverageData)) {
710
+ const coverageRelative = this.makeRelativeToProject(coveragePath, projectRoot);
711
+ if (targetRelative === coverageRelative) {
712
+ return coveragePath;
713
+ }
714
+ }
715
+
716
+ // Strategy 3: Find path that contains the target filename
717
+ for (const coveragePath of Object.keys(this.coverageData)) {
718
+ if (coveragePath.includes(targetBasename)) {
719
+ return coveragePath;
720
+ }
721
+ }
722
+
723
+ // Strategy 4: Find path where target contains the coverage filename
724
+ for (const coveragePath of Object.keys(this.coverageData)) {
725
+ const coverageBasename = path.basename(coveragePath);
726
+ if (targetPath.includes(coverageBasename)) {
727
+ return coveragePath;
728
+ }
729
+ }
730
+
731
+ // Strategy 5: Fuzzy matching - remove common path differences
732
+ const normalizedTarget = this.normalizePath(targetPath);
733
+ for (const coveragePath of Object.keys(this.coverageData)) {
734
+ const normalizedCoverage = this.normalizePath(coveragePath);
735
+ if (normalizedTarget === normalizedCoverage) {
736
+ return coveragePath;
737
+ }
738
+ }
739
+
740
+ return null;
741
+ }
742
+
743
+ findProjectRoot(startPath) {
744
+ let currentDir = startPath;
745
+ const root = path.parse(currentDir).root;
746
+
747
+ while (currentDir !== root) {
748
+ const packageJsonPath = path.join(currentDir, 'package.json');
749
+ if (fs.existsSync(packageJsonPath)) {
750
+ return currentDir;
751
+ }
752
+ currentDir = path.dirname(currentDir);
753
+ }
754
+
755
+ // Fallback to current working directory if no package.json found
756
+ return process.cwd();
757
+ }
758
+
759
+ makeRelativeToProject(filePath, projectRoot) {
760
+ // If path is absolute and starts with project root, make it relative
761
+ if (path.isAbsolute(filePath) && filePath.startsWith(projectRoot)) {
762
+ return path.relative(projectRoot, filePath);
763
+ }
764
+
765
+ // If path is already relative, return as-is
766
+ if (!path.isAbsolute(filePath)) {
767
+ return filePath;
768
+ }
769
+
770
+ // Try to find common base with project root
771
+ const relativePath = path.relative(projectRoot, filePath);
772
+ if (!relativePath.startsWith('..')) {
773
+ return relativePath;
774
+ }
775
+
776
+ // Fallback to original path
777
+ return filePath;
778
+ }
779
+
780
+ normalizePath(filePath) {
781
+ // Remove common path variations that might cause mismatches
782
+ return filePath
783
+ .replace(/\/services\//g, '/') // Remove /services/ directory
784
+ .replace(/\/src\//g, '/') // Remove /src/ directory
785
+ .replace(/\/lib\//g, '/') // Remove /lib/ directory
786
+ .replace(/\/app\//g, '/') // Remove /app/ directory
787
+ .replace(/\/server\//g, '/') // Remove /server/ directory
788
+ .replace(/\/client\//g, '/') // Remove /client/ directory
789
+ .replace(/\/+/g, '/') // Replace multiple slashes with single
790
+ .toLowerCase(); // Case insensitive matching
791
+ }
792
+
793
+ generateErrorSection(filePath, errorMessage) {
794
+ return `<div class="file-section">
795
+ <div class="file-header">❌ Error processing ${path.basename(filePath)}</div>
796
+ <div class="error">
797
+ <p><strong>File:</strong> ${filePath}</p>
798
+ <p><strong>Error:</strong> ${errorMessage}</p>
799
+ <p><strong>Suggestion:</strong> Check if the file exists and has valid coverage data.</p>
800
+ </div>
801
+ </div>`;
802
+ }
803
+
804
+ generateHtmlTemplate() {
805
+ return `<!DOCTYPE html>
806
+ <html lang="en">
807
+ <head>
808
+ <meta charset="UTF-8">
809
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
810
+ <title>Jest Test Lineage Report</title>
811
+ <style>
812
+ body {
813
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
814
+ margin: 0;
815
+ padding: 20px;
816
+ background-color: #f5f5f5;
817
+ line-height: 1.6;
818
+ }
819
+ .header {
820
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
821
+ color: white;
822
+ padding: 30px;
823
+ border-radius: 10px;
824
+ margin-bottom: 20px;
825
+ text-align: center;
826
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
827
+ }
828
+ .navigation {
829
+ background: white;
830
+ padding: 20px;
831
+ margin-bottom: 20px;
832
+ border-radius: 10px;
833
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
834
+ display: flex;
835
+ justify-content: space-between;
836
+ align-items: center;
837
+ flex-wrap: wrap;
838
+ gap: 15px;
839
+ }
840
+ .nav-buttons {
841
+ display: flex;
842
+ gap: 10px;
843
+ flex-wrap: wrap;
844
+ }
845
+ .nav-btn, .action-btn {
846
+ padding: 10px 20px;
847
+ border: none;
848
+ border-radius: 6px;
849
+ cursor: pointer;
850
+ font-size: 14px;
851
+ font-weight: 500;
852
+ transition: all 0.2s ease;
853
+ }
854
+ .nav-btn {
855
+ background: #e9ecef;
856
+ color: #495057;
857
+ }
858
+ .nav-btn.active {
859
+ background: #007bff;
860
+ color: white;
861
+ }
862
+ .nav-btn:hover {
863
+ background: #007bff;
864
+ color: white;
865
+ }
866
+ .action-btn {
867
+ background: #28a745;
868
+ color: white;
869
+ }
870
+ .action-btn:hover {
871
+ background: #218838;
872
+ }
873
+ .sort-controls {
874
+ display: flex;
875
+ align-items: center;
876
+ gap: 10px;
877
+ }
878
+ .sort-controls label {
879
+ font-weight: 500;
880
+ color: #495057;
881
+ }
882
+ .sort-controls select {
883
+ padding: 8px 12px;
884
+ border: 1px solid #ced4da;
885
+ border-radius: 4px;
886
+ background: white;
887
+ font-size: 14px;
888
+ }
889
+ .header h1 {
890
+ margin: 0;
891
+ font-size: 2.5em;
892
+ font-weight: 300;
893
+ }
894
+ .header p {
895
+ margin: 10px 0 0 0;
896
+ opacity: 0.9;
897
+ font-size: 1.1em;
898
+ }
899
+ .file-section {
900
+ background: white;
901
+ margin-bottom: 30px;
902
+ border-radius: 10px;
903
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
904
+ overflow: hidden;
905
+ }
906
+ .file-header {
907
+ background: #2c3e50;
908
+ color: white;
909
+ padding: 20px;
910
+ font-size: 1.2em;
911
+ font-weight: 500;
912
+ cursor: pointer;
913
+ transition: background 0.2s ease;
914
+ display: flex;
915
+ justify-content: space-between;
916
+ align-items: center;
917
+ }
918
+ .file-header:hover {
919
+ background: #34495e;
920
+ }
921
+ .file-header .expand-icon {
922
+ font-size: 18px;
923
+ transition: transform 0.3s ease;
924
+ }
925
+ .file-header.expanded .expand-icon {
926
+ transform: rotate(90deg);
927
+ }
928
+ .file-content {
929
+ display: none;
930
+ }
931
+ .file-content.expanded {
932
+ display: block;
933
+ }
934
+ .file-path {
935
+ font-family: 'Courier New', monospace;
936
+ opacity: 0.8;
937
+ font-size: 0.9em;
938
+ }
939
+ .code-container {
940
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
941
+ font-size: 14px;
942
+ line-height: 1.5;
943
+ background: #f8f9fa;
944
+ }
945
+ .code-line {
946
+ display: flex;
947
+ border-bottom: 1px solid #e9ecef;
948
+ transition: background-color 0.2s ease;
949
+ }
950
+ .code-line:hover {
951
+ background-color: #e3f2fd;
952
+ }
953
+ .line-number {
954
+ background: #6c757d;
955
+ color: white;
956
+ padding: 8px 12px;
957
+ min-width: 60px;
958
+ text-align: right;
959
+ font-weight: bold;
960
+ user-select: none;
961
+ }
962
+ .line-covered {
963
+ background: #28a745;
964
+ }
965
+ .line-uncovered {
966
+ background: #dc3545;
967
+ }
968
+ .line-content {
969
+ flex: 1;
970
+ padding: 8px 16px;
971
+ white-space: pre;
972
+ overflow-x: auto;
973
+ }
974
+ .coverage-indicator {
975
+ background: #17a2b8;
976
+ color: white;
977
+ padding: 8px 12px;
978
+ min-width: 80px;
979
+ text-align: center;
980
+ font-size: 12px;
981
+ cursor: pointer;
982
+ user-select: none;
983
+ transition: background-color 0.2s ease;
984
+ }
985
+ .coverage-indicator:hover {
986
+ background: #138496;
987
+ }
988
+ .coverage-details {
989
+ display: none;
990
+ background: #fff3cd;
991
+ border-left: 4px solid #ffc107;
992
+ margin: 0;
993
+ padding: 15px;
994
+ border-bottom: 1px solid #e9ecef;
995
+ }
996
+ .coverage-details.expanded {
997
+ display: block;
998
+ }
999
+ .test-badge {
1000
+ display: inline-block;
1001
+ background: #007bff;
1002
+ color: white;
1003
+ padding: 4px 8px;
1004
+ margin: 2px;
1005
+ border-radius: 12px;
1006
+ font-size: 11px;
1007
+ transition: all 0.2s ease;
1008
+ }
1009
+ .test-badge:hover {
1010
+ background: #0056b3;
1011
+ transform: translateY(-1px);
1012
+ }
1013
+ .depth-badge {
1014
+ display: inline-block;
1015
+ background: #6c757d;
1016
+ color: white;
1017
+ padding: 2px 6px;
1018
+ margin-left: 4px;
1019
+ border-radius: 8px;
1020
+ font-size: 9px;
1021
+ font-weight: bold;
1022
+ vertical-align: middle;
1023
+ }
1024
+ .depth-badge.depth-1 {
1025
+ background: #28a745; /* Green for direct calls */
1026
+ }
1027
+ .depth-badge.depth-2 {
1028
+ background: #ffc107; /* Yellow for one level deep */
1029
+ color: #212529;
1030
+ }
1031
+ .depth-badge.depth-3 {
1032
+ background: #fd7e14; /* Orange for two levels deep */
1033
+ }
1034
+ .depth-badge.depth-4,
1035
+ .depth-badge.depth-5,
1036
+ .depth-badge.depth-6,
1037
+ .depth-badge.depth-7,
1038
+ .depth-badge.depth-8,
1039
+ .depth-badge.depth-9,
1040
+ .depth-badge.depth-10 {
1041
+ background: #dc3545; /* Red for very deep calls */
1042
+ }
1043
+ .lines-analysis {
1044
+ background: white;
1045
+ padding: 30px;
1046
+ border-radius: 10px;
1047
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
1048
+ margin-bottom: 30px;
1049
+ }
1050
+ .lines-table {
1051
+ width: 100%;
1052
+ border-collapse: collapse;
1053
+ margin-top: 20px;
1054
+ }
1055
+ .lines-table th,
1056
+ .lines-table td {
1057
+ padding: 12px;
1058
+ text-align: left;
1059
+ border-bottom: 1px solid #e9ecef;
1060
+ }
1061
+ .lines-table th {
1062
+ background: #f8f9fa;
1063
+ font-weight: 600;
1064
+ color: #495057;
1065
+ position: sticky;
1066
+ top: 0;
1067
+ }
1068
+ .lines-table tr:hover {
1069
+ background: #f8f9fa;
1070
+ }
1071
+ .file-name {
1072
+ font-family: 'Courier New', monospace;
1073
+ font-weight: 500;
1074
+ color: #007bff;
1075
+ }
1076
+ .line-number {
1077
+ font-family: 'Courier New', monospace;
1078
+ font-weight: bold;
1079
+ color: #6c757d;
1080
+ text-align: center;
1081
+ }
1082
+ .code-preview {
1083
+ font-family: 'Courier New', monospace;
1084
+ font-size: 12px;
1085
+ color: #495057;
1086
+ max-width: 300px;
1087
+ overflow: hidden;
1088
+ text-overflow: ellipsis;
1089
+ white-space: nowrap;
1090
+ }
1091
+ .test-count,
1092
+ .execution-count,
1093
+ .cpu-cycles,
1094
+ .cpu-time,
1095
+ .wall-time {
1096
+ text-align: center;
1097
+ font-weight: 600;
1098
+ }
1099
+ .cpu-cycles {
1100
+ color: #dc3545;
1101
+ font-family: 'Courier New', monospace;
1102
+ }
1103
+ .cpu-time {
1104
+ color: #fd7e14;
1105
+ font-family: 'Courier New', monospace;
1106
+ }
1107
+ .wall-time {
1108
+ color: #6f42c1;
1109
+ font-family: 'Courier New', monospace;
1110
+ }
1111
+ .max-depth,
1112
+ .depth-range,
1113
+ .quality-score,
1114
+ .reliability-score,
1115
+ .test-smells {
1116
+ text-align: center;
1117
+ }
1118
+ .quality-excellent {
1119
+ color: #28a745;
1120
+ font-weight: bold;
1121
+ }
1122
+ .quality-good {
1123
+ color: #6f42c1;
1124
+ font-weight: bold;
1125
+ }
1126
+ .quality-fair {
1127
+ color: #ffc107;
1128
+ font-weight: bold;
1129
+ }
1130
+ .quality-poor {
1131
+ color: #dc3545;
1132
+ font-weight: bold;
1133
+ }
1134
+ .smells-none {
1135
+ color: #28a745;
1136
+ font-weight: bold;
1137
+ }
1138
+ .smells-few {
1139
+ color: #ffc107;
1140
+ font-weight: bold;
1141
+ cursor: help;
1142
+ }
1143
+ .smells-many {
1144
+ color: #dc3545;
1145
+ font-weight: bold;
1146
+ cursor: help;
1147
+ }
1148
+ .smells-few[title]:hover,
1149
+ .smells-many[title]:hover {
1150
+ text-decoration: underline;
1151
+ }
1152
+ .performance-analysis,
1153
+ .quality-analysis,
1154
+ .mutations-analysis {
1155
+ background: white;
1156
+ padding: 30px;
1157
+ border-radius: 10px;
1158
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
1159
+ margin-bottom: 30px;
1160
+ }
1161
+ .performance-summary,
1162
+ .quality-summary {
1163
+ margin-bottom: 30px;
1164
+ }
1165
+ .perf-stats,
1166
+ .quality-stats {
1167
+ display: flex;
1168
+ gap: 20px;
1169
+ margin: 20px 0;
1170
+ flex-wrap: wrap;
1171
+ }
1172
+ .perf-stat,
1173
+ .quality-stat {
1174
+ background: #f8f9fa;
1175
+ padding: 20px;
1176
+ border-radius: 8px;
1177
+ text-align: center;
1178
+ min-width: 120px;
1179
+ }
1180
+ .perf-number,
1181
+ .quality-number {
1182
+ font-size: 24px;
1183
+ font-weight: bold;
1184
+ color: #007bff;
1185
+ }
1186
+ .perf-label,
1187
+ .quality-label {
1188
+ font-size: 12px;
1189
+ color: #6c757d;
1190
+ margin-top: 5px;
1191
+ }
1192
+ .performance-tables,
1193
+ .quality-tables {
1194
+ display: flex;
1195
+ gap: 30px;
1196
+ flex-wrap: wrap;
1197
+ }
1198
+ .perf-table-container,
1199
+ .quality-table-container {
1200
+ flex: 1;
1201
+ min-width: 400px;
1202
+ }
1203
+ .perf-table,
1204
+ .quality-table {
1205
+ width: 100%;
1206
+ border-collapse: collapse;
1207
+ margin-top: 10px;
1208
+ }
1209
+ .perf-table th,
1210
+ .perf-table td,
1211
+ .quality-table th,
1212
+ .quality-table td {
1213
+ padding: 8px 12px;
1214
+ text-align: left;
1215
+ border-bottom: 1px solid #e9ecef;
1216
+ }
1217
+ .perf-table th,
1218
+ .quality-table th {
1219
+ background: #f8f9fa;
1220
+ font-weight: 600;
1221
+ }
1222
+ .memory-usage {
1223
+ color: #6f42c1;
1224
+ font-weight: bold;
1225
+ }
1226
+ .memory-leaks {
1227
+ color: #dc3545;
1228
+ font-weight: bold;
1229
+ }
1230
+ .quality-distribution {
1231
+ margin: 20px 0;
1232
+ }
1233
+ .quality-bars {
1234
+ margin: 15px 0;
1235
+ }
1236
+ .quality-bar {
1237
+ margin: 10px 0;
1238
+ }
1239
+ .bar {
1240
+ width: 100%;
1241
+ height: 20px;
1242
+ background: #e9ecef;
1243
+ border-radius: 10px;
1244
+ overflow: hidden;
1245
+ margin: 5px 0;
1246
+ }
1247
+ .fill {
1248
+ height: 100%;
1249
+ transition: width 0.3s ease;
1250
+ }
1251
+ .fill.excellent {
1252
+ background: #28a745;
1253
+ }
1254
+ .fill.good {
1255
+ background: #6f42c1;
1256
+ }
1257
+ .fill.fair {
1258
+ background: #ffc107;
1259
+ }
1260
+ .fill.poor {
1261
+ background: #dc3545;
1262
+ }
1263
+ .quality-issues {
1264
+ color: #dc3545;
1265
+ font-size: 12px;
1266
+ }
1267
+ .recommendations {
1268
+ color: #007bff;
1269
+ font-size: 12px;
1270
+ font-style: italic;
1271
+ }
1272
+ .performance-help {
1273
+ background: #f8f9fa;
1274
+ border: 1px solid #e9ecef;
1275
+ border-radius: 8px;
1276
+ padding: 15px;
1277
+ margin: 15px 0;
1278
+ font-size: 14px;
1279
+ }
1280
+ .help-section {
1281
+ margin: 10px 0;
1282
+ padding: 8px;
1283
+ background: white;
1284
+ border-radius: 4px;
1285
+ border-left: 4px solid #007bff;
1286
+ }
1287
+ .help-section strong {
1288
+ color: #495057;
1289
+ }
1290
+ .help-section em {
1291
+ color: #28a745;
1292
+ font-weight: 600;
1293
+ }
1294
+ .test-file {
1295
+ font-weight: bold;
1296
+ color: #495057;
1297
+ margin-top: 10px;
1298
+ margin-bottom: 5px;
1299
+ }
1300
+ .test-file:first-child {
1301
+ margin-top: 0;
1302
+ }
1303
+ .line-coverage {
1304
+ padding: 20px;
1305
+ }
1306
+ .line-item {
1307
+ margin-bottom: 20px;
1308
+ padding: 15px;
1309
+ border-left: 4px solid #3498db;
1310
+ background: #f8f9fa;
1311
+ border-radius: 0 5px 5px 0;
1312
+ }
1313
+ .line-number {
1314
+ font-weight: bold;
1315
+ color: #2c3e50;
1316
+ font-size: 1.1em;
1317
+ margin-bottom: 10px;
1318
+ }
1319
+ .test-count {
1320
+ color: #7f8c8d;
1321
+ font-size: 0.9em;
1322
+ }
1323
+ .test-list {
1324
+ margin-top: 10px;
1325
+ }
1326
+ .test-item {
1327
+ background: white;
1328
+ margin: 5px 0;
1329
+ padding: 8px 12px;
1330
+ border-radius: 20px;
1331
+ display: inline-block;
1332
+ margin-right: 8px;
1333
+ margin-bottom: 8px;
1334
+ border: 1px solid #e1e8ed;
1335
+ font-size: 0.85em;
1336
+ transition: all 0.2s ease;
1337
+ }
1338
+ .test-item:hover {
1339
+ background: #3498db;
1340
+ color: white;
1341
+ transform: translateY(-1px);
1342
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1343
+ }
1344
+ .stats {
1345
+ background: white;
1346
+ padding: 20px;
1347
+ border-radius: 10px;
1348
+ margin-bottom: 30px;
1349
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
1350
+ }
1351
+ .stats h3 {
1352
+ margin-top: 0;
1353
+ color: #2c3e50;
1354
+ }
1355
+ .stat-item {
1356
+ display: inline-block;
1357
+ margin-right: 30px;
1358
+ margin-bottom: 10px;
1359
+ }
1360
+ .stat-number {
1361
+ font-size: 2em;
1362
+ font-weight: bold;
1363
+ color: #3498db;
1364
+ }
1365
+ .stat-label {
1366
+ display: block;
1367
+ color: #7f8c8d;
1368
+ font-size: 0.9em;
1369
+ }
1370
+ .footer {
1371
+ text-align: center;
1372
+ color: #7f8c8d;
1373
+ margin-top: 40px;
1374
+ padding: 20px;
1375
+ }
1376
+
1377
+ /* Mutation Testing Styles */
1378
+ .mutation-results {
1379
+ background: #f8f9fa;
1380
+ border: 1px solid #dee2e6;
1381
+ border-radius: 8px;
1382
+ margin: 15px 0;
1383
+ padding: 15px;
1384
+ }
1385
+ .mutation-results h4 {
1386
+ margin: 0 0 15px 0;
1387
+ color: #495057;
1388
+ font-size: 16px;
1389
+ }
1390
+ .mutation-summary {
1391
+ display: flex;
1392
+ align-items: center;
1393
+ gap: 15px;
1394
+ margin-bottom: 15px;
1395
+ flex-wrap: wrap;
1396
+ }
1397
+ .mutation-score {
1398
+ font-weight: bold;
1399
+ padding: 8px 12px;
1400
+ border-radius: 6px;
1401
+ font-size: 14px;
1402
+ }
1403
+ .mutation-score-good {
1404
+ background: #d4edda;
1405
+ color: #155724;
1406
+ border: 1px solid #c3e6cb;
1407
+ }
1408
+ .mutation-score-fair {
1409
+ background: #fff3cd;
1410
+ color: #856404;
1411
+ border: 1px solid #ffeaa7;
1412
+ }
1413
+ .mutation-score-poor {
1414
+ background: #f8d7da;
1415
+ color: #721c24;
1416
+ border: 1px solid #f5c6cb;
1417
+ }
1418
+ .mutation-stats {
1419
+ display: flex;
1420
+ gap: 10px;
1421
+ flex-wrap: wrap;
1422
+ }
1423
+ .mutation-stat {
1424
+ padding: 4px 8px;
1425
+ border-radius: 4px;
1426
+ font-size: 12px;
1427
+ font-weight: 500;
1428
+ }
1429
+ .mutation-stat.killed {
1430
+ background: #d4edda;
1431
+ color: #155724;
1432
+ }
1433
+ .mutation-stat.survived {
1434
+ background: #f8d7da;
1435
+ color: #721c24;
1436
+ }
1437
+ .mutation-stat.error {
1438
+ background: #f8d7da;
1439
+ color: #721c24;
1440
+ }
1441
+ .mutation-stat.timeout {
1442
+ background: #fff3cd;
1443
+ color: #856404;
1444
+ }
1445
+ .mutation-details {
1446
+ margin-top: 15px;
1447
+ }
1448
+ .mutation-group {
1449
+ margin-bottom: 15px;
1450
+ }
1451
+ .mutation-group h5 {
1452
+ margin: 0 0 10px 0;
1453
+ font-size: 14px;
1454
+ color: #495057;
1455
+ }
1456
+ .mutation-group summary {
1457
+ cursor: pointer;
1458
+ font-weight: 500;
1459
+ color: #495057;
1460
+ margin-bottom: 10px;
1461
+ }
1462
+ .mutation-item {
1463
+ display: flex;
1464
+ align-items: center;
1465
+ gap: 10px;
1466
+ padding: 8px 12px;
1467
+ margin: 5px 0;
1468
+ border-radius: 4px;
1469
+ font-size: 13px;
1470
+ flex-wrap: wrap;
1471
+ }
1472
+ .mutation-item.survived {
1473
+ background: #f8d7da;
1474
+ border-left: 4px solid #dc3545;
1475
+ }
1476
+ .mutation-item.killed {
1477
+ background: #d4edda;
1478
+ border-left: 4px solid #28a745;
1479
+ }
1480
+ .mutation-item.error {
1481
+ background: #f8d7da;
1482
+ border-left: 4px solid #dc3545;
1483
+ }
1484
+ .mutation-type {
1485
+ background: #6c757d;
1486
+ color: white;
1487
+ padding: 2px 6px;
1488
+ border-radius: 3px;
1489
+ font-size: 11px;
1490
+ font-weight: 500;
1491
+ text-transform: uppercase;
1492
+ min-width: 80px;
1493
+ text-align: center;
1494
+ }
1495
+ .mutation-description {
1496
+ flex: 1;
1497
+ color: #495057;
1498
+ }
1499
+ .mutation-tests {
1500
+ color: #6c757d;
1501
+ font-size: 12px;
1502
+ }
1503
+ .mutation-error {
1504
+ color: #721c24;
1505
+ font-style: italic;
1506
+ flex: 1;
1507
+ }
1508
+
1509
+ /* Mutation Dashboard Styles */
1510
+ .mutations-overview {
1511
+ margin-bottom: 30px;
1512
+ }
1513
+ .mutations-summary-cards {
1514
+ display: grid;
1515
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1516
+ gap: 20px;
1517
+ margin-bottom: 30px;
1518
+ }
1519
+ .summary-card {
1520
+ background: white;
1521
+ padding: 20px;
1522
+ border-radius: 8px;
1523
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1524
+ text-align: center;
1525
+ border: 2px solid #e9ecef;
1526
+ }
1527
+ .summary-card.excellent {
1528
+ border-color: #28a745;
1529
+ background: #f8fff9;
1530
+ }
1531
+ .summary-card.good {
1532
+ border-color: #6f42c1;
1533
+ background: #f8f7ff;
1534
+ }
1535
+ .summary-card.fair {
1536
+ border-color: #ffc107;
1537
+ background: #fffef8;
1538
+ }
1539
+ .summary-card.poor {
1540
+ border-color: #dc3545;
1541
+ background: #fff8f8;
1542
+ }
1543
+ .summary-card.survived {
1544
+ border-color: #dc3545;
1545
+ background: #fff8f8;
1546
+ }
1547
+ .summary-card.killed {
1548
+ border-color: #28a745;
1549
+ background: #f8fff9;
1550
+ }
1551
+ .summary-card h3 {
1552
+ margin: 0 0 10px 0;
1553
+ font-size: 14px;
1554
+ color: #6c757d;
1555
+ font-weight: 600;
1556
+ }
1557
+ .big-number {
1558
+ font-size: 2.5em;
1559
+ font-weight: bold;
1560
+ color: #495057;
1561
+ margin: 10px 0;
1562
+ }
1563
+ .subtitle {
1564
+ font-size: 12px;
1565
+ color: #6c757d;
1566
+ }
1567
+ .mutations-chart {
1568
+ margin: 30px 0;
1569
+ }
1570
+ .mutation-bars {
1571
+ margin: 15px 0;
1572
+ }
1573
+ .mutation-bar {
1574
+ margin: 10px 0;
1575
+ }
1576
+ .mutation-bar span {
1577
+ display: block;
1578
+ margin-bottom: 5px;
1579
+ font-size: 14px;
1580
+ font-weight: 500;
1581
+ }
1582
+ .mutation-killed {
1583
+ color: #28a745;
1584
+ }
1585
+ .mutation-survived {
1586
+ color: #dc3545;
1587
+ }
1588
+ .mutation-timeout {
1589
+ color: #ffc107;
1590
+ }
1591
+ .mutation-error {
1592
+ color: #dc3545;
1593
+ }
1594
+ .fill.killed {
1595
+ background: #28a745;
1596
+ }
1597
+ .fill.survived {
1598
+ background: #dc3545;
1599
+ }
1600
+ .fill.timeout {
1601
+ background: #ffc107;
1602
+ }
1603
+ .fill.error {
1604
+ background: #dc3545;
1605
+ }
1606
+ .mutations-files {
1607
+ margin-top: 30px;
1608
+ }
1609
+ .mutations-table {
1610
+ width: 100%;
1611
+ border-collapse: collapse;
1612
+ margin-top: 15px;
1613
+ }
1614
+ .mutations-table th,
1615
+ .mutations-table td {
1616
+ padding: 12px;
1617
+ text-align: left;
1618
+ border-bottom: 1px solid #e9ecef;
1619
+ }
1620
+ .mutations-table th {
1621
+ background: #f8f9fa;
1622
+ font-weight: 600;
1623
+ position: sticky;
1624
+ top: 0;
1625
+ }
1626
+ .mutations-table tr:hover {
1627
+ background: #f8f9fa;
1628
+ }
1629
+ .mutations-table .killed {
1630
+ color: #28a745;
1631
+ font-weight: bold;
1632
+ }
1633
+ .mutations-table .survived {
1634
+ color: #dc3545;
1635
+ font-weight: bold;
1636
+ }
1637
+ .no-data {
1638
+ text-align: center;
1639
+ padding: 40px;
1640
+ color: #6c757d;
1641
+ }
1642
+ .no-data h3 {
1643
+ margin-bottom: 10px;
1644
+ color: #495057;
1645
+ }
1646
+ .file-row {
1647
+ cursor: pointer;
1648
+ }
1649
+ .file-row:hover {
1650
+ background: #f8f9fa;
1651
+ }
1652
+ .file-details {
1653
+ background: #f8f9fa;
1654
+ }
1655
+ .file-mutation-details {
1656
+ padding: 15px;
1657
+ }
1658
+ .mutations-by-line {
1659
+ margin: 10px 0;
1660
+ }
1661
+ .line-mutations {
1662
+ margin-bottom: 20px;
1663
+ border: 1px solid #e9ecef;
1664
+ border-radius: 6px;
1665
+ padding: 15px;
1666
+ background: white;
1667
+ }
1668
+ .line-mutations h5 {
1669
+ margin: 0 0 10px 0;
1670
+ color: #495057;
1671
+ font-size: 14px;
1672
+ font-weight: 600;
1673
+ }
1674
+ .mutations-list {
1675
+ display: flex;
1676
+ flex-direction: column;
1677
+ gap: 10px;
1678
+ }
1679
+ .mutation-detail {
1680
+ padding: 10px;
1681
+ border-radius: 4px;
1682
+ border-left: 4px solid #6c757d;
1683
+ }
1684
+ .mutation-detail.killed {
1685
+ background: #d4edda;
1686
+ border-left-color: #28a745;
1687
+ }
1688
+ .mutation-detail.survived {
1689
+ background: #f8d7da;
1690
+ border-left-color: #dc3545;
1691
+ }
1692
+ .mutation-detail.error {
1693
+ background: #fff3cd;
1694
+ border-left-color: #ffc107;
1695
+ }
1696
+ .mutation-header {
1697
+ display: flex;
1698
+ justify-content: space-between;
1699
+ align-items: center;
1700
+ margin-bottom: 8px;
1701
+ }
1702
+ .mutation-status {
1703
+ font-weight: bold;
1704
+ font-size: 12px;
1705
+ }
1706
+ .mutation-type {
1707
+ background: #6c757d;
1708
+ color: white;
1709
+ padding: 2px 6px;
1710
+ border-radius: 3px;
1711
+ font-size: 11px;
1712
+ font-weight: 500;
1713
+ text-transform: uppercase;
1714
+ }
1715
+ .mutation-change {
1716
+ margin: 8px 0;
1717
+ font-size: 13px;
1718
+ }
1719
+ .mutation-change code {
1720
+ background: #f8f9fa;
1721
+ padding: 2px 4px;
1722
+ border-radius: 3px;
1723
+ font-family: 'Courier New', monospace;
1724
+ }
1725
+ .mutation-tests {
1726
+ font-size: 12px;
1727
+ color: #6c757d;
1728
+ font-style: italic;
1729
+ }
1730
+ </style>
1731
+ <script>
1732
+ function toggleCoverage(lineNumber, filePath) {
1733
+ const detailsId = 'details-' + filePath.replace(/[^a-zA-Z0-9]/g, '_') + '-' + lineNumber;
1734
+ const details = document.getElementById(detailsId);
1735
+ if (details) {
1736
+ details.classList.toggle('expanded');
1737
+ }
1738
+ }
1739
+ </script>
1740
+ </head>
1741
+ <body>
1742
+ <div class="header">
1743
+ <h1>🧪 Jest Test Lineage Report</h1>
1744
+ <p>Click on coverage indicators to see which tests cover each line</p>
1745
+ <div class="file-path">Generated on ${new Date().toLocaleString()}</div>
1746
+ </div>
1747
+
1748
+ <div class="navigation">
1749
+ <div class="nav-buttons">
1750
+ <button id="view-files" class="nav-btn active" onclick="showView('files')">📁 Files View</button>
1751
+ <button id="view-lines" class="nav-btn" onclick="showView('lines')">📊 Lines Analysis</button>
1752
+ <button id="view-performance" class="nav-btn" onclick="showView('performance')">🔥 Performance Analytics</button>
1753
+ <button id="view-quality" class="nav-btn" onclick="showView('quality')">🧪 Test Quality</button>
1754
+ <button id="view-mutations" class="nav-btn" onclick="showView('mutations')">🧬 Mutation Testing</button>
1755
+ <button id="expand-all" class="action-btn" onclick="expandAll()">📖 Expand All</button>
1756
+ <button id="collapse-all" class="action-btn" onclick="collapseAll()">📕 Collapse All</button>
1757
+ </div>
1758
+ <div class="sort-controls" id="sort-controls" style="display: none;">
1759
+ <label>Sort by:</label>
1760
+ <select id="sort-by" onchange="sortLines()">
1761
+ <option value="executions">Most Executions</option>
1762
+ <option value="tests">Most Tests</option>
1763
+ <option value="depth">Deepest Calls</option>
1764
+ <option value="cpuCycles">Most CPU Cycles</option>
1765
+ <option value="cpuTime">Most CPU Time</option>
1766
+ <option value="wallTime">Most Wall Time</option>
1767
+ <option value="quality">Best Quality</option>
1768
+ <option value="reliability">Most Reliable</option>
1769
+ <option value="maintainability">Most Maintainable</option>
1770
+ <option value="testSmells">Most Test Smells</option>
1771
+ <option value="file">File Name</option>
1772
+ </select>
1773
+ </div>
1774
+ </div>
1775
+
1776
+ <div id="files-view">
1777
+ `;
1778
+ }
1779
+
1780
+ async generateCodeTreeSection(filePath) {
1781
+ try {
1782
+ // Check if file exists first
1783
+ if (!fs.existsSync(filePath)) {
1784
+ console.warn(`⚠️ File not found: ${filePath}`);
1785
+
1786
+ // Try to find the file with alternative paths
1787
+ const alternativePaths = this.findAlternativeFilePaths(filePath);
1788
+ if (alternativePaths.length > 0) {
1789
+ console.log(`🔍 Trying alternative paths for ${path.basename(filePath)}:`, alternativePaths);
1790
+ for (const altPath of alternativePaths) {
1791
+ if (fs.existsSync(altPath)) {
1792
+ console.log(`✅ Found file at: ${altPath}`);
1793
+ return this.generateCodeTreeSection(altPath); // Recursive call with found path
1794
+ }
1795
+ }
1796
+ }
1797
+
1798
+ return `<div class="file-section">
1799
+ <div class="file-header">❌ Error reading ${path.basename(filePath)}</div>
1800
+ <div class="error">
1801
+ <p><strong>File not found:</strong> ${filePath}</p>
1802
+ <p><strong>Tried alternatives:</strong> ${alternativePaths.slice(0, 3).join(', ')}</p>
1803
+ <p><strong>Suggestion:</strong> Check your project structure and source directory configuration.</p>
1804
+ </div>
1805
+ </div>`;
1806
+ }
1807
+
1808
+ // Read the source file content
1809
+ const sourceCode = fs.readFileSync(filePath, 'utf8');
1810
+ const lines = sourceCode.split('\n');
1811
+
1812
+ // Check if we have coverage data for this file
1813
+ let lineCoverage = this.coverageData[filePath];
1814
+ let actualFilePath = filePath;
1815
+
1816
+ if (!lineCoverage || typeof lineCoverage !== 'object') {
1817
+ // Try to find coverage data with smart path matching
1818
+ const matchedPath = this.findMatchingCoveragePath(filePath);
1819
+ if (matchedPath) {
1820
+ console.log(`🔧 Path mismatch resolved: "${filePath}" -> "${matchedPath}"`);
1821
+ lineCoverage = this.coverageData[matchedPath];
1822
+ actualFilePath = matchedPath;
1823
+ }
1824
+ }
1825
+
1826
+ if (!lineCoverage || typeof lineCoverage !== 'object') {
1827
+ console.warn(`⚠️ No coverage data found for: ${filePath}`);
1828
+
1829
+ // Debug: Log all available coverage data
1830
+ // Debug logging removed for production
1831
+
1832
+ console.log(`🔍 DEBUG: Looking for: "${filePath}"`);
1833
+ console.log(`🔍 DEBUG: Exact match exists: ${this.coverageData.hasOwnProperty(filePath)}`);
1834
+
1835
+ // Check for similar paths
1836
+ const similarPaths = Object.keys(this.coverageData).filter(key =>
1837
+ key.includes(path.basename(filePath)) || filePath.includes(path.basename(key))
1838
+ );
1839
+ if (similarPaths.length > 0) {
1840
+ console.log(`🔍 DEBUG: Similar paths found:`, similarPaths);
1841
+ }
1842
+
1843
+ // Log the actual data structure for the first few files
1844
+ console.log(`🔍 DEBUG: Sample coverage data structure:`);
1845
+ Object.entries(this.coverageData).slice(0, 2).forEach(([key, value]) => {
1846
+ console.log(` File: ${key}`);
1847
+ console.log(` Type: ${typeof value}, Keys: ${value ? Object.keys(value).length : 'N/A'}`);
1848
+ if (value && typeof value === 'object') {
1849
+ const sampleLines = Object.keys(value).slice(0, 3);
1850
+ console.log(` Sample lines: ${sampleLines.join(', ')}`);
1851
+ }
1852
+ });
1853
+
1854
+ return `<div class="file-section">
1855
+ <div class="file-header">❌ Error reading ${path.basename(filePath)}</div>
1856
+ <div class="error">
1857
+ <p><strong>File:</strong> ${filePath}</p>
1858
+ <p><strong>Error:</strong> No coverage data available</p>
1859
+ <p><strong>Available files:</strong> ${Object.keys(this.coverageData).length} files tracked</p>
1860
+ <p><strong>Similar paths:</strong> ${similarPaths.length > 0 ? similarPaths.join(', ') : 'None found'}</p>
1861
+ <p><strong>Suggestion:</strong> Make sure the file is being instrumented and tests are running.</p>
1862
+ </div>
1863
+ </div>`;
1864
+ }
1865
+
1866
+ const coveredLineNumbers = Object.keys(lineCoverage).map(n => parseInt(n));
1867
+
1868
+ // Calculate stats for this file
1869
+ const totalLines = lines.length;
1870
+ const coveredLines = coveredLineNumbers.length;
1871
+ const totalTests = new Set();
1872
+ Object.values(lineCoverage).forEach(tests => {
1873
+ if (Array.isArray(tests)) {
1874
+ tests.forEach(test => totalTests.add(test));
1875
+ }
1876
+ });
1877
+
1878
+ const fileId = actualFilePath.replace(/[^a-zA-Z0-9]/g, '_');
1879
+
1880
+ let html = `
1881
+ <div class="file-section">
1882
+ <div class="file-header" onclick="toggleFile('${fileId}')">
1883
+ <div>
1884
+ 📄 ${path.basename(filePath)}
1885
+ <div class="file-path">${filePath}</div>
1886
+ </div>
1887
+ <span class="expand-icon">▶</span>
1888
+ </div>
1889
+ <div class="file-content" id="content-${fileId}">
1890
+ <div class="stats">
1891
+ <h3>File Statistics</h3>
1892
+ <div class="stat-item">
1893
+ <div class="stat-number">${coveredLines}</div>
1894
+ <span class="stat-label">Lines Covered</span>
1895
+ </div>
1896
+ <div class="stat-item">
1897
+ <div class="stat-number">${totalLines}</div>
1898
+ <span class="stat-label">Total Lines</span>
1899
+ </div>
1900
+ <div class="stat-item">
1901
+ <div class="stat-number">${totalTests.size}</div>
1902
+ <span class="stat-label">Unique Tests</span>
1903
+ </div>
1904
+ </div>
1905
+ <div class="code-container">
1906
+ `;
1907
+
1908
+ // Generate each line of code
1909
+ lines.forEach((lineContent, index) => {
1910
+ const lineNumber = index + 1;
1911
+ const isCovered = coveredLineNumbers.includes(lineNumber);
1912
+ const lineNumberClass = isCovered ? 'line-covered' : 'line-uncovered';
1913
+
1914
+ html += `
1915
+ <div class="code-line">
1916
+ <div class="line-number ${lineNumberClass}">${lineNumber}</div>
1917
+ <div class="line-content">${this.escapeHtml(lineContent || ' ')}</div>`;
1918
+
1919
+ if (isCovered) {
1920
+ const testInfos = lineCoverage[lineNumber];
1921
+ if (testInfos && Array.isArray(testInfos)) {
1922
+ const uniqueTests = this.deduplicateTests(testInfos);
1923
+ html += `
1924
+ <div class="coverage-indicator" onclick="toggleCoverage(${lineNumber}, '${fileId}')">
1925
+ ${uniqueTests.length} test${uniqueTests.length !== 1 ? 's' : ''}
1926
+ </div>`;
1927
+ } else {
1928
+ html += `
1929
+ <div class="coverage-indicator" style="background: #ffc107;">
1930
+ Invalid data
1931
+ </div>`;
1932
+ }
1933
+ } else {
1934
+ html += `
1935
+ <div class="coverage-indicator" style="background: #6c757d;">
1936
+ No coverage
1937
+ </div>`;
1938
+ }
1939
+
1940
+ html += `
1941
+ </div>`;
1942
+
1943
+ // Add coverage details (initially hidden)
1944
+ if (isCovered) {
1945
+ const testInfos = lineCoverage[lineNumber];
1946
+ if (testInfos && Array.isArray(testInfos) && testInfos.length > 0) {
1947
+ const uniqueTests = this.deduplicateTests(testInfos);
1948
+ const testsByFile = this.groupTestInfosByFile(uniqueTests);
1949
+
1950
+ // Get mutation results for this line
1951
+ const mutationResults = this.getMutationResultsForLine(actualFilePath, lineNumber);
1952
+
1953
+ html += `
1954
+ <div id="details-${fileId}-${lineNumber}" class="coverage-details">
1955
+ <strong>Line ${lineNumber} is covered by ${uniqueTests.length} test${uniqueTests.length !== 1 ? 's' : ''}:</strong>`;
1956
+
1957
+ // Add mutation testing results if available
1958
+ if (mutationResults && mutationResults.length > 0) {
1959
+ html += this.generateMutationResultsHtml(mutationResults, lineNumber);
1960
+ }
1961
+
1962
+ for (const [testFile, tests] of Object.entries(testsByFile)) {
1963
+ if (tests && Array.isArray(tests)) {
1964
+ html += `
1965
+ <div class="test-file">📁 ${testFile}</div>`;
1966
+ tests.forEach(testInfo => {
1967
+ if (testInfo) {
1968
+ const testName = typeof testInfo === 'string' ? testInfo : (testInfo.name || 'Unknown test');
1969
+ const executionCount = typeof testInfo === 'object' ? (testInfo.executionCount || 1) : 1;
1970
+ const trackingType = typeof testInfo === 'object' && testInfo.type === 'precise' ? 'PRECISE' : 'ESTIMATED';
1971
+
1972
+ // Generate depth information
1973
+ let depthInfo = '';
1974
+ let depthBadge = '';
1975
+ if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.depthData) {
1976
+ const depths = Object.keys(testInfo.depthData).map(d => parseInt(d)).sort((a, b) => a - b);
1977
+ const minDepth = Math.min(...depths);
1978
+
1979
+ if (depths.length === 1) {
1980
+ depthInfo = `, depth ${depths[0]}`;
1981
+ depthBadge = `<span class="depth-badge depth-${minDepth}">D${minDepth}</span>`;
1982
+ } else {
1983
+ depthInfo = `, depths ${depths.join(',')}`;
1984
+ depthBadge = `<span class="depth-badge depth-${minDepth}">D${depths.join(',')}</span>`;
1985
+ }
1986
+ }
1987
+
1988
+ const badgeColor = trackingType === 'PRECISE' ? '#28a745' : '#ffc107';
1989
+ const icon = trackingType === 'PRECISE' ? '✅' : '⚠️';
1990
+
1991
+ html += `<span class="test-badge"
1992
+ style="background-color: ${badgeColor};"
1993
+ title="${testName} (${trackingType}: ${executionCount} executions${depthInfo})">${icon} ${testName} ${depthBadge}</span>`;
1994
+ }
1995
+ });
1996
+ }
1997
+ }
1998
+ } else {
1999
+ html += `
2000
+ <div id="details-${fileId}-${lineNumber}" class="coverage-details">
2001
+ <strong>Line ${lineNumber}: No valid test data available</strong>
2002
+ </div>`;
2003
+ }
2004
+
2005
+ html += `
2006
+ </div>`;
2007
+ }
2008
+ });
2009
+
2010
+ html += `
2011
+ </div>
2012
+ </div>
2013
+ </div>`;
2014
+
2015
+ return html;
2016
+ } catch (error) {
2017
+ console.error(`Error reading file ${filePath}:`, error.message);
2018
+ return `<div class="file-section">
2019
+ <div class="file-header">❌ Error reading ${path.basename(filePath)}</div>
2020
+ <div class="error">
2021
+ <p><strong>File:</strong> ${filePath}</p>
2022
+ <p><strong>Error:</strong> ${error.message}</p>
2023
+ <p><strong>Suggestion:</strong> Make sure the file exists and is readable.</p>
2024
+ </div>
2025
+ </div>`;
2026
+ }
2027
+ }
2028
+
2029
+ escapeHtml(text) {
2030
+ return text
2031
+ .replace(/&/g, '&amp;')
2032
+ .replace(/</g, '&lt;')
2033
+ .replace(/>/g, '&gt;')
2034
+ .replace(/"/g, '&quot;')
2035
+ .replace(/'/g, '&#39;');
2036
+ }
2037
+
2038
+ deduplicateTests(testInfos) {
2039
+ const seen = new Set();
2040
+ return testInfos.filter(testInfo => {
2041
+ const key = typeof testInfo === 'string' ? testInfo : testInfo.name;
2042
+ if (seen.has(key)) {
2043
+ return false;
2044
+ }
2045
+ seen.add(key);
2046
+ return true;
2047
+ });
2048
+ }
2049
+
2050
+ groupTestInfosByFile(tests) {
2051
+ const grouped = {};
2052
+ tests.forEach(testInfo => {
2053
+ const testFile = typeof testInfo === 'string' ? 'Unknown' : testInfo.file;
2054
+
2055
+ if (!grouped[testFile]) grouped[testFile] = [];
2056
+ grouped[testFile].push(testInfo);
2057
+ });
2058
+ return grouped;
2059
+ }
2060
+
2061
+ getConfidenceColor(confidence) {
2062
+ if (confidence >= 80) return '#28a745'; // Green - high confidence
2063
+ if (confidence >= 60) return '#ffc107'; // Yellow - medium confidence
2064
+ if (confidence >= 40) return '#fd7e14'; // Orange - low confidence
2065
+ return '#dc3545'; // Red - very low confidence
2066
+ }
2067
+
2068
+ groupTestsByFile(tests) {
2069
+ const grouped = {};
2070
+ tests.forEach(testName => {
2071
+ // Extract test file from test name based on describe block patterns
2072
+ let testFile = 'Unknown';
2073
+
2074
+ if (testName.includes('Calculator')) testFile = 'calculator.test.ts';
2075
+ else if (testName.includes('Add Function Only')) testFile = 'add-only.test.ts';
2076
+ else if (testName.includes('Multiply Function Only')) testFile = 'multiply-only.test.ts';
2077
+ else if (testName.includes('Subtract Function Only')) testFile = 'subtract-only.test.ts';
2078
+
2079
+ if (!grouped[testFile]) grouped[testFile] = [];
2080
+ grouped[testFile].push(testName);
2081
+ });
2082
+ return grouped;
2083
+ }
2084
+
2085
+ findAlternativeFilePaths(originalPath) {
2086
+ const filename = path.basename(originalPath);
2087
+ const cwd = process.cwd();
2088
+
2089
+ // Use the same smart discovery logic as testSetup.js
2090
+ return this.discoverFileInProject(cwd, filename);
2091
+ }
2092
+
2093
+ discoverFileInProject(projectRoot, targetFilename) {
2094
+ const discoveredPaths = [];
2095
+ const maxDepth = 4; // Slightly deeper search for reporter
2096
+
2097
+ const scanDirectory = (dir, depth = 0) => {
2098
+ if (depth > maxDepth) return;
2099
+
2100
+ try {
2101
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
2102
+
2103
+ for (const entry of entries) {
2104
+ if (entry.isFile() && entry.name === targetFilename) {
2105
+ discoveredPaths.push(path.join(dir, entry.name));
2106
+ } else if (entry.isDirectory()) {
2107
+ const dirName = entry.name;
2108
+
2109
+ // Skip common non-source directories
2110
+ if (this.shouldSkipDirectory(dirName)) continue;
2111
+
2112
+ const fullDirPath = path.join(dir, dirName);
2113
+ scanDirectory(fullDirPath, depth + 1);
2114
+ }
2115
+ }
2116
+ } catch (error) {
2117
+ // Skip directories we can't read
2118
+ }
2119
+ };
2120
+
2121
+ scanDirectory(projectRoot);
2122
+
2123
+ // Sort by preference (source directories first, then by depth)
2124
+ return discoveredPaths.sort((a, b) => {
2125
+ const aScore = this.getPathScore(a);
2126
+ const bScore = this.getPathScore(b);
2127
+ return bScore - aScore; // Higher score first
2128
+ });
2129
+ }
2130
+
2131
+ shouldSkipDirectory(dirName) {
2132
+ const skipPatterns = [
2133
+ 'node_modules', '.git', '.next', '.nuxt', 'dist', 'build',
2134
+ 'coverage', '.nyc_output', 'tmp', 'temp', '.cache',
2135
+ '.vscode', '.idea', '__pycache__', '.pytest_cache',
2136
+ 'vendor', 'target', 'bin', 'obj', '.DS_Store'
2137
+ ];
2138
+
2139
+ return skipPatterns.includes(dirName) || dirName.startsWith('.');
2140
+ }
2141
+
2142
+ getPathScore(filePath) {
2143
+ const pathParts = filePath.split(path.sep);
2144
+ let score = 0;
2145
+
2146
+ // Prefer shorter paths
2147
+ score += Math.max(0, 10 - pathParts.length);
2148
+
2149
+ // Prefer common source directory names
2150
+ const sourcePreference = ['src', 'lib', 'source', 'app', 'server', 'client'];
2151
+ for (const part of pathParts) {
2152
+ const index = sourcePreference.indexOf(part);
2153
+ if (index !== -1) {
2154
+ score += sourcePreference.length - index;
2155
+ }
2156
+ }
2157
+
2158
+ // Bonus for TypeScript files
2159
+ if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
2160
+ score += 2;
2161
+ }
2162
+
2163
+ return score;
2164
+ }
2165
+
2166
+ generateTestQualitySummary() {
2167
+ console.log('\n🧪 TEST QUALITY & PERFORMANCE ANALYSIS');
2168
+ console.log('=' .repeat(60));
2169
+
2170
+ const qualityStats = {
2171
+ totalTests: 0,
2172
+ highQuality: 0,
2173
+ goodQuality: 0,
2174
+ fairQuality: 0,
2175
+ poorQuality: 0,
2176
+ totalSmells: 0,
2177
+ memoryLeaks: 0,
2178
+ gcPressure: 0,
2179
+ slowTests: 0,
2180
+ reliableTests: 0,
2181
+ totalAssertions: 0,
2182
+ totalComplexity: 0
2183
+ };
2184
+
2185
+ const testSmellTypes = {};
2186
+ const performanceIssues = [];
2187
+
2188
+ // Analyze all tests
2189
+ for (const filePath in this.coverageData) {
2190
+ const lineCoverage = this.coverageData[filePath];
2191
+ for (const lineNumber in lineCoverage) {
2192
+ const tests = lineCoverage[lineNumber];
2193
+ if (!Array.isArray(tests)) continue;
2194
+
2195
+ tests.forEach(test => {
2196
+ if (test.type === 'precise' && test.quality) {
2197
+ qualityStats.totalTests++;
2198
+
2199
+ // Quality classification
2200
+ if (test.quality.maintainability >= 80) qualityStats.highQuality++;
2201
+ else if (test.quality.maintainability >= 60) qualityStats.goodQuality++;
2202
+ else if (test.quality.maintainability >= 40) qualityStats.fairQuality++;
2203
+ else qualityStats.poorQuality++;
2204
+
2205
+ // Reliability
2206
+ if (test.quality.reliability >= 80) qualityStats.reliableTests++;
2207
+
2208
+ // Test smells
2209
+ if (test.quality.testSmells) {
2210
+ qualityStats.totalSmells += test.quality.testSmells.length;
2211
+ test.quality.testSmells.forEach(smell => {
2212
+ testSmellTypes[smell] = (testSmellTypes[smell] || 0) + 1;
2213
+ });
2214
+ }
2215
+
2216
+ // Performance issues - check for large memory allocations
2217
+ if (test.performance) {
2218
+ // Check for memory leaks (large allocations)
2219
+ const memoryMB = Math.abs(test.performance.totalMemoryDelta || 0) / (1024 * 1024);
2220
+ if (memoryMB > 1) { // > 1MB
2221
+ qualityStats.memoryLeaks++;
2222
+ performanceIssues.push(`${test.name}: Large memory allocation (${memoryMB.toFixed(1)}MB)`);
2223
+ }
2224
+
2225
+ // Check for GC pressure
2226
+ if (test.performance.gcPressure > 5) {
2227
+ qualityStats.gcPressure++;
2228
+ performanceIssues.push(`${test.name}: High GC pressure (${test.performance.gcPressure})`);
2229
+ }
2230
+
2231
+ // Check for slow executions
2232
+ if (test.performance.slowExecutions > test.performance.fastExecutions) {
2233
+ qualityStats.slowTests++;
2234
+ performanceIssues.push(`${test.name}: Inconsistent performance`);
2235
+ }
2236
+ }
2237
+
2238
+ qualityStats.totalAssertions += test.quality.assertions || 0;
2239
+ qualityStats.totalComplexity += test.quality.complexity || 0;
2240
+ }
2241
+ });
2242
+ }
2243
+ }
2244
+
2245
+ // Display quality summary
2246
+ console.log(`\n📊 QUALITY DISTRIBUTION (${qualityStats.totalTests} tests analyzed):`);
2247
+ console.log(` 🏆 High Quality (80-100%): ${qualityStats.highQuality} tests (${((qualityStats.highQuality/qualityStats.totalTests)*100).toFixed(1)}%)`);
2248
+ console.log(` ✅ Good Quality (60-79%): ${qualityStats.goodQuality} tests (${((qualityStats.goodQuality/qualityStats.totalTests)*100).toFixed(1)}%)`);
2249
+ console.log(` ⚠️ Fair Quality (40-59%): ${qualityStats.fairQuality} tests (${((qualityStats.fairQuality/qualityStats.totalTests)*100).toFixed(1)}%)`);
2250
+ console.log(` ❌ Poor Quality (0-39%): ${qualityStats.poorQuality} tests (${((qualityStats.poorQuality/qualityStats.totalTests)*100).toFixed(1)}%)`);
2251
+
2252
+ console.log(`\n🛡️ RELIABILITY METRICS:`);
2253
+ console.log(` 🔒 Reliable Tests: ${qualityStats.reliableTests}/${qualityStats.totalTests} (${((qualityStats.reliableTests/qualityStats.totalTests)*100).toFixed(1)}%)`);
2254
+ console.log(` 🎯 Total Assertions: ${qualityStats.totalAssertions} (avg: ${(qualityStats.totalAssertions/qualityStats.totalTests).toFixed(1)} per test)`);
2255
+ console.log(` 🔧 Total Complexity: ${qualityStats.totalComplexity} (avg: ${(qualityStats.totalComplexity/qualityStats.totalTests).toFixed(1)} per test)`);
2256
+
2257
+ // Test smells breakdown
2258
+ if (qualityStats.totalSmells > 0) {
2259
+ console.log(`\n🚨 TEST SMELLS DETECTED (${qualityStats.totalSmells} total):`);
2260
+ Object.entries(testSmellTypes).forEach(([smell, count]) => {
2261
+ console.log(` • ${smell}: ${count} occurrences`);
2262
+ });
2263
+
2264
+ console.log(`\n💡 HOW TO IMPROVE TEST SCORES:`);
2265
+ if (testSmellTypes['Long Test']) {
2266
+ console.log(` 📏 Long Tests: Break down tests >50 lines into smaller, focused tests`);
2267
+ }
2268
+ if (testSmellTypes['No Assertions']) {
2269
+ console.log(` 🎯 No Assertions: Add expect() statements to verify behavior`);
2270
+ }
2271
+ if (testSmellTypes['Too Many Assertions']) {
2272
+ console.log(` 🔢 Too Many Assertions: Split tests with >10 assertions into separate test cases`);
2273
+ }
2274
+ if (testSmellTypes['Excessive Mocking']) {
2275
+ console.log(` 🎭 Excessive Mocking: Reduce mocks >5 per test, consider integration testing`);
2276
+ }
2277
+ if (testSmellTypes['Sleep/Wait Usage']) {
2278
+ console.log(` ⏰ Sleep/Wait Usage: Replace with proper async/await or mock timers`);
2279
+ }
2280
+ }
2281
+
2282
+ // Performance issues
2283
+ if (performanceIssues.length > 0) {
2284
+ console.log(`\n🔥 PERFORMANCE ISSUES DETECTED:`);
2285
+ console.log(` 🚨 Memory Leaks: ${qualityStats.memoryLeaks} tests`);
2286
+ console.log(` 🗑️ GC Pressure: ${qualityStats.gcPressure} tests`);
2287
+ console.log(` 🐌 Slow Tests: ${qualityStats.slowTests} tests`);
2288
+
2289
+ console.log(`\n⚡ PERFORMANCE RECOMMENDATIONS:`);
2290
+ if (qualityStats.memoryLeaks > 0) {
2291
+ console.log(` 💾 Memory Leaks: Review large object allocations, ensure proper cleanup`);
2292
+ console.log(` • Look for objects >1MB that aren't being released`);
2293
+ console.log(` • Check for global variables holding references`);
2294
+ console.log(` • Ensure event listeners are properly removed`);
2295
+ }
2296
+ if (qualityStats.gcPressure > 0) {
2297
+ console.log(` 🗑️ GC Pressure: Reduce frequent small allocations, reuse objects`);
2298
+ console.log(` • Object pooling: Reuse objects instead of creating new ones`);
2299
+ console.log(` • Batch operations: Process multiple items at once`);
2300
+ console.log(` • Avoid creating objects in loops`);
2301
+ console.log(` • Use primitive values when possible`);
2302
+ }
2303
+ if (qualityStats.slowTests > 0) {
2304
+ console.log(` 🐌 Slow Tests: Profile inconsistent tests, optimize heavy operations`);
2305
+ console.log(` • Use async/await instead of setTimeout`);
2306
+ console.log(` • Mock heavy operations in tests`);
2307
+ console.log(` • Reduce test data size`);
2308
+ }
2309
+ }
2310
+
2311
+ console.log('\n' + '=' .repeat(60));
2312
+ }
2313
+
2314
+ generateLinesData() {
2315
+ const linesData = [];
2316
+
2317
+ for (const filePath in this.coverageData) {
2318
+ const lineCoverage = this.coverageData[filePath];
2319
+
2320
+ for (const lineNumber in lineCoverage) {
2321
+ const tests = lineCoverage[lineNumber];
2322
+ if (!Array.isArray(tests) || tests.length === 0) continue;
2323
+
2324
+ let totalExecutions = 0;
2325
+ let maxDepth = 1;
2326
+ let minDepth = 1;
2327
+ const depths = new Set();
2328
+
2329
+ let totalCpuCycles = 0;
2330
+ let totalCpuTime = 0;
2331
+ let totalWallTime = 0;
2332
+ let maxCpuCycles = 0;
2333
+ let avgQuality = 0;
2334
+ let avgReliability = 0;
2335
+ let avgMaintainability = 0;
2336
+ let totalTestSmells = 0;
2337
+ let totalAssertions = 0;
2338
+ let totalComplexity = 0;
2339
+ let totalMemoryDelta = 0;
2340
+ let memoryLeaks = 0;
2341
+ let gcPressure = 0;
2342
+ let slowExecutions = 0;
2343
+ let fastExecutions = 0;
2344
+ let allTestSmells = new Set(); // Collect unique test smells
2345
+
2346
+ tests.forEach(test => {
2347
+ totalExecutions += test.executionCount || 1;
2348
+ if (test.maxDepth) maxDepth = Math.max(maxDepth, test.maxDepth);
2349
+ if (test.minDepth) minDepth = Math.min(minDepth, test.minDepth);
2350
+ if (test.depthData) {
2351
+ Object.keys(test.depthData).forEach(d => depths.add(parseInt(d)));
2352
+ }
2353
+ if (test.performance) {
2354
+ totalCpuCycles += test.performance.totalCpuCycles || 0;
2355
+ totalCpuTime += test.performance.totalCpuTime || 0;
2356
+ totalWallTime += test.performance.totalWallTime || 0;
2357
+ maxCpuCycles = Math.max(maxCpuCycles, test.performance.avgCpuCycles || 0);
2358
+
2359
+ // Add memory tracking
2360
+ totalMemoryDelta += Math.abs(test.performance.totalMemoryDelta || 0);
2361
+ memoryLeaks += test.performance.memoryLeaks || 0;
2362
+ gcPressure += test.performance.gcPressure || 0;
2363
+ slowExecutions += test.performance.slowExecutions || 0;
2364
+ fastExecutions += test.performance.fastExecutions || 0;
2365
+ }
2366
+ if (test.quality) {
2367
+ avgQuality += test.quality.maintainability || 0;
2368
+ avgReliability += test.quality.reliability || 0;
2369
+ avgMaintainability += test.quality.maintainability || 0;
2370
+ totalTestSmells += test.quality.testSmells ? test.quality.testSmells.length : 0;
2371
+ totalAssertions += test.quality.assertions || 0;
2372
+ totalComplexity += test.quality.complexity || 0;
2373
+
2374
+ // Collect individual test smells
2375
+ if (test.quality.testSmells && Array.isArray(test.quality.testSmells)) {
2376
+ test.quality.testSmells.forEach(smell => allTestSmells.add(smell));
2377
+ }
2378
+ }
2379
+ });
2380
+
2381
+ // Calculate averages
2382
+ avgQuality = tests.length > 0 ? avgQuality / tests.length : 0;
2383
+ avgReliability = tests.length > 0 ? avgReliability / tests.length : 0;
2384
+ avgMaintainability = tests.length > 0 ? avgMaintainability / tests.length : 0;
2385
+
2386
+ // Try to get code preview (simplified)
2387
+ let codePreview = `Line ${lineNumber}`;
2388
+ try {
2389
+ if (fs.existsSync(filePath)) {
2390
+ const content = fs.readFileSync(filePath, 'utf8');
2391
+ const lines = content.split('\n');
2392
+ const lineIndex = parseInt(lineNumber) - 1;
2393
+ if (lineIndex >= 0 && lineIndex < lines.length) {
2394
+ codePreview = lines[lineIndex].trim().substring(0, 50) + (lines[lineIndex].trim().length > 50 ? '...' : '');
2395
+ }
2396
+ }
2397
+ } catch (error) {
2398
+ // Fallback to line number only
2399
+ }
2400
+
2401
+ const depthArray = Array.from(depths).sort((a, b) => a - b);
2402
+ const depthRange = depthArray.length > 1 ? `${Math.min(...depthArray)}-${Math.max(...depthArray)}` : `${depthArray[0] || 1}`;
2403
+
2404
+ linesData.push({
2405
+ fileName: path.basename(filePath),
2406
+ filePath: filePath,
2407
+ lineNumber: parseInt(lineNumber),
2408
+ codePreview: codePreview,
2409
+ testCount: tests.length,
2410
+ totalExecutions: totalExecutions,
2411
+ maxDepth: maxDepth,
2412
+ minDepth: minDepth,
2413
+ depthRange: depthRange,
2414
+ performance: {
2415
+ totalCpuCycles: totalCpuCycles,
2416
+ avgCpuCycles: totalExecutions > 0 ? totalCpuCycles / totalExecutions : 0,
2417
+ totalCpuTime: totalCpuTime,
2418
+ avgCpuTime: totalExecutions > 0 ? totalCpuTime / totalExecutions : 0,
2419
+ totalWallTime: totalWallTime,
2420
+ avgWallTime: totalExecutions > 0 ? totalWallTime / totalExecutions : 0,
2421
+ maxCpuCycles: maxCpuCycles,
2422
+ totalMemoryDelta: totalMemoryDelta,
2423
+ memoryLeaks: memoryLeaks,
2424
+ gcPressure: gcPressure,
2425
+ slowExecutions: slowExecutions,
2426
+ fastExecutions: fastExecutions
2427
+ },
2428
+ quality: {
2429
+ avgQuality: avgQuality,
2430
+ avgReliability: avgReliability,
2431
+ avgMaintainability: avgMaintainability,
2432
+ totalTestSmells: totalTestSmells,
2433
+ totalAssertions: totalAssertions,
2434
+ totalComplexity: totalComplexity,
2435
+ qualityScore: (avgQuality + avgReliability + avgMaintainability) / 3,
2436
+ testSmells: Array.from(allTestSmells) // Include the actual smell names
2437
+ }
2438
+ });
2439
+ }
2440
+ }
2441
+
2442
+ return linesData;
2443
+ }
2444
+
2445
+ generateHtmlFooter() {
2446
+ // Calculate overall stats
2447
+ const allFiles = Object.keys(this.coverageData);
2448
+ const totalLines = allFiles.reduce((sum, file) => sum + Object.keys(this.coverageData[file]).length, 0);
2449
+ const uniqueTestNames = new Set();
2450
+
2451
+ allFiles.forEach(file => {
2452
+ Object.values(this.coverageData[file]).forEach(tests => {
2453
+ tests.forEach(test => {
2454
+ // Use test name to identify unique tests, not the entire test object
2455
+ uniqueTestNames.add(test.name);
2456
+ });
2457
+ });
2458
+ });
2459
+
2460
+ // console.log(`🔍 DEBUG: Total unique test names found: ${uniqueTestNames.size}`);
2461
+ // console.log(`🔍 DEBUG: Test names: ${Array.from(uniqueTestNames).join(', ')}`);
2462
+
2463
+ return `
2464
+ </div>
2465
+
2466
+ <div id="lines-view" style="display: none;">
2467
+ <div class="lines-analysis">
2468
+ <h2>📊 Lines Analysis</h2>
2469
+ <div id="lines-table"></div>
2470
+ </div>
2471
+ </div>
2472
+
2473
+ <div id="performance-view" style="display: none;">
2474
+ <div class="performance-analysis">
2475
+ <h2>🔥 Performance Analytics</h2>
2476
+ <div id="performance-dashboard"></div>
2477
+ </div>
2478
+ </div>
2479
+
2480
+ <div id="quality-view" style="display: none;">
2481
+ <div class="quality-analysis">
2482
+ <h2>🧪 Test Quality Analysis</h2>
2483
+ <div id="quality-dashboard"></div>
2484
+ </div>
2485
+ </div>
2486
+
2487
+ <div id="mutations-view" style="display: none;">
2488
+ <div class="mutations-analysis">
2489
+ <h2>🧬 Mutation Testing Results</h2>
2490
+ <div id="mutations-dashboard"></div>
2491
+ </div>
2492
+ </div>
2493
+
2494
+ <div class="stats">
2495
+ <h3>📊 Overall Statistics</h3>
2496
+ <div class="stat-item">
2497
+ <div class="stat-number">${allFiles.length}</div>
2498
+ <span class="stat-label">Files Analyzed</span>
2499
+ </div>
2500
+ <div class="stat-item">
2501
+ <div class="stat-number">${totalLines}</div>
2502
+ <span class="stat-label">Total Lines Covered</span>
2503
+ </div>
2504
+ <div class="stat-item">
2505
+ <div class="stat-number">${uniqueTestNames.size}</div>
2506
+ <span class="stat-label">Total Unique Tests</span>
2507
+ </div>
2508
+ </div>
2509
+
2510
+ <div class="footer">
2511
+ <p>Generated by Jest Test Lineage Reporter</p>
2512
+ <p>This report shows which tests cover which lines of code to help identify test redundancy and coverage gaps.</p>
2513
+ </div>
2514
+
2515
+ <script>
2516
+ // Global data for lines analysis
2517
+ window.linesData = ${JSON.stringify(this.generateLinesData())};
2518
+
2519
+ function showView(viewName) {
2520
+ document.querySelectorAll('.nav-btn').forEach(btn => btn.classList.remove('active'));
2521
+ document.getElementById('view-' + viewName).classList.add('active');
2522
+
2523
+ document.getElementById('files-view').style.display = viewName === 'files' ? 'block' : 'none';
2524
+ document.getElementById('lines-view').style.display = viewName === 'lines' ? 'block' : 'none';
2525
+ document.getElementById('performance-view').style.display = viewName === 'performance' ? 'block' : 'none';
2526
+ document.getElementById('quality-view').style.display = viewName === 'quality' ? 'block' : 'none';
2527
+ document.getElementById('mutations-view').style.display = viewName === 'mutations' ? 'block' : 'none';
2528
+ document.getElementById('sort-controls').style.display = viewName === 'lines' ? 'flex' : 'none';
2529
+
2530
+ if (viewName === 'lines') {
2531
+ generateLinesTable();
2532
+ } else if (viewName === 'performance') {
2533
+ generatePerformanceDashboard();
2534
+ } else if (viewName === 'quality') {
2535
+ generateQualityDashboard();
2536
+ } else if (viewName === 'mutations') {
2537
+ generateMutationsDashboard();
2538
+ }
2539
+ }
2540
+
2541
+ function toggleFile(fileId) {
2542
+ const content = document.getElementById('content-' + fileId);
2543
+ const header = content.previousElementSibling;
2544
+ const icon = header.querySelector('.expand-icon');
2545
+
2546
+ if (content.classList.contains('expanded')) {
2547
+ content.classList.remove('expanded');
2548
+ header.classList.remove('expanded');
2549
+ icon.textContent = '▶';
2550
+ } else {
2551
+ content.classList.add('expanded');
2552
+ header.classList.add('expanded');
2553
+ icon.textContent = '▼';
2554
+ }
2555
+ }
2556
+
2557
+ function expandAll() {
2558
+ document.querySelectorAll('.file-content').forEach(content => {
2559
+ content.classList.add('expanded');
2560
+ const header = content.previousElementSibling;
2561
+ const icon = header.querySelector('.expand-icon');
2562
+ header.classList.add('expanded');
2563
+ icon.textContent = '▼';
2564
+ });
2565
+ }
2566
+
2567
+ function collapseAll() {
2568
+ document.querySelectorAll('.file-content').forEach(content => {
2569
+ content.classList.remove('expanded');
2570
+ const header = content.previousElementSibling;
2571
+ const icon = header.querySelector('.expand-icon');
2572
+ header.classList.remove('expanded');
2573
+ icon.textContent = '▶';
2574
+ });
2575
+ }
2576
+
2577
+ function generateLinesTable() {
2578
+ const sortBy = document.getElementById('sort-by').value;
2579
+ const sortedLines = [...window.linesData].sort((a, b) => {
2580
+ switch (sortBy) {
2581
+ case 'executions':
2582
+ return b.totalExecutions - a.totalExecutions;
2583
+ case 'tests':
2584
+ return b.testCount - a.testCount;
2585
+ case 'depth':
2586
+ return b.maxDepth - a.maxDepth;
2587
+ case 'cpuCycles':
2588
+ return (b.performance?.totalCpuCycles || 0) - (a.performance?.totalCpuCycles || 0);
2589
+ case 'cpuTime':
2590
+ return (b.performance?.totalCpuTime || 0) - (a.performance?.totalCpuTime || 0);
2591
+ case 'wallTime':
2592
+ return (b.performance?.totalWallTime || 0) - (a.performance?.totalWallTime || 0);
2593
+ case 'quality':
2594
+ return (b.quality?.qualityScore || 0) - (a.quality?.qualityScore || 0);
2595
+ case 'reliability':
2596
+ return (b.quality?.avgReliability || 0) - (a.quality?.avgReliability || 0);
2597
+ case 'maintainability':
2598
+ return (b.quality?.avgMaintainability || 0) - (a.quality?.avgMaintainability || 0);
2599
+ case 'testSmells':
2600
+ return (b.quality?.totalTestSmells || 0) - (a.quality?.totalTestSmells || 0);
2601
+ case 'file':
2602
+ return a.fileName.localeCompare(b.fileName);
2603
+ default:
2604
+ return 0;
2605
+ }
2606
+ });
2607
+
2608
+ let html = \`<table class="lines-table">
2609
+ <thead>
2610
+ <tr>
2611
+ <th>File</th>
2612
+ <th>Line</th>
2613
+ <th>Code Preview</th>
2614
+ <th>Tests</th>
2615
+ <th>Executions</th>
2616
+ <th>CPU Cycles</th>
2617
+ <th>CPU Time (μs)</th>
2618
+ <th>Wall Time (μs)</th>
2619
+ <th>Quality Score</th>
2620
+ <th>Reliability</th>
2621
+ <th>Test Smells</th>
2622
+ <th>Max Depth</th>
2623
+ <th>Depth Range</th>
2624
+ </tr>
2625
+ </thead>
2626
+ <tbody>\`;
2627
+
2628
+ sortedLines.forEach(line => {
2629
+ const depthClass = line.maxDepth <= 1 ? 'depth-1' :
2630
+ line.maxDepth <= 2 ? 'depth-2' :
2631
+ line.maxDepth <= 3 ? 'depth-3' : 'depth-4';
2632
+
2633
+ // Format performance numbers
2634
+ const formatCycles = (cycles) => {
2635
+ if (cycles > 1000000) return \`\${(cycles / 1000000).toFixed(1)}M\`;
2636
+ if (cycles > 1000) return \`\${(cycles / 1000).toFixed(1)}K\`;
2637
+ return Math.round(cycles).toString();
2638
+ };
2639
+
2640
+ const formatTime = (microseconds) => {
2641
+ if (microseconds > 1000) return \`\${(microseconds / 1000).toFixed(2)}ms\`;
2642
+ return \`\${microseconds.toFixed(1)}μs\`;
2643
+ };
2644
+
2645
+ // Format quality scores
2646
+ const formatQuality = (score) => {
2647
+ const rounded = Math.round(score);
2648
+ if (rounded >= 80) return \`<span class="quality-excellent">\${rounded}%</span>\`;
2649
+ if (rounded >= 60) return \`<span class="quality-good">\${rounded}%</span>\`;
2650
+ if (rounded >= 40) return \`<span class="quality-fair">\${rounded}%</span>\`;
2651
+ return \`<span class="quality-poor">\${rounded}%</span>\`;
2652
+ };
2653
+
2654
+ const formatTestSmells = (count, smells) => {
2655
+ if (count === 0) return \`<span class="smells-none">0</span>\`;
2656
+
2657
+ const smellsText = smells && smells.length > 0 ? smells.join(', ') : '';
2658
+ const title = smellsText ? \`title="\${smellsText}"\` : '';
2659
+
2660
+ if (count <= 2) return \`<span class="smells-few" \${title}>\${count}</span>\`;
2661
+ return \`<span class="smells-many" \${title}>\${count}</span>\`;
2662
+ };
2663
+
2664
+ html += \`<tr>
2665
+ <td class="file-name">\${line.fileName}</td>
2666
+ <td class="line-number">\${line.lineNumber}</td>
2667
+ <td class="code-preview">\${line.codePreview}</td>
2668
+ <td class="test-count">\${line.testCount}</td>
2669
+ <td class="execution-count">\${line.totalExecutions}</td>
2670
+ <td class="cpu-cycles">\${formatCycles(line.performance?.totalCpuCycles || 0)}</td>
2671
+ <td class="cpu-time">\${formatTime(line.performance?.totalCpuTime || 0)}</td>
2672
+ <td class="wall-time">\${formatTime(line.performance?.totalWallTime || 0)}</td>
2673
+ <td class="quality-score">\${formatQuality(line.quality?.qualityScore || 0)}</td>
2674
+ <td class="reliability-score">\${formatQuality(line.quality?.avgReliability || 0)}</td>
2675
+ <td class="test-smells">\${formatTestSmells(line.quality?.totalTestSmells || 0, line.quality?.testSmells)}</td>
2676
+ <td class="max-depth"><span class="depth-badge \${depthClass}">D\${line.maxDepth}</span></td>
2677
+ <td class="depth-range">\${line.depthRange}</td>
2678
+ </tr>\`;
2679
+ });
2680
+
2681
+ html += \`</tbody></table>\`;
2682
+ document.getElementById('lines-table').innerHTML = html;
2683
+ }
2684
+
2685
+ function sortLines() {
2686
+ generateLinesTable();
2687
+ }
2688
+
2689
+ function generatePerformanceDashboard() {
2690
+ const performanceData = window.linesData || [];
2691
+
2692
+ // Calculate performance statistics
2693
+ let totalCpuCycles = 0;
2694
+ let totalMemoryUsage = 0;
2695
+ let memoryLeaks = 0;
2696
+ let gcPressureIssues = 0;
2697
+ let slowTests = 0;
2698
+
2699
+ performanceData.forEach(line => {
2700
+ if (line.performance) {
2701
+ totalCpuCycles += line.performance.totalCpuCycles || 0;
2702
+ totalMemoryUsage += Math.abs(line.performance.totalMemoryDelta || 0);
2703
+ memoryLeaks += line.performance.memoryLeaks || 0;
2704
+ gcPressureIssues += line.performance.gcPressure || 0;
2705
+ if ((line.performance.slowExecutions || 0) > (line.performance.fastExecutions || 0)) {
2706
+ slowTests++;
2707
+ }
2708
+ }
2709
+ });
2710
+
2711
+ // Sort by performance metrics
2712
+ const topCpuLines = [...performanceData]
2713
+ .filter(line => line.performance && line.performance.totalCpuCycles > 0)
2714
+ .sort((a, b) => (b.performance.totalCpuCycles || 0) - (a.performance.totalCpuCycles || 0))
2715
+ .slice(0, 10);
2716
+
2717
+ const topMemoryLines = [...performanceData]
2718
+ .filter(line => line.performance && Math.abs(line.performance.totalMemoryDelta || 0) > 0)
2719
+ .sort((a, b) => Math.abs(b.performance.totalMemoryDelta || 0) - Math.abs(a.performance.totalMemoryDelta || 0))
2720
+ .slice(0, 10);
2721
+
2722
+ // Debug logging
2723
+ console.log('Performance Dashboard Data:', {
2724
+ totalLines: performanceData.length,
2725
+ totalCpuCycles,
2726
+ totalMemoryUsage,
2727
+ memoryLeaks,
2728
+ gcPressureIssues,
2729
+ slowTests,
2730
+ sampleLine: performanceData[0]
2731
+ });
2732
+
2733
+ let html = \`
2734
+ <div class="performance-summary">
2735
+ <h3>🔥 Performance Overview</h3>
2736
+ <div class="performance-help">
2737
+ <h4>📚 Understanding Performance Metrics:</h4>
2738
+ <div class="help-section">
2739
+ <strong>🚨 Memory Leaks:</strong> Large allocations (>50KB) that may not be properly cleaned up.
2740
+ <br><em>Action:</em> Review object lifecycle, ensure proper cleanup, avoid global references.
2741
+ </div>
2742
+ <div class="help-section">
2743
+ <strong>🗑️ GC Pressure:</strong> Frequent small allocations that stress the garbage collector.
2744
+ <br><em>Action:</em> Use object pooling, batch operations, avoid creating objects in loops.
2745
+ </div>
2746
+ <div class="help-section">
2747
+ <strong>🐌 Slow Tests:</strong> Tests with inconsistent performance or high variance.
2748
+ <br><em>Action:</em> Use async/await, mock heavy operations, reduce test data size.
2749
+ </div>
2750
+ </div>
2751
+ <div class="perf-stats">
2752
+ <div class="perf-stat">
2753
+ <div class="perf-number">\${(totalCpuCycles / 1000000).toFixed(1)}M</div>
2754
+ <div class="perf-label">Total CPU Cycles</div>
2755
+ </div>
2756
+ <div class="perf-stat">
2757
+ <div class="perf-number">\${(totalMemoryUsage / (1024 * 1024)).toFixed(1)}MB</div>
2758
+ <div class="perf-label">Memory Usage</div>
2759
+ </div>
2760
+ <div class="perf-stat">
2761
+ <div class="perf-number">\${memoryLeaks}</div>
2762
+ <div class="perf-label">Memory Leaks</div>
2763
+ </div>
2764
+ <div class="perf-stat">
2765
+ <div class="perf-number">\${gcPressureIssues}</div>
2766
+ <div class="perf-label">GC Pressure</div>
2767
+ </div>
2768
+ <div class="perf-stat">
2769
+ <div class="perf-number">\${slowTests}</div>
2770
+ <div class="perf-label">Slow Tests</div>
2771
+ </div>
2772
+ </div>
2773
+ </div>
2774
+
2775
+ <div class="performance-tables">
2776
+ <div class="perf-table-container">
2777
+ <h4>🔥 Top CPU Intensive Lines</h4>
2778
+ <table class="perf-table">
2779
+ <thead>
2780
+ <tr><th>File</th><th>Line</th><th>CPU Cycles</th><th>Executions</th></tr>
2781
+ </thead>
2782
+ <tbody>
2783
+ \`;
2784
+
2785
+ topCpuLines.forEach(line => {
2786
+ const cycles = line.performance.totalCpuCycles > 1000000 ?
2787
+ \`\${(line.performance.totalCpuCycles / 1000000).toFixed(1)}M\` :
2788
+ \`\${Math.round(line.performance.totalCpuCycles)}\`;
2789
+ html += \`<tr>
2790
+ <td>\${line.fileName}</td>
2791
+ <td>\${line.lineNumber}</td>
2792
+ <td class="cpu-cycles">\${cycles}</td>
2793
+ <td>\${line.totalExecutions}</td>
2794
+ </tr>\`;
2795
+ });
2796
+
2797
+ html += \`
2798
+ </tbody>
2799
+ </table>
2800
+ </div>
2801
+
2802
+ <div class="perf-table-container">
2803
+ <h4>💾 Top Memory Usage Lines</h4>
2804
+ <table class="perf-table">
2805
+ <thead>
2806
+ <tr><th>File</th><th>Line</th><th>Memory Usage</th><th>Leaks</th></tr>
2807
+ </thead>
2808
+ <tbody>
2809
+ \`;
2810
+
2811
+ topMemoryLines.forEach(line => {
2812
+ const memory = Math.abs(line.performance.totalMemoryDelta || 0);
2813
+ const memoryStr = memory > 1024 * 1024 ?
2814
+ \`\${(memory / (1024 * 1024)).toFixed(1)}MB\` :
2815
+ \`\${(memory / 1024).toFixed(1)}KB\`;
2816
+ html += \`<tr>
2817
+ <td>\${line.fileName}</td>
2818
+ <td>\${line.lineNumber}</td>
2819
+ <td class="memory-usage">\${memoryStr}</td>
2820
+ <td class="memory-leaks">\${line.performance.memoryLeaks || 0}</td>
2821
+ </tr>\`;
2822
+ });
2823
+
2824
+ html += \`
2825
+ </tbody>
2826
+ </table>
2827
+ </div>
2828
+ </div>
2829
+ \`;
2830
+
2831
+ document.getElementById('performance-dashboard').innerHTML = html;
2832
+ }
2833
+
2834
+ function generateQualityDashboard() {
2835
+ const qualityData = window.linesData;
2836
+
2837
+ // Calculate quality statistics
2838
+ let totalTests = 0;
2839
+ let highQuality = 0;
2840
+ let goodQuality = 0;
2841
+ let fairQuality = 0;
2842
+ let poorQuality = 0;
2843
+ let totalSmells = 0;
2844
+ let totalAssertions = 0;
2845
+
2846
+ const smellTypes = {};
2847
+
2848
+ qualityData.forEach(line => {
2849
+ if (line.quality) {
2850
+ totalTests += line.testCount;
2851
+ totalAssertions += line.quality.totalAssertions;
2852
+ totalSmells += line.quality.totalTestSmells;
2853
+
2854
+ const avgQuality = line.quality.qualityScore;
2855
+ if (avgQuality >= 80) highQuality++;
2856
+ else if (avgQuality >= 60) goodQuality++;
2857
+ else if (avgQuality >= 40) fairQuality++;
2858
+ else poorQuality++;
2859
+ }
2860
+ });
2861
+
2862
+ // Sort by quality metrics
2863
+ const topQualityLines = [...qualityData].sort((a, b) => b.quality.qualityScore - a.quality.qualityScore).slice(0, 10);
2864
+ const worstQualityLines = [...qualityData].sort((a, b) => a.quality.qualityScore - b.quality.qualityScore).slice(0, 10);
2865
+
2866
+ let html = \`
2867
+ <div class="quality-summary">
2868
+ <h3>🧪 Test Quality Overview</h3>
2869
+ <div class="quality-stats">
2870
+ <div class="quality-stat">
2871
+ <div class="quality-number">\${totalTests}</div>
2872
+ <div class="quality-label">Total Tests</div>
2873
+ </div>
2874
+ <div class="quality-stat">
2875
+ <div class="quality-number">\${totalAssertions}</div>
2876
+ <div class="quality-label">Total Assertions</div>
2877
+ </div>
2878
+ <div class="quality-stat">
2879
+ <div class="quality-number">\${totalSmells}</div>
2880
+ <div class="quality-label">Test Smells</div>
2881
+ </div>
2882
+ <div class="quality-stat">
2883
+ <div class="quality-number">\${(totalAssertions/totalTests).toFixed(1)}</div>
2884
+ <div class="quality-label">Avg Assertions/Test</div>
2885
+ </div>
2886
+ </div>
2887
+
2888
+ <div class="quality-distribution">
2889
+ <h4>Quality Distribution</h4>
2890
+ <div class="quality-bars">
2891
+ <div class="quality-bar">
2892
+ <span class="quality-excellent">🏆 High Quality: \${highQuality} lines</span>
2893
+ <div class="bar"><div class="fill excellent" style="width: \${(highQuality/qualityData.length)*100}%"></div></div>
2894
+ </div>
2895
+ <div class="quality-bar">
2896
+ <span class="quality-good">✅ Good Quality: \${goodQuality} lines</span>
2897
+ <div class="bar"><div class="fill good" style="width: \${(goodQuality/qualityData.length)*100}%"></div></div>
2898
+ </div>
2899
+ <div class="quality-bar">
2900
+ <span class="quality-fair">⚠️ Fair Quality: \${fairQuality} lines</span>
2901
+ <div class="bar"><div class="fill fair" style="width: \${(fairQuality/qualityData.length)*100}%"></div></div>
2902
+ </div>
2903
+ <div class="quality-bar">
2904
+ <span class="quality-poor">❌ Poor Quality: \${poorQuality} lines</span>
2905
+ <div class="bar"><div class="fill poor" style="width: \${(poorQuality/qualityData.length)*100}%"></div></div>
2906
+ </div>
2907
+ </div>
2908
+ </div>
2909
+ </div>
2910
+
2911
+ <div class="quality-tables">
2912
+ <div class="quality-table-container">
2913
+ <h4>🏆 Highest Quality Tests</h4>
2914
+ <table class="quality-table">
2915
+ <thead>
2916
+ <tr><th>File</th><th>Line</th><th>Quality Score</th><th>Reliability</th><th>Test Smells</th></tr>
2917
+ </thead>
2918
+ <tbody>
2919
+ \`;
2920
+
2921
+ topQualityLines.forEach(line => {
2922
+ const qualityClass = line.quality.qualityScore >= 80 ? 'quality-excellent' :
2923
+ line.quality.qualityScore >= 60 ? 'quality-good' :
2924
+ line.quality.qualityScore >= 40 ? 'quality-fair' : 'quality-poor';
2925
+ html += \`<tr>
2926
+ <td>\${line.fileName}</td>
2927
+ <td>\${line.lineNumber}</td>
2928
+ <td class="\${qualityClass}">\${Math.round(line.quality.qualityScore)}%</td>
2929
+ <td class="\${qualityClass}">\${Math.round(line.quality.avgReliability)}%</td>
2930
+ <td class="smells-\${line.quality.totalTestSmells === 0 ? 'none' : line.quality.totalTestSmells <= 2 ? 'few' : 'many'}">\${line.quality.totalTestSmells}</td>
2931
+ </tr>\`;
2932
+ });
2933
+
2934
+ html += \`
2935
+ </tbody>
2936
+ </table>
2937
+ </div>
2938
+
2939
+ <div class="quality-table-container">
2940
+ <h4>🚨 Tests Needing Improvement</h4>
2941
+ <table class="quality-table">
2942
+ <thead>
2943
+ <tr><th>File</th><th>Line</th><th>Quality Score</th><th>Issues</th><th>Recommendations</th></tr>
2944
+ </thead>
2945
+ <tbody>
2946
+ \`;
2947
+
2948
+ worstQualityLines.forEach(line => {
2949
+ const qualityClass = line.quality.qualityScore >= 80 ? 'quality-excellent' :
2950
+ line.quality.qualityScore >= 60 ? 'quality-good' :
2951
+ line.quality.qualityScore >= 40 ? 'quality-fair' : 'quality-poor';
2952
+
2953
+ let recommendations = [];
2954
+ if (line.quality.totalTestSmells > 0) recommendations.push('Fix test smells');
2955
+ if (line.quality.totalAssertions < 3) recommendations.push('Add more assertions');
2956
+ if (line.quality.avgReliability < 60) recommendations.push('Improve error handling');
2957
+
2958
+ html += \`<tr>
2959
+ <td>\${line.fileName}</td>
2960
+ <td>\${line.lineNumber}</td>
2961
+ <td class="\${qualityClass}">\${Math.round(line.quality.qualityScore)}%</td>
2962
+ <td class="quality-issues">
2963
+ \${line.quality.totalTestSmells} smells\${line.quality.testSmells && line.quality.testSmells.length > 0 ? ' (' + line.quality.testSmells.join(', ') + ')' : ''},
2964
+ \${line.quality.totalAssertions} assertions
2965
+ </td>
2966
+ <td class="recommendations">\${recommendations.join(', ') || 'Good as is'}</td>
2967
+ </tr>\`;
2968
+ });
2969
+
2970
+ html += \`
2971
+ </tbody>
2972
+ </table>
2973
+ </div>
2974
+ </div>
2975
+ \`;
2976
+
2977
+ document.getElementById('quality-dashboard').innerHTML = html;
2978
+ }
2979
+
2980
+ function generateMutationsDashboard() {
2981
+ // Get mutation data from the global variable set by the mutation testing
2982
+ let mutationData = window.mutationTestingResults || ${JSON.stringify(this.mutationResults || {})};
2983
+
2984
+ if (!mutationData || mutationData.totalMutations === undefined || mutationData.totalMutations === 0) {
2985
+ document.getElementById('mutations-dashboard').innerHTML = \`
2986
+ <div class="no-data">
2987
+ <h3>🧬 No Mutation Testing Data Available</h3>
2988
+ <p>Run mutation testing to see results here.</p>
2989
+ </div>
2990
+ \`;
2991
+ return;
2992
+ }
2993
+
2994
+ // Use the actual structure returned by MutationTester
2995
+ const summary = {
2996
+ total: mutationData.totalMutations || 0,
2997
+ killed: mutationData.killedMutations || 0,
2998
+ survived: mutationData.survivedMutations || 0,
2999
+ timeout: mutationData.timeoutMutations || 0,
3000
+ error: mutationData.errorMutations || 0
3001
+ };
3002
+ const mutationScore = mutationData.mutationScore || 0;
3003
+ const scoreClass = mutationScore >= 80 ? 'excellent' : mutationScore >= 60 ? 'good' : mutationScore >= 40 ? 'fair' : 'poor';
3004
+
3005
+ let html = \`
3006
+ <div class="mutations-overview">
3007
+ <div class="mutations-summary-cards">
3008
+ <div class="summary-card \${scoreClass}">
3009
+ <h3>🎯 Mutation Score</h3>
3010
+ <div class="big-number">\${mutationScore}%</div>
3011
+ <div class="subtitle">\${summary.killed}/\${summary.total} mutations killed</div>
3012
+ </div>
3013
+ <div class="summary-card">
3014
+ <h3>🔬 Total Mutations</h3>
3015
+ <div class="big-number">\${summary.total}</div>
3016
+ <div class="subtitle">Generated across all files</div>
3017
+ </div>
3018
+ <div class="summary-card survived">
3019
+ <h3>🔴 Survived</h3>
3020
+ <div class="big-number">\${summary.survived}</div>
3021
+ <div class="subtitle">Mutations not caught by tests</div>
3022
+ </div>
3023
+ <div class="summary-card killed">
3024
+ <h3>✅ Killed</h3>
3025
+ <div class="big-number">\${summary.killed}</div>
3026
+ <div class="subtitle">Mutations caught by tests</div>
3027
+ </div>
3028
+ </div>
3029
+
3030
+ <div class="mutations-chart">
3031
+ <h4>Mutation Results Distribution</h4>
3032
+ <div class="mutation-bars">
3033
+ <div class="mutation-bar">
3034
+ <span class="mutation-killed">✅ Killed: \${summary.killed} mutations</span>
3035
+ <div class="bar"><div class="fill killed" style="width: \${summary.total > 0 ? (summary.killed/summary.total)*100 : 0}%"></div></div>
3036
+ </div>
3037
+ <div class="mutation-bar">
3038
+ <span class="mutation-survived">🔴 Survived: \${summary.survived} mutations</span>
3039
+ <div class="bar"><div class="fill survived" style="width: \${summary.total > 0 ? (summary.survived/summary.total)*100 : 0}%"></div></div>
3040
+ </div>
3041
+ <div class="mutation-bar">
3042
+ <span class="mutation-timeout">⏰ Timeout: \${summary.timeout || 0} mutations</span>
3043
+ <div class="bar"><div class="fill timeout" style="width: \${summary.total > 0 ? ((summary.timeout || 0)/summary.total)*100 : 0}%"></div></div>
3044
+ </div>
3045
+ <div class="mutation-bar">
3046
+ <span class="mutation-error">❌ Error: \${summary.error || 0} mutations</span>
3047
+ <div class="bar"><div class="fill error" style="width: \${summary.total > 0 ? ((summary.error || 0)/summary.total)*100 : 0}%"></div></div>
3048
+ </div>
3049
+ </div>
3050
+ </div>
3051
+ </div>
3052
+ \`;
3053
+
3054
+ // Add file-by-file breakdown if available
3055
+ if (mutationData.fileResults) {
3056
+ html += \`
3057
+ <div class="mutations-files">
3058
+ <h4>📁 File-by-File Results</h4>
3059
+ <table class="mutations-table">
3060
+ <thead>
3061
+ <tr><th>File</th><th>Total</th><th>Killed</th><th>Survived</th><th>Score</th><th>Status</th></tr>
3062
+ </thead>
3063
+ <tbody>
3064
+ \`;
3065
+
3066
+ Object.entries(mutationData.fileResults).forEach(([filePath, fileData]) => {
3067
+ const fileName = filePath.split('/').pop();
3068
+ const fileScore = fileData.totalMutations > 0 ? Math.round((fileData.killedMutations / fileData.totalMutations) * 100) : 0;
3069
+ const fileScoreClass = fileScore >= 80 ? 'excellent' : fileScore >= 60 ? 'good' : fileScore >= 40 ? 'fair' : 'poor';
3070
+ const statusIcon = fileScore >= 80 ? '🏆' : fileScore >= 60 ? '✅' : fileScore >= 40 ? '⚠️' : '❌';
3071
+
3072
+ html += \`<tr class="file-row" onclick="toggleFileDetails('\${filePath.replace(/[^a-zA-Z0-9]/g, '_')}')">
3073
+ <td class="file-name">\${fileName}</td>
3074
+ <td>\${fileData.totalMutations}</td>
3075
+ <td class="killed">\${fileData.killedMutations}</td>
3076
+ <td class="survived">\${fileData.survivedMutations}</td>
3077
+ <td class="\${fileScoreClass}">\${fileScore}%</td>
3078
+ <td>\${statusIcon}</td>
3079
+ </tr>
3080
+ <tr id="details-\${filePath.replace(/[^a-zA-Z0-9]/g, '_')}" class="file-details" style="display: none;">
3081
+ <td colspan="6">
3082
+ <div class="file-mutation-details">
3083
+ \${generateFileDetailedMutations(fileData, filePath)}
3084
+ </div>
3085
+ </td>
3086
+ </tr>\`;
3087
+ });
3088
+
3089
+ html += \`
3090
+ </tbody>
3091
+ </table>
3092
+ </div>
3093
+ \`;
3094
+ }
3095
+
3096
+ document.getElementById('mutations-dashboard').innerHTML = html;
3097
+ }
3098
+
3099
+ function generateFileDetailedMutations(fileData, filePath) {
3100
+ if (!fileData.mutations || fileData.mutations.length === 0) {
3101
+ return '<p>No mutations found for this file.</p>';
3102
+ }
3103
+
3104
+ // Group mutations by line
3105
+ const mutationsByLine = {};
3106
+ fileData.mutations.forEach(mutation => {
3107
+ const line = mutation.line || 'unknown';
3108
+ if (!mutationsByLine[line]) {
3109
+ mutationsByLine[line] = [];
3110
+ }
3111
+ mutationsByLine[line].push(mutation);
3112
+ });
3113
+
3114
+ let html = '<div class="mutations-by-line">';
3115
+
3116
+ Object.entries(mutationsByLine).forEach(([line, mutations]) => {
3117
+ html += \`<div class="line-mutations">
3118
+ <h5>Line \${line} (\${mutations.length} mutations)</h5>
3119
+ <div class="mutations-list">\`;
3120
+
3121
+ mutations.forEach(mutation => {
3122
+ const statusClass = mutation.status === 'killed' ? 'killed' :
3123
+ mutation.status === 'survived' ? 'survived' : 'error';
3124
+ const statusIcon = mutation.status === 'killed' ? '✅' :
3125
+ mutation.status === 'survived' ? '❌' : '⚠️';
3126
+
3127
+ const testsInfo = mutation.killedBy && mutation.killedBy.length > 0 ?
3128
+ \`Killed by: \${mutation.killedBy.join(', ')}\` :
3129
+ mutation.status === 'survived' ? 'No tests killed this mutation' :
3130
+ mutation.error ? \`Error: \${mutation.error}\` : 'Unknown status';
3131
+
3132
+ html += \`<div class="mutation-detail \${statusClass}">
3133
+ <div class="mutation-header">
3134
+ <span class="mutation-status">\${statusIcon} \${mutation.status.toUpperCase()}</span>
3135
+ <span class="mutation-type">\${mutation.mutatorName || mutation.type || 'Unknown'}</span>
3136
+ </div>
3137
+ <div class="mutation-change">
3138
+ <strong>Original:</strong> <code>\${mutation.original || 'N/A'}</code><br>
3139
+ <strong>Mutated:</strong> <code>\${mutation.replacement || 'N/A'}</code>
3140
+ </div>
3141
+ <div class="mutation-tests">\${testsInfo}</div>
3142
+ </div>\`;
3143
+ });
3144
+
3145
+ html += '</div></div>';
3146
+ });
3147
+
3148
+ html += '</div>';
3149
+ return html;
3150
+ }
3151
+
3152
+ function toggleFileDetails(fileId) {
3153
+ const details = document.getElementById('details-' + fileId);
3154
+ if (details) {
3155
+ details.style.display = details.style.display === 'none' ? 'table-row' : 'none';
3156
+ }
3157
+ }
3158
+ </script>
3159
+
3160
+ <!-- Inject mutation testing results -->
3161
+ <script>
3162
+ window.mutationTestingResults = ${JSON.stringify(this.mutationResults || {})};
3163
+ </script>
3164
+ </body>
3165
+ </html>`;
3166
+ }
3167
+
3168
+ /**
3169
+ * Get mutation testing results for a specific line
3170
+ */
3171
+ getMutationResultsForLine(filePath, lineNumber) {
3172
+ if (!this.mutationResults || !this.mutationResults.fileResults) {
3173
+ return [];
3174
+ }
3175
+
3176
+ const fileResults = this.mutationResults.fileResults[filePath];
3177
+ if (!fileResults || !fileResults.lineResults) {
3178
+ return [];
3179
+ }
3180
+
3181
+ const lineResults = fileResults.lineResults[lineNumber];
3182
+ if (!lineResults || !lineResults.mutations) {
3183
+ return [];
3184
+ }
3185
+
3186
+ return lineResults.mutations;
3187
+ }
3188
+
3189
+ /**
3190
+ * Generate HTML for mutation testing results
3191
+ */
3192
+ generateMutationResultsHtml(mutationResults, lineNumber) {
3193
+ const totalMutations = mutationResults.length;
3194
+ const killedMutations = mutationResults.filter(m => m.status === 'killed').length;
3195
+ const survivedMutations = mutationResults.filter(m => m.status === 'survived').length;
3196
+ const errorMutations = mutationResults.filter(m => m.status === 'error').length;
3197
+ const timeoutMutations = mutationResults.filter(m => m.status === 'timeout').length;
3198
+
3199
+ const mutationScore = totalMutations > 0 ? Math.round((killedMutations / totalMutations) * 100) : 0;
3200
+ const scoreClass = mutationScore >= 80 ? 'mutation-score-good' :
3201
+ mutationScore >= 60 ? 'mutation-score-fair' : 'mutation-score-poor';
3202
+
3203
+ let html = `
3204
+ <div class="mutation-results">
3205
+ <h4>🧬 Mutation Testing Results</h4>
3206
+ <div class="mutation-summary">
3207
+ <div class="mutation-score ${scoreClass}">
3208
+ Score: ${mutationScore}%
3209
+ </div>
3210
+ <div class="mutation-stats">
3211
+ <span class="mutation-stat killed">✅ ${killedMutations} killed</span>
3212
+ <span class="mutation-stat survived">🔴 ${survivedMutations} survived</span>`;
3213
+
3214
+ if (errorMutations > 0) {
3215
+ html += `<span class="mutation-stat error">❌ ${errorMutations} error</span>`;
3216
+ }
3217
+ if (timeoutMutations > 0) {
3218
+ html += `<span class="mutation-stat timeout">⏰ ${timeoutMutations} timeout</span>`;
3219
+ }
3220
+
3221
+ html += `
3222
+ </div>
3223
+ </div>
3224
+ <div class="mutation-details">`;
3225
+
3226
+ // Group mutations by status
3227
+ const mutationsByStatus = {
3228
+ survived: mutationResults.filter(m => m.status === 'survived'),
3229
+ killed: mutationResults.filter(m => m.status === 'killed'),
3230
+ error: mutationResults.filter(m => m.status === 'error'),
3231
+ timeout: mutationResults.filter(m => m.status === 'timeout')
3232
+ };
3233
+
3234
+ // Show survived mutations first (most important)
3235
+ if (mutationsByStatus.survived.length > 0) {
3236
+ html += `
3237
+ <div class="mutation-group survived">
3238
+ <h5>🔴 Survived Mutations (${mutationsByStatus.survived.length})</h5>`;
3239
+ mutationsByStatus.survived.forEach(mutation => {
3240
+ html += `
3241
+ <div class="mutation-item survived">
3242
+ <span class="mutation-type">${mutation.mutationType}</span>
3243
+ <span class="mutation-description">${this.getMutationDescription(mutation)}</span>
3244
+ <span class="mutation-tests">Tests run: ${mutation.testsRun || 0}</span>
3245
+ </div>`;
3246
+ });
3247
+ html += `</div>`;
3248
+ }
3249
+
3250
+ // Show killed mutations (collapsed by default)
3251
+ if (mutationsByStatus.killed.length > 0) {
3252
+ html += `
3253
+ <details class="mutation-group killed">
3254
+ <summary>✅ Killed Mutations (${mutationsByStatus.killed.length})</summary>`;
3255
+ mutationsByStatus.killed.forEach(mutation => {
3256
+ html += `
3257
+ <div class="mutation-item killed">
3258
+ <span class="mutation-type">${mutation.mutationType}</span>
3259
+ <span class="mutation-description">${this.getMutationDescription(mutation)}</span>
3260
+ <span class="mutation-tests">Tests run: ${mutation.testsRun || 0}</span>
3261
+ </div>`;
3262
+ });
3263
+ html += `</details>`;
3264
+ }
3265
+
3266
+ // Show errors if any
3267
+ if (mutationsByStatus.error.length > 0) {
3268
+ html += `
3269
+ <details class="mutation-group error">
3270
+ <summary>❌ Error Mutations (${mutationsByStatus.error.length})</summary>`;
3271
+ mutationsByStatus.error.forEach(mutation => {
3272
+ html += `
3273
+ <div class="mutation-item error">
3274
+ <span class="mutation-type">${mutation.mutationType}</span>
3275
+ <span class="mutation-error">${mutation.error || 'Unknown error'}</span>
3276
+ </div>`;
3277
+ });
3278
+ html += `</details>`;
3279
+ }
3280
+
3281
+ html += `
3282
+ </div>
3283
+ </div>`;
3284
+
3285
+ return html;
3286
+ }
3287
+
3288
+ /**
3289
+ * Get a human-readable description of a mutation
3290
+ */
3291
+ getMutationDescription(mutation) {
3292
+ const descriptions = {
3293
+ 'arithmetic': 'Changed arithmetic operator (+, -, *, /, %)',
3294
+ 'comparison': 'Changed comparison operator (==, !=, <, >, <=, >=)',
3295
+ 'logical': 'Changed logical operator (&&, ||, !)',
3296
+ 'conditional': 'Negated conditional statement',
3297
+ 'literals': 'Changed literal value (number, boolean, string)',
3298
+ 'returns': 'Changed return value to null',
3299
+ 'increments': 'Changed increment/decrement operator (++, --)',
3300
+ 'assignment': 'Changed assignment operator (+=, -=, *=, /=)'
3301
+ };
3302
+
3303
+ return descriptions[mutation.mutationType] || `${mutation.mutationType} mutation`;
3304
+ }
3305
+ }
3306
+
3307
+ module.exports = TestCoverageReporter;