elit 3.5.6 → 3.5.8
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.
- package/Cargo.toml +1 -1
- package/README.md +1 -1
- package/desktop/build.rs +83 -0
- package/desktop/icon.rs +106 -0
- package/desktop/lib.rs +2 -0
- package/desktop/main.rs +235 -0
- package/desktop/native_main.rs +128 -0
- package/desktop/native_renderer/action_widgets.rs +184 -0
- package/desktop/native_renderer/app_models.rs +171 -0
- package/desktop/native_renderer/app_runtime.rs +140 -0
- package/desktop/native_renderer/container_rendering.rs +610 -0
- package/desktop/native_renderer/content_widgets.rs +634 -0
- package/desktop/native_renderer/css_models.rs +371 -0
- package/desktop/native_renderer/embedded_surfaces.rs +414 -0
- package/desktop/native_renderer/form_controls.rs +516 -0
- package/desktop/native_renderer/interaction_dispatch.rs +89 -0
- package/desktop/native_renderer/runtime_support.rs +135 -0
- package/desktop/native_renderer/utilities.rs +495 -0
- package/desktop/native_renderer/vector_drawing.rs +491 -0
- package/desktop/native_renderer.rs +4122 -0
- package/desktop/runtime/external.rs +422 -0
- package/desktop/runtime/mod.rs +67 -0
- package/desktop/runtime/quickjs.rs +106 -0
- package/desktop/window.rs +383 -0
- package/dist/build.d.ts +1 -1
- package/dist/cli.cjs +16 -2
- package/dist/cli.mjs +16 -2
- package/dist/config.d.ts +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/desktop-auto-render.cjs +2370 -0
- package/dist/desktop-auto-render.d.ts +13 -0
- package/dist/desktop-auto-render.js +2341 -0
- package/dist/desktop-auto-render.mjs +2344 -0
- package/dist/render-context.cjs +118 -0
- package/dist/render-context.d.ts +39 -0
- package/dist/render-context.js +77 -0
- package/dist/render-context.mjs +87 -0
- package/dist/{server-CNgDUgSZ.d.ts → server-FCdUqabc.d.ts} +1 -1
- package/dist/server.d.ts +1 -1
- package/package.json +26 -3
- package/dist/build.d.mts +0 -20
- package/dist/chokidar.d.mts +0 -134
- package/dist/cli.d.mts +0 -81
- package/dist/config.d.mts +0 -254
- package/dist/coverage.d.mts +0 -85
- package/dist/database.d.mts +0 -52
- package/dist/desktop.d.mts +0 -68
- package/dist/dom.d.mts +0 -87
- package/dist/el.d.mts +0 -208
- package/dist/fs.d.mts +0 -255
- package/dist/hmr.d.mts +0 -38
- package/dist/http.d.mts +0 -169
- package/dist/https.d.mts +0 -108
- package/dist/index.d.mts +0 -13
- package/dist/mime-types.d.mts +0 -48
- package/dist/native.d.mts +0 -136
- package/dist/path.d.mts +0 -163
- package/dist/router.d.mts +0 -49
- package/dist/runtime.d.mts +0 -97
- package/dist/server-D0Dp4R5z.d.mts +0 -449
- package/dist/server.d.mts +0 -7
- package/dist/state.d.mts +0 -117
- package/dist/style.d.mts +0 -232
- package/dist/test-reporter.d.mts +0 -77
- package/dist/test-runtime.d.mts +0 -122
- package/dist/test.d.mts +0 -39
- package/dist/types.d.mts +0 -586
- package/dist/universal.d.mts +0 -21
- package/dist/ws.d.mts +0 -200
- package/dist/wss.d.mts +0 -108
- package/src/build.ts +0 -362
- package/src/chokidar.ts +0 -427
- package/src/cli.ts +0 -1162
- package/src/config.ts +0 -509
- package/src/coverage.ts +0 -1479
- package/src/database.ts +0 -1410
- package/src/desktop-auto-render.ts +0 -317
- package/src/desktop-cli.ts +0 -1533
- package/src/desktop.ts +0 -99
- package/src/dev-build.ts +0 -340
- package/src/dom.ts +0 -901
- package/src/el.ts +0 -183
- package/src/fs.ts +0 -609
- package/src/hmr.ts +0 -149
- package/src/http.ts +0 -856
- package/src/https.ts +0 -411
- package/src/index.ts +0 -16
- package/src/mime-types.ts +0 -222
- package/src/mobile-cli.ts +0 -2313
- package/src/native-background.ts +0 -444
- package/src/native-border.ts +0 -343
- package/src/native-canvas.ts +0 -260
- package/src/native-cli.ts +0 -414
- package/src/native-color.ts +0 -904
- package/src/native-estimation.ts +0 -194
- package/src/native-grid.ts +0 -590
- package/src/native-interaction.ts +0 -1289
- package/src/native-layout.ts +0 -568
- package/src/native-link.ts +0 -76
- package/src/native-render-support.ts +0 -361
- package/src/native-spacing.ts +0 -231
- package/src/native-state.ts +0 -318
- package/src/native-strings.ts +0 -46
- package/src/native-transform.ts +0 -120
- package/src/native-types.ts +0 -439
- package/src/native-typography.ts +0 -254
- package/src/native-units.ts +0 -441
- package/src/native-vector.ts +0 -910
- package/src/native.ts +0 -5606
- package/src/path.ts +0 -493
- package/src/pm-cli.ts +0 -2498
- package/src/preview-build.ts +0 -294
- package/src/render-context.ts +0 -138
- package/src/router.ts +0 -260
- package/src/runtime.ts +0 -97
- package/src/server.ts +0 -2294
- package/src/state.ts +0 -556
- package/src/style.ts +0 -1790
- package/src/test-globals.d.ts +0 -184
- package/src/test-reporter.ts +0 -609
- package/src/test-runtime.ts +0 -1359
- package/src/test.ts +0 -368
- package/src/types.ts +0 -381
- package/src/universal.ts +0 -81
- package/src/wapk-cli.ts +0 -3213
- package/src/workspace-package.ts +0 -102
- package/src/ws.ts +0 -648
- package/src/wss.ts +0 -241
package/src/coverage.ts
DELETED
|
@@ -1,1479 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Coverage collection and reporting with vitest-style output
|
|
3
|
-
*
|
|
4
|
-
* This module provides coverage collection using V8 native coverage
|
|
5
|
-
* with beautiful vitest-style text and HTML reports.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync } from './fs';
|
|
9
|
-
import { dirname, join, relative } from './path';
|
|
10
|
-
import type { TestCoverageReporter } from './types';
|
|
11
|
-
|
|
12
|
-
// Global coverage tracking - stores executed lines for each file
|
|
13
|
-
const executedLinesMap = new Map<string, Set<number>>();
|
|
14
|
-
|
|
15
|
-
// Total executable lines for each file (calculated during test execution)
|
|
16
|
-
const totalLinesMap = new Map<string, number>();
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Get all executable line numbers from a TypeScript source file
|
|
20
|
-
* This analyzes the source to identify which lines actually contain executable code
|
|
21
|
-
*/
|
|
22
|
-
function getExecutableLines(filePath: string): Set<number> {
|
|
23
|
-
const executableLines = new Set<number>();
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const sourceCode = readFileSync(filePath, 'utf-8').toString();
|
|
27
|
-
const lines = sourceCode.split('\n');
|
|
28
|
-
|
|
29
|
-
for (let i = 0; i < lines.length; i++) {
|
|
30
|
-
const line = lines[i];
|
|
31
|
-
const trimmed = line.trim();
|
|
32
|
-
|
|
33
|
-
// Skip non-executable lines
|
|
34
|
-
if (!trimmed ||
|
|
35
|
-
trimmed.startsWith('//') ||
|
|
36
|
-
trimmed.startsWith('*') ||
|
|
37
|
-
trimmed.startsWith('/*') ||
|
|
38
|
-
trimmed.startsWith('*/') ||
|
|
39
|
-
trimmed.startsWith('import ') ||
|
|
40
|
-
(trimmed.startsWith('export ') && !trimmed.includes('function') && !trimmed.includes('class') && !trimmed.includes('const') && !trimmed.includes('let') && !trimmed.includes('var')) ||
|
|
41
|
-
trimmed.startsWith('interface ') ||
|
|
42
|
-
trimmed.startsWith('type ') ||
|
|
43
|
-
trimmed.startsWith('enum ') ||
|
|
44
|
-
trimmed.match(/^class\s+\w+.*{?\s*$/) ||
|
|
45
|
-
trimmed === '{' ||
|
|
46
|
-
trimmed === '}' ||
|
|
47
|
-
trimmed === '();') {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// This is an executable line (1-indexed)
|
|
52
|
-
executableLines.add(i + 1);
|
|
53
|
-
}
|
|
54
|
-
} catch (e) {
|
|
55
|
-
// If we can't read the file, return empty set
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return executableLines;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Mark a file as covered (being tracked)
|
|
63
|
-
* This is called when a file is imported/loaded during tests
|
|
64
|
-
* NOTE: We DON'T mark all lines as executed here - we just track that the file is loaded
|
|
65
|
-
* The coveredFiles Set in test-runner already tracks this
|
|
66
|
-
*/
|
|
67
|
-
export function markFileAsCovered(_filePath: string): void {
|
|
68
|
-
// Don't mark all lines as executed - just track that file is loaded
|
|
69
|
-
// The coveredFiles Set in test-runner already tracks this
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Track that a specific line was executed during testing
|
|
74
|
-
* Call this during test execution to mark lines as covered
|
|
75
|
-
*/
|
|
76
|
-
export function markLineExecuted(filePath: string, lineNumber: number): void {
|
|
77
|
-
if (!executedLinesMap.has(filePath)) {
|
|
78
|
-
executedLinesMap.set(filePath, new Set<number>());
|
|
79
|
-
}
|
|
80
|
-
executedLinesMap.get(filePath)!.add(lineNumber);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get all executed lines for a file
|
|
85
|
-
*/
|
|
86
|
-
export function getExecutedLines(filePath: string): Set<number> {
|
|
87
|
-
return executedLinesMap.get(filePath) || new Set<number>();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Calculate uncovered lines by comparing executable lines vs executed lines
|
|
92
|
-
*/
|
|
93
|
-
export function calculateUncoveredLines(filePath: string): number[] {
|
|
94
|
-
const executableLines = getExecutableLines(filePath);
|
|
95
|
-
const executedLines = getExecutedLines(filePath);
|
|
96
|
-
|
|
97
|
-
const uncovered: number[] = [];
|
|
98
|
-
for (const line of executableLines) {
|
|
99
|
-
if (!executedLines.has(line)) {
|
|
100
|
-
uncovered.push(line);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return uncovered.sort((a, b) => a - b);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Reset coverage tracking (call before running tests)
|
|
109
|
-
*/
|
|
110
|
-
export function resetCoverageTracking(): void {
|
|
111
|
-
executedLinesMap.clear();
|
|
112
|
-
totalLinesMap.clear();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Initialize coverage tracking in the global scope
|
|
117
|
-
* Call this once before running tests
|
|
118
|
-
*/
|
|
119
|
-
export function initializeCoverageTracking(): void {
|
|
120
|
-
// Reset any existing coverage data
|
|
121
|
-
resetCoverageTracking();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface CoverageOptions {
|
|
125
|
-
reportsDirectory: string;
|
|
126
|
-
include?: string[];
|
|
127
|
-
exclude?: string[];
|
|
128
|
-
reporter?: TestCoverageReporter[];
|
|
129
|
-
coveredFiles?: Set<string>; // Set of files that were executed during tests
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface FileCoverage {
|
|
133
|
-
path: string;
|
|
134
|
-
statements: number;
|
|
135
|
-
coveredStatements: number;
|
|
136
|
-
branches: number;
|
|
137
|
-
coveredBranches: number;
|
|
138
|
-
functions: number;
|
|
139
|
-
coveredFunctions: number;
|
|
140
|
-
lines: number; // total lines
|
|
141
|
-
coveredLines: number; // covered lines
|
|
142
|
-
uncoveredLines?: number[]; // line numbers that are not covered (from v8 coverage)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Convert glob pattern to regex using safe character-by-character processing
|
|
147
|
-
* This avoids ReDoS vulnerabilities from using .replace() with regex on user input
|
|
148
|
-
*
|
|
149
|
-
* @param pattern - Glob pattern (e.g., "**/*.test.ts", "src/**/*", "*.js")
|
|
150
|
-
* @returns RegExp object for matching file paths
|
|
151
|
-
*/
|
|
152
|
-
function globToRegex(pattern: string): RegExp {
|
|
153
|
-
let regexStr = '^';
|
|
154
|
-
|
|
155
|
-
// Process pattern character by character to avoid regex on user input
|
|
156
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
157
|
-
const char = pattern[i];
|
|
158
|
-
|
|
159
|
-
switch (char) {
|
|
160
|
-
case '.':
|
|
161
|
-
// Escape literal dot
|
|
162
|
-
regexStr += '\\.';
|
|
163
|
-
break;
|
|
164
|
-
case '*':
|
|
165
|
-
// Handle ** as a special case for matching directories
|
|
166
|
-
if (i + 1 < pattern.length && pattern[i + 1] === '*') {
|
|
167
|
-
// ** matches any number of directories (including none)
|
|
168
|
-
regexStr += '(?:[^/]*(?:\/|$))*';
|
|
169
|
-
i++; // Skip the next *
|
|
170
|
-
} else {
|
|
171
|
-
// * matches any characters except /
|
|
172
|
-
regexStr += '[^/]*';
|
|
173
|
-
}
|
|
174
|
-
break;
|
|
175
|
-
case '?':
|
|
176
|
-
// ? matches exactly one character except /
|
|
177
|
-
regexStr += '[^/]';
|
|
178
|
-
break;
|
|
179
|
-
case '/':
|
|
180
|
-
// Match directory separator
|
|
181
|
-
regexStr += '/';
|
|
182
|
-
break;
|
|
183
|
-
// Escape special regex characters
|
|
184
|
-
case '^':
|
|
185
|
-
case '$':
|
|
186
|
-
case '+':
|
|
187
|
-
case '(':
|
|
188
|
-
case ')':
|
|
189
|
-
case '[':
|
|
190
|
-
case ']':
|
|
191
|
-
case '{':
|
|
192
|
-
case '}':
|
|
193
|
-
case '|':
|
|
194
|
-
case '\\':
|
|
195
|
-
regexStr += '\\' + char;
|
|
196
|
-
break;
|
|
197
|
-
default:
|
|
198
|
-
// Literal character
|
|
199
|
-
regexStr += char;
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
regexStr += '$';
|
|
205
|
-
return new RegExp(regexStr);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Check if a file path matches any of the include patterns
|
|
210
|
-
*/
|
|
211
|
-
function matchesInclude(filePath: string, include: string[]): boolean {
|
|
212
|
-
if (include.length === 0) return true;
|
|
213
|
-
|
|
214
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
215
|
-
|
|
216
|
-
for (const pattern of include) {
|
|
217
|
-
const regex = globToRegex(pattern);
|
|
218
|
-
if (regex.test(normalizedPath)) {
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Check if a file path matches any of the exclude patterns
|
|
227
|
-
*/
|
|
228
|
-
function matchesExclude(filePath: string, exclude: string[]): boolean {
|
|
229
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
230
|
-
|
|
231
|
-
for (const pattern of exclude) {
|
|
232
|
-
const regex = globToRegex(pattern);
|
|
233
|
-
if (regex.test(normalizedPath)) {
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Find all TypeScript files in a directory
|
|
242
|
-
*/
|
|
243
|
-
function findAllTypeScriptFiles(dir: string, include: string[], exclude: string[]): string[] {
|
|
244
|
-
const files: string[] = [];
|
|
245
|
-
|
|
246
|
-
if (!existsSync(dir)) {
|
|
247
|
-
return files;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
252
|
-
|
|
253
|
-
for (const entry of entries) {
|
|
254
|
-
if (typeof entry === 'string') continue;
|
|
255
|
-
|
|
256
|
-
const fullPath = join(dir, entry.name);
|
|
257
|
-
|
|
258
|
-
if (entry.isDirectory()) {
|
|
259
|
-
if (matchesExclude(fullPath, exclude)) continue;
|
|
260
|
-
files.push(...findAllTypeScriptFiles(fullPath, include, exclude));
|
|
261
|
-
} else if (entry.isFile() && fullPath.endsWith('.ts')) {
|
|
262
|
-
if (matchesInclude(fullPath, include) && !matchesExclude(fullPath, exclude)) {
|
|
263
|
-
files.push(fullPath);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
} catch (e) {
|
|
268
|
-
// Ignore permission errors
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return files;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Read source file and count executable lines
|
|
276
|
-
*/
|
|
277
|
-
function analyzeSourceFile(filePath: string): { statements: number; branches: number; functions: number; lines: number } {
|
|
278
|
-
try {
|
|
279
|
-
const sourceCode = readFileSync(filePath, 'utf-8').toString();
|
|
280
|
-
const lines = sourceCode.split('\n');
|
|
281
|
-
|
|
282
|
-
let statements = 0;
|
|
283
|
-
let branches = 0;
|
|
284
|
-
let functions = 0;
|
|
285
|
-
let executableLines = 0;
|
|
286
|
-
|
|
287
|
-
const branchKeywords = ['if', 'else if', 'for', 'while', 'switch', 'case', 'catch', '?', '&&', '||'];
|
|
288
|
-
const functionPatterns = [/function\s+\w+/, /(\w+)\s*\([^)]*\)\s*{/, /\(\s*\w+\s*(?:,\s*\w+\s*)*\)\s*=>/];
|
|
289
|
-
|
|
290
|
-
for (const line of lines) {
|
|
291
|
-
const trimmed = line.trim();
|
|
292
|
-
|
|
293
|
-
// Skip empty lines, comments, and type-only declarations
|
|
294
|
-
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*') ||
|
|
295
|
-
trimmed.startsWith('import ') || trimmed.startsWith('export ') ||
|
|
296
|
-
trimmed.startsWith('interface ') || trimmed.startsWith('type ') ||
|
|
297
|
-
trimmed.startsWith('enum ') || trimmed.match(/^class\s+\w+/)) {
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Count branches
|
|
302
|
-
for (const keyword of branchKeywords) {
|
|
303
|
-
if (trimmed.includes(keyword)) {
|
|
304
|
-
branches++;
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Count functions
|
|
310
|
-
for (const pattern of functionPatterns) {
|
|
311
|
-
if (pattern.test(trimmed)) {
|
|
312
|
-
functions++;
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Count statements (lines with actual code)
|
|
318
|
-
const codeOnly = trimmed
|
|
319
|
-
.replace(/\{|\}|\(|\)|;$/g, '')
|
|
320
|
-
.replace(/^import\s+.*$/, '')
|
|
321
|
-
.replace(/^export\s+.*$/, '')
|
|
322
|
-
.replace(/^interface\s+.*$/, '')
|
|
323
|
-
.replace(/^type\s+.*$/, '')
|
|
324
|
-
.replace(/^enum\s+.*$/, '')
|
|
325
|
-
.replace(/^class\s+\w+.*$/, '')
|
|
326
|
-
.trim();
|
|
327
|
-
|
|
328
|
-
if (codeOnly && codeOnly.length > 0) {
|
|
329
|
-
statements++;
|
|
330
|
-
executableLines++;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return { statements, branches, functions, lines: executableLines };
|
|
335
|
-
} catch (e) {
|
|
336
|
-
return { statements: 0, branches: 0, functions: 0, lines: 0 };
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Process coverage data and map it to source files
|
|
342
|
-
*/
|
|
343
|
-
export async function processCoverage(options: CoverageOptions): Promise<Map<string, FileCoverage>> {
|
|
344
|
-
const {
|
|
345
|
-
include = ['**/*.ts'],
|
|
346
|
-
exclude = ['**/*.test.ts', '**/*.spec.ts', '**/node_modules/**', '**/dist/**', '**/coverage/**'],
|
|
347
|
-
coveredFiles,
|
|
348
|
-
} = options;
|
|
349
|
-
|
|
350
|
-
const coverageMap = new Map<string, FileCoverage>();
|
|
351
|
-
|
|
352
|
-
// Note: We use static analysis instead of V8 coverage
|
|
353
|
-
// V8 coverage has limitations with dynamically transpiled files
|
|
354
|
-
|
|
355
|
-
// Find all TypeScript files in current directory
|
|
356
|
-
const allTsFiles = findAllTypeScriptFiles(process.cwd(), include, exclude);
|
|
357
|
-
|
|
358
|
-
for (const tsFile of allTsFiles) {
|
|
359
|
-
// Check if this file was executed (imported/loaded) during tests
|
|
360
|
-
const isCovered = coveredFiles?.has(tsFile) || false;
|
|
361
|
-
|
|
362
|
-
// Analyze source file to get statement/branch/function counts
|
|
363
|
-
const analysis = analyzeSourceFile(tsFile);
|
|
364
|
-
|
|
365
|
-
// Get executable lines
|
|
366
|
-
const executableLines = getExecutableLines(tsFile);
|
|
367
|
-
|
|
368
|
-
// For covered files that are imported and tested, we assume ALL executable lines are covered
|
|
369
|
-
// This is a limitation of static analysis - we can't track actual line execution without V8 instrumentation
|
|
370
|
-
// If a file is imported during tests, we assume the tests exercise the exported functions
|
|
371
|
-
const executedLines = isCovered ? executableLines : new Set<number>();
|
|
372
|
-
|
|
373
|
-
// Calculate uncovered lines
|
|
374
|
-
const uncoveredLinesArray: number[] = [];
|
|
375
|
-
for (const line of executableLines) {
|
|
376
|
-
if (!executedLines.has(line)) {
|
|
377
|
-
uncoveredLinesArray.push(line);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const uncoveredLines = uncoveredLinesArray.length > 0 ? uncoveredLinesArray.sort((a, b) => a - b) : undefined;
|
|
381
|
-
|
|
382
|
-
// Calculate covered lines
|
|
383
|
-
const coveredLinesCount = executedLines.size;
|
|
384
|
-
|
|
385
|
-
// Add file with coverage data
|
|
386
|
-
coverageMap.set(tsFile, {
|
|
387
|
-
path: tsFile,
|
|
388
|
-
statements: analysis.statements,
|
|
389
|
-
coveredStatements: isCovered ? analysis.statements : 0,
|
|
390
|
-
branches: analysis.branches,
|
|
391
|
-
coveredBranches: isCovered ? analysis.branches : 0,
|
|
392
|
-
functions: analysis.functions,
|
|
393
|
-
coveredFunctions: isCovered ? analysis.functions : 0,
|
|
394
|
-
lines: executableLines.size,
|
|
395
|
-
coveredLines: coveredLinesCount,
|
|
396
|
-
uncoveredLines: uncoveredLines,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Also include any covered files that are not in the current directory (e.g., linked package source files)
|
|
401
|
-
if (coveredFiles) {
|
|
402
|
-
for (const coveredFile of coveredFiles) {
|
|
403
|
-
// Skip if already in coverage map
|
|
404
|
-
if (coverageMap.has(coveredFile)) continue;
|
|
405
|
-
|
|
406
|
-
// Skip files that are outside the current project directory (linked packages)
|
|
407
|
-
// This excludes files like ../../src/* from the main elit package
|
|
408
|
-
const relativePath = relative(process.cwd(), coveredFile);
|
|
409
|
-
const isOutsideProject = relativePath.startsWith('..');
|
|
410
|
-
|
|
411
|
-
// Only include files that are:
|
|
412
|
-
// - Not in node_modules or dist
|
|
413
|
-
// - Within the current project directory (not linked packages)
|
|
414
|
-
if (!coveredFile.includes('node_modules') && !coveredFile.includes('dist') && !isOutsideProject) {
|
|
415
|
-
const analysis = analyzeSourceFile(coveredFile);
|
|
416
|
-
|
|
417
|
-
// Get executable lines
|
|
418
|
-
const executableLines = getExecutableLines(coveredFile);
|
|
419
|
-
|
|
420
|
-
// For covered files that are imported and tested, assume ALL executable lines are covered
|
|
421
|
-
// This is a limitation of static analysis - we can't track actual line execution without V8 instrumentation
|
|
422
|
-
const executedLines = executableLines;
|
|
423
|
-
|
|
424
|
-
// Calculate uncovered lines
|
|
425
|
-
const uncoveredLinesArray: number[] = [];
|
|
426
|
-
for (const line of executableLines) {
|
|
427
|
-
if (!executedLines.has(line)) {
|
|
428
|
-
uncoveredLinesArray.push(line);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const uncoveredLines = uncoveredLinesArray.length > 0 ? uncoveredLinesArray.sort((a, b) => a - b) : undefined;
|
|
432
|
-
|
|
433
|
-
// Calculate covered lines
|
|
434
|
-
const coveredLinesCount = executedLines.size;
|
|
435
|
-
|
|
436
|
-
coverageMap.set(coveredFile, {
|
|
437
|
-
path: coveredFile,
|
|
438
|
-
statements: analysis.statements,
|
|
439
|
-
coveredStatements: analysis.statements,
|
|
440
|
-
branches: analysis.branches,
|
|
441
|
-
coveredBranches: analysis.branches,
|
|
442
|
-
functions: analysis.functions,
|
|
443
|
-
coveredFunctions: analysis.functions,
|
|
444
|
-
lines: executableLines.size,
|
|
445
|
-
coveredLines: coveredLinesCount,
|
|
446
|
-
uncoveredLines: uncoveredLines,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return coverageMap;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* ANSI color codes - vitest style
|
|
457
|
-
*/
|
|
458
|
-
const colors = {
|
|
459
|
-
reset: '\x1b[0m',
|
|
460
|
-
bold: '\x1b[1m',
|
|
461
|
-
dim: '\x1b[2m',
|
|
462
|
-
red: '\x1b[31m',
|
|
463
|
-
green: '\x1b[32m',
|
|
464
|
-
yellow: '\x1b[33m',
|
|
465
|
-
cyan: '\x1b[36m',
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Get color for percentage - vitest style
|
|
470
|
-
*/
|
|
471
|
-
function getColorForPercentage(pct: number): string {
|
|
472
|
-
if (pct >= 80) return colors.green;
|
|
473
|
-
if (pct >= 50) return colors.yellow;
|
|
474
|
-
return colors.red;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Calculate coverage percentages for a file
|
|
479
|
-
*/
|
|
480
|
-
function calculateFileCoverage(file: FileCoverage): {
|
|
481
|
-
statements: { total: number; covered: number; percentage: number };
|
|
482
|
-
branches: { total: number; covered: number; percentage: number };
|
|
483
|
-
functions: { total: number; covered: number; percentage: number };
|
|
484
|
-
lines: { total: number; covered: number; percentage: number };
|
|
485
|
-
} {
|
|
486
|
-
const stmtPct = file.statements > 0 ? (file.coveredStatements / file.statements) * 100 : 0;
|
|
487
|
-
const branchPct = file.branches > 0 ? (file.coveredBranches / file.branches) * 100 : 0;
|
|
488
|
-
const funcPct = file.functions > 0 ? (file.coveredFunctions / file.functions) * 100 : 0;
|
|
489
|
-
const linePct = file.lines > 0 ? (file.coveredLines / file.lines) * 100 : 0;
|
|
490
|
-
|
|
491
|
-
return {
|
|
492
|
-
statements: { total: file.statements, covered: file.coveredStatements, percentage: stmtPct },
|
|
493
|
-
branches: { total: file.branches, covered: file.coveredBranches, percentage: branchPct },
|
|
494
|
-
functions: { total: file.functions, covered: file.coveredFunctions, percentage: funcPct },
|
|
495
|
-
lines: { total: file.lines, covered: file.coveredLines, percentage: linePct },
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Strip ANSI color codes from string for width calculation
|
|
501
|
-
*/
|
|
502
|
-
function stripAnsi(str: string): string {
|
|
503
|
-
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Get visible width of string (excluding ANSI codes)
|
|
508
|
-
*/
|
|
509
|
-
function getVisibleWidth(str: string): number {
|
|
510
|
-
return stripAnsi(str).length;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Format coverage metric with count - vitest style
|
|
515
|
-
* Example: "90.00% ( 9/ 10)"
|
|
516
|
-
* Returns fixed-width string for table alignment
|
|
517
|
-
* Count is padded to ensure consistent width (e.g., " 9/ 10" vs "409/2511")
|
|
518
|
-
*/
|
|
519
|
-
function formatMetricFixedWidth(covered: number, total: number, percentage: number, includeSeparator: boolean = false): string {
|
|
520
|
-
const color = getColorForPercentage(percentage);
|
|
521
|
-
const pctStr = percentage.toFixed(2);
|
|
522
|
-
const pct = color + pctStr + '%' + colors.reset;
|
|
523
|
-
|
|
524
|
-
// Pad count values for consistent width
|
|
525
|
-
// Max covered is ~4 digits (e.g., 2511), max total is ~4 digits
|
|
526
|
-
const coveredPadded = covered.toString().padStart(4);
|
|
527
|
-
const totalPadded = total.toString().padStart(4);
|
|
528
|
-
const count = `${colors.dim}${coveredPadded}${colors.reset}/${totalPadded}`;
|
|
529
|
-
|
|
530
|
-
// Build the metric string (no progress bar, no leading space)
|
|
531
|
-
const metric = `${pct} (${count})`;
|
|
532
|
-
|
|
533
|
-
// Calculate visible width
|
|
534
|
-
const visibleWidth = getVisibleWidth(metric);
|
|
535
|
-
|
|
536
|
-
// Pad to 19 visible characters (20 - 1 for separator)
|
|
537
|
-
const padding = ' '.repeat(Math.max(0, 19 - visibleWidth));
|
|
538
|
-
|
|
539
|
-
// Add separator at the end if requested (except for last column)
|
|
540
|
-
const separator = includeSeparator ? `${colors.dim}│${colors.reset}` : ' ';
|
|
541
|
-
|
|
542
|
-
return metric + padding + separator;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Format uncovered line numbers for display
|
|
547
|
-
* Converts array of line numbers to compact string like "1,3,5-7,10"
|
|
548
|
-
* Also handles case where specific lines were requested by user
|
|
549
|
-
*/
|
|
550
|
-
function formatUncoveredLines(uncoveredLines: number[] | undefined): string {
|
|
551
|
-
if (!uncoveredLines || uncoveredLines.length === 0) {
|
|
552
|
-
return '';
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
const ranges: string[] = [];
|
|
556
|
-
let start = uncoveredLines[0];
|
|
557
|
-
let end = uncoveredLines[0];
|
|
558
|
-
|
|
559
|
-
for (let i = 1; i < uncoveredLines.length; i++) {
|
|
560
|
-
if (uncoveredLines[i] === end + 1) {
|
|
561
|
-
// Consecutive line, extend the range
|
|
562
|
-
end = uncoveredLines[i];
|
|
563
|
-
} else {
|
|
564
|
-
// Non-consecutive, output the current range
|
|
565
|
-
if (start === end) {
|
|
566
|
-
ranges.push(start.toString());
|
|
567
|
-
} else {
|
|
568
|
-
ranges.push(`${start}-${end}`);
|
|
569
|
-
}
|
|
570
|
-
start = uncoveredLines[i];
|
|
571
|
-
end = uncoveredLines[i];
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Add the last range
|
|
576
|
-
if (start === end) {
|
|
577
|
-
ranges.push(start.toString());
|
|
578
|
-
} else {
|
|
579
|
-
ranges.push(`${start}-${end}`);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return ranges.join(',');
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Generate vitest-style text coverage report
|
|
587
|
-
*/
|
|
588
|
-
export function generateTextReport(
|
|
589
|
-
coverageMap: Map<string, FileCoverage>,
|
|
590
|
-
testResults?: any[]
|
|
591
|
-
): string {
|
|
592
|
-
let output = '\n';
|
|
593
|
-
|
|
594
|
-
// testResults can be used for warning indicators (currently reserved for future use)
|
|
595
|
-
void testResults;
|
|
596
|
-
|
|
597
|
-
// Calculate totals
|
|
598
|
-
let totalStatements = 0, coveredStatements = 0;
|
|
599
|
-
let totalBranches = 0, coveredBranches = 0;
|
|
600
|
-
let totalFunctions = 0, coveredFunctions = 0;
|
|
601
|
-
let totalLines = 0, coveredLines = 0;
|
|
602
|
-
|
|
603
|
-
for (const coverage of coverageMap.values()) {
|
|
604
|
-
totalStatements += coverage.statements;
|
|
605
|
-
coveredStatements += coverage.coveredStatements;
|
|
606
|
-
totalBranches += coverage.branches;
|
|
607
|
-
coveredBranches += coverage.coveredBranches;
|
|
608
|
-
totalFunctions += coverage.functions;
|
|
609
|
-
coveredFunctions += coverage.coveredFunctions;
|
|
610
|
-
totalLines += coverage.lines;
|
|
611
|
-
coveredLines += coverage.coveredLines;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const pctStmts = totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0;
|
|
615
|
-
const pctBranch = totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0;
|
|
616
|
-
const pctFunc = totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0;
|
|
617
|
-
const pctLines = totalLines > 0 ? (coveredLines / totalLines) * 100 : 0;
|
|
618
|
-
|
|
619
|
-
// Header - vitest style
|
|
620
|
-
output += `${colors.bold}% Coverage report from v8\x1b[0m\n`;
|
|
621
|
-
output += `\n`;
|
|
622
|
-
|
|
623
|
-
// Summary line with progress bar and totals - vitest style
|
|
624
|
-
output += `${colors.dim}${colors.bold}All files\x1b[0m`;
|
|
625
|
-
|
|
626
|
-
// Calculate width needed for file names
|
|
627
|
-
const maxFileNameLength = Math.max(...Array.from(coverageMap.keys()).map(f => relative(process.cwd(), f).length));
|
|
628
|
-
const namePadding = Math.max(45, maxFileNameLength + 2);
|
|
629
|
-
|
|
630
|
-
output += ' '.repeat(namePadding - 9); // Adjust spacing after "All files"
|
|
631
|
-
|
|
632
|
-
// Statements with count - include separator after first 4 columns (last is uncovered lines)
|
|
633
|
-
const stmtsMetric = formatMetricFixedWidth(coveredStatements, totalStatements, pctStmts, true);
|
|
634
|
-
const branchMetric = formatMetricFixedWidth(coveredBranches, totalBranches, pctBranch, true);
|
|
635
|
-
const funcsMetric = formatMetricFixedWidth(coveredFunctions, totalFunctions, pctFunc, true);
|
|
636
|
-
const linesMetric = formatMetricFixedWidth(coveredLines, totalLines, pctLines, true);
|
|
637
|
-
|
|
638
|
-
output += `${stmtsMetric}${branchMetric}${funcsMetric}${linesMetric}\n`;
|
|
639
|
-
|
|
640
|
-
// Column headers - align to center of each 20-char column
|
|
641
|
-
output += `${colors.dim}`;
|
|
642
|
-
output += ' '.repeat(namePadding); // Full padding (same as data line with "All files" + spaces)
|
|
643
|
-
// Each column is 20 chars wide (19 data + 1 separator)
|
|
644
|
-
// Column 1: "Statements" (10 chars) - centered in 20 chars = 5 spaces before
|
|
645
|
-
output += ' '.repeat(5) + 'Statements'; // position: 5-14 (10 chars)
|
|
646
|
-
// Column 2: "Branch" (6 chars) - need to center in next 20 chars
|
|
647
|
-
output += ' '.repeat(12) + 'Branch'; // position: 26-31 (6 chars)
|
|
648
|
-
// Column 3: "Functions" (9 chars) - need to center in next 20 chars
|
|
649
|
-
output += ' '.repeat(12) + 'Functions'; // position: 43-51 (9 chars)
|
|
650
|
-
// Column 4: "Lines" (5 chars) - need to center in next 20 chars
|
|
651
|
-
output += ' '.repeat(13) + 'Lines'; // position: 64-68 (5 chars)
|
|
652
|
-
// Column 5: "Uncovered" (9 chars) - centered in 20 chars
|
|
653
|
-
output += ' '.repeat(12) + 'Uncovered'; // position: 80-88 (9 chars)
|
|
654
|
-
output += `${colors.reset}\n`;
|
|
655
|
-
|
|
656
|
-
// Separator line under headers with vertical separators
|
|
657
|
-
// Structure: namePadding + 19 + │ + 19 + │ + 19 + │ + 19 + │ + 19
|
|
658
|
-
// Junctions at: namePadding + 19, namePadding + 39, namePadding + 59, namePadding + 79
|
|
659
|
-
output += `${colors.dim}`;
|
|
660
|
-
output += '─'.repeat(namePadding); // ─ across name padding
|
|
661
|
-
output += '─'.repeat(19); // First column data (19 chars)
|
|
662
|
-
output += '┼'; // Junction at namePadding + 19
|
|
663
|
-
output += '─'.repeat(19); // Second column data (19 chars)
|
|
664
|
-
output += '┼'; // Junction at namePadding + 39
|
|
665
|
-
output += '─'.repeat(19); // Third column data (19 chars)
|
|
666
|
-
output += '┼'; // Junction at namePadding + 59
|
|
667
|
-
output += '─'.repeat(19); // Fourth column data (19 chars)
|
|
668
|
-
output += '┼'; // Junction at namePadding + 79
|
|
669
|
-
output += '─'.repeat(19); // Fifth column (Uncovered) - 19 chars
|
|
670
|
-
output += `${colors.reset}\n`;
|
|
671
|
-
|
|
672
|
-
// Group files by directory
|
|
673
|
-
const groupedFiles = new Map<string, Array<{ path: string; coverage: FileCoverage }>>();
|
|
674
|
-
|
|
675
|
-
for (const [filePath, coverage] of coverageMap.entries()) {
|
|
676
|
-
const dir = dirname(filePath);
|
|
677
|
-
if (!groupedFiles.has(dir)) {
|
|
678
|
-
groupedFiles.set(dir, []);
|
|
679
|
-
}
|
|
680
|
-
groupedFiles.get(dir)!.push({ path: filePath, coverage });
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const cwd = process.cwd();
|
|
684
|
-
const toRelative = (path: string) => relative(cwd, path).replace(/\\/g, '/');
|
|
685
|
-
|
|
686
|
-
// Display files grouped by directory
|
|
687
|
-
for (const [dir, files] of groupedFiles.entries()) {
|
|
688
|
-
const relDir = toRelative(dir);
|
|
689
|
-
if (relDir !== '.') {
|
|
690
|
-
output += `\n${colors.cyan}${relDir}/${colors.reset}\n`;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
for (const { path, coverage } of files) {
|
|
694
|
-
const stats = calculateFileCoverage(coverage);
|
|
695
|
-
const relPath = toRelative(path);
|
|
696
|
-
|
|
697
|
-
// Truncate long paths
|
|
698
|
-
let displayName = relPath;
|
|
699
|
-
if (displayName.length > namePadding - 2) {
|
|
700
|
-
displayName = '...' + displayName.slice(-(namePadding - 5));
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
output += displayName.padEnd(namePadding);
|
|
704
|
-
|
|
705
|
-
// Statements with count - fixed width for alignment, include separator
|
|
706
|
-
output += formatMetricFixedWidth(
|
|
707
|
-
stats.statements.covered,
|
|
708
|
-
stats.statements.total,
|
|
709
|
-
stats.statements.percentage,
|
|
710
|
-
true // Include separator
|
|
711
|
-
);
|
|
712
|
-
|
|
713
|
-
// Branches with count, include separator
|
|
714
|
-
output += formatMetricFixedWidth(
|
|
715
|
-
stats.branches.covered,
|
|
716
|
-
stats.branches.total,
|
|
717
|
-
stats.branches.percentage,
|
|
718
|
-
true // Include separator
|
|
719
|
-
);
|
|
720
|
-
|
|
721
|
-
// Functions with count, include separator
|
|
722
|
-
output += formatMetricFixedWidth(
|
|
723
|
-
stats.functions.covered,
|
|
724
|
-
stats.functions.total,
|
|
725
|
-
stats.functions.percentage,
|
|
726
|
-
true // Include separator
|
|
727
|
-
);
|
|
728
|
-
|
|
729
|
-
// Lines with count, include separator
|
|
730
|
-
output += formatMetricFixedWidth(
|
|
731
|
-
stats.lines.covered,
|
|
732
|
-
stats.lines.total,
|
|
733
|
-
stats.lines.percentage,
|
|
734
|
-
true // Include separator
|
|
735
|
-
);
|
|
736
|
-
|
|
737
|
-
// Uncovered lines - variable width, no separator (last column)
|
|
738
|
-
const uncoveredStr = formatUncoveredLines(coverage.uncoveredLines);
|
|
739
|
-
output += `${colors.red}${uncoveredStr}${colors.reset}`;
|
|
740
|
-
|
|
741
|
-
output += '\n';
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Footer summary - vitest style
|
|
746
|
-
output += `\n`;
|
|
747
|
-
output += `${colors.dim}${colors.bold}Test Files\x1b[0m ${coverageMap.size} passed (100%)\n`;
|
|
748
|
-
output += `${colors.dim}${colors.bold}Tests\x1b[0m ${coverageMap.size} passed (100%)\n`;
|
|
749
|
-
output += `\n`;
|
|
750
|
-
output += `${colors.dim}${colors.bold}Statements\x1b[0m ${colors.green}${coveredStatements}${colors.reset} ${colors.dim}/${colors.reset} ${totalStatements}\n`;
|
|
751
|
-
output += `${colors.dim}${colors.bold}Branches\x1b[0m ${colors.green}${coveredBranches}${colors.reset} ${colors.dim}/${colors.reset} ${totalBranches}\n`;
|
|
752
|
-
output += `${colors.dim}${colors.bold}Functions\x1b[0m ${colors.green}${coveredFunctions}${colors.reset} ${colors.dim}/${colors.reset} ${totalFunctions}\n`;
|
|
753
|
-
output += `${colors.dim}${colors.bold}Lines\x1b[0m ${colors.green}${coveredLines}${colors.reset} ${colors.dim}/${colors.reset} ${totalLines}\n`;
|
|
754
|
-
|
|
755
|
-
return output;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
* Generate HTML coverage report - vitest dark theme style
|
|
760
|
-
*/
|
|
761
|
-
export function generateHtmlReport(coverageMap: Map<string, FileCoverage>, reportsDir: string): void {
|
|
762
|
-
if (!existsSync(reportsDir)) {
|
|
763
|
-
mkdirSync(reportsDir, { recursive: true });
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Calculate totals
|
|
767
|
-
let totalStatements = 0, coveredStatements = 0;
|
|
768
|
-
let totalBranches = 0, coveredBranches = 0;
|
|
769
|
-
let totalFunctions = 0, coveredFunctions = 0;
|
|
770
|
-
let totalLines = 0, coveredLines = 0;
|
|
771
|
-
|
|
772
|
-
for (const coverage of coverageMap.values()) {
|
|
773
|
-
totalStatements += coverage.statements;
|
|
774
|
-
coveredStatements += coverage.coveredStatements;
|
|
775
|
-
totalBranches += coverage.branches;
|
|
776
|
-
coveredBranches += coverage.coveredBranches;
|
|
777
|
-
totalFunctions += coverage.functions;
|
|
778
|
-
coveredFunctions += coverage.coveredFunctions;
|
|
779
|
-
totalLines += coverage.lines;
|
|
780
|
-
coveredLines += coverage.coveredLines;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const pctStmts = totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0;
|
|
784
|
-
const pctBranch = totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0;
|
|
785
|
-
const pctFunc = totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0;
|
|
786
|
-
const pctLines = totalLines > 0 ? (coveredLines / totalLines) * 100 : 0;
|
|
787
|
-
const overallPct = (pctStmts + pctBranch + pctFunc + pctLines) / 4;
|
|
788
|
-
|
|
789
|
-
// Count covered vs total files
|
|
790
|
-
const totalFiles = coverageMap.size;
|
|
791
|
-
const coveredFiles = Array.from(coverageMap.values()).filter(c => c.coveredStatements > 0).length;
|
|
792
|
-
|
|
793
|
-
const cwd = process.cwd();
|
|
794
|
-
const toRelative = (path: string) => relative(cwd, path).replace(/\\/g, '/');
|
|
795
|
-
|
|
796
|
-
// Generate index.html with vitest dark theme
|
|
797
|
-
const indexHtml = `<!DOCTYPE html>
|
|
798
|
-
<html>
|
|
799
|
-
<head>
|
|
800
|
-
<meta charset="utf-8">
|
|
801
|
-
<title>Coverage Report</title>
|
|
802
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%236366f1'/%3E%3Cstop offset='100%25' stop-color='%238b5cf6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='100' height='100' rx='20' fill='url(%23grad)'/%3E%3Crect x='28' y='25' width='44' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='46' width='32' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='67' width='44' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='25' width='8' height='50' rx='4' fill='white'/%3E%3Ccircle cx='72' cy='50' r='6' fill='white' opacity='0.5'/%3E%3C/svg%3E">
|
|
803
|
-
<style>
|
|
804
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
805
|
-
body {
|
|
806
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
807
|
-
background: #0d1117;
|
|
808
|
-
color: #c9d1d9;
|
|
809
|
-
padding: 20px;
|
|
810
|
-
}
|
|
811
|
-
.container { max-width: 1400px; margin: 0 auto; }
|
|
812
|
-
h1 {
|
|
813
|
-
font-size: 24px;
|
|
814
|
-
font-weight: 600;
|
|
815
|
-
margin-bottom: 20px;
|
|
816
|
-
color: #58a6ff;
|
|
817
|
-
}
|
|
818
|
-
.overall-bar {
|
|
819
|
-
background: #161b22;
|
|
820
|
-
border: 1px solid #30363d;
|
|
821
|
-
border-radius: 6px;
|
|
822
|
-
padding: 15px 20px;
|
|
823
|
-
margin-bottom: 20px;
|
|
824
|
-
}
|
|
825
|
-
.overall-bar-inner {
|
|
826
|
-
display: flex;
|
|
827
|
-
align-items: center;
|
|
828
|
-
gap: 15px;
|
|
829
|
-
}
|
|
830
|
-
.overall-bar-label {
|
|
831
|
-
font-size: 14px;
|
|
832
|
-
font-weight: 600;
|
|
833
|
-
color: #8b949e;
|
|
834
|
-
min-width: 140px;
|
|
835
|
-
}
|
|
836
|
-
.overall-bar-visual {
|
|
837
|
-
flex: 1;
|
|
838
|
-
height: 24px;
|
|
839
|
-
background: #21262d;
|
|
840
|
-
border-radius: 4px;
|
|
841
|
-
overflow: hidden;
|
|
842
|
-
position: relative;
|
|
843
|
-
}
|
|
844
|
-
.overall-bar-fill {
|
|
845
|
-
height: 100%;
|
|
846
|
-
background: ${overallPct >= 80 ? '#3fb950' : overallPct >= 50 ? '#d29922' : '#f85149'};
|
|
847
|
-
display: flex;
|
|
848
|
-
align-items: center;
|
|
849
|
-
justify-content: center;
|
|
850
|
-
transition: width 0.3s ease;
|
|
851
|
-
}
|
|
852
|
-
.overall-bar-text {
|
|
853
|
-
position: absolute;
|
|
854
|
-
right: 10px;
|
|
855
|
-
top: 50%;
|
|
856
|
-
transform: translateY(-50%);
|
|
857
|
-
font-size: 12px;
|
|
858
|
-
font-weight: 600;
|
|
859
|
-
color: #ffffff;
|
|
860
|
-
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
|
861
|
-
}
|
|
862
|
-
.files-info {
|
|
863
|
-
font-size: 13px;
|
|
864
|
-
color: #8b949e;
|
|
865
|
-
margin-top: 8px;
|
|
866
|
-
}
|
|
867
|
-
.files-info span { color: #58a6ff; font-weight: 600; }
|
|
868
|
-
.summary {
|
|
869
|
-
background: #161b22;
|
|
870
|
-
border: 1px solid #30363d;
|
|
871
|
-
border-radius: 6px;
|
|
872
|
-
padding: 20px;
|
|
873
|
-
margin-bottom: 20px;
|
|
874
|
-
}
|
|
875
|
-
.summary-title {
|
|
876
|
-
font-size: 16px;
|
|
877
|
-
font-weight: 600;
|
|
878
|
-
margin-bottom: 15px;
|
|
879
|
-
color: #c9d1d9;
|
|
880
|
-
}
|
|
881
|
-
.metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }
|
|
882
|
-
.metric {
|
|
883
|
-
background: #21262d;
|
|
884
|
-
border: 1px solid #30363d;
|
|
885
|
-
border-radius: 6px;
|
|
886
|
-
padding: 15px;
|
|
887
|
-
text-align: center;
|
|
888
|
-
}
|
|
889
|
-
.metric-label { font-size: 12px; color: #8b949e; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
890
|
-
.metric-value { font-size: 24px; font-weight: 700; }
|
|
891
|
-
.metric-value.high { color: #3fb950; }
|
|
892
|
-
.metric-value.medium { color: #d29922; }
|
|
893
|
-
.metric-value.low { color: #f85149; }
|
|
894
|
-
.progress-bar {
|
|
895
|
-
height: 8px;
|
|
896
|
-
background: #21262d;
|
|
897
|
-
border-radius: 4px;
|
|
898
|
-
overflow: hidden;
|
|
899
|
-
margin-top: 8px;
|
|
900
|
-
}
|
|
901
|
-
.progress-fill { height: 100%; transition: width 0.3s ease; }
|
|
902
|
-
.progress-fill.high { background: #3fb950; }
|
|
903
|
-
.progress-fill.medium { background: #d29922; }
|
|
904
|
-
.progress-fill.low { background: #f85149; }
|
|
905
|
-
.metric-count { font-size: 11px; color: #8b949e; margin-top: 5px; }
|
|
906
|
-
.file-list {
|
|
907
|
-
background: #161b22;
|
|
908
|
-
border: 1px solid #30363d;
|
|
909
|
-
border-radius: 6px;
|
|
910
|
-
overflow: hidden;
|
|
911
|
-
}
|
|
912
|
-
.file-header {
|
|
913
|
-
display: grid;
|
|
914
|
-
grid-template-columns: 1fr 80px 80px 80px 80px;
|
|
915
|
-
padding: 12px 15px;
|
|
916
|
-
background: #21262d;
|
|
917
|
-
font-size: 12px;
|
|
918
|
-
font-weight: 600;
|
|
919
|
-
color: #8b949e;
|
|
920
|
-
border-bottom: 1px solid #30363d;
|
|
921
|
-
}
|
|
922
|
-
.file-row {
|
|
923
|
-
display: grid;
|
|
924
|
-
grid-template-columns: 1fr 80px 80px 80px 80px;
|
|
925
|
-
padding: 10px 15px;
|
|
926
|
-
border-bottom: 1px solid #21262d;
|
|
927
|
-
font-size: 13px;
|
|
928
|
-
}
|
|
929
|
-
.file-row:hover { background: #21262d; }
|
|
930
|
-
.file-row:last-child { border-bottom: none; }
|
|
931
|
-
.file-name { color: #58a6ff; text-decoration: none; cursor: pointer; }
|
|
932
|
-
.file-name:hover { text-decoration: underline; }
|
|
933
|
-
.percentage { font-weight: 600; }
|
|
934
|
-
.percentage.high { color: #3fb950; }
|
|
935
|
-
.percentage.medium { color: #d29922; }
|
|
936
|
-
.percentage.low { color: #f85149; }
|
|
937
|
-
.metric-detail { font-size: 11px; color: #8b949e; margin-top: 2px; }
|
|
938
|
-
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; margin-left: 8px; }
|
|
939
|
-
.badge.covered { background: #238636; color: #fff; }
|
|
940
|
-
.badge.uncovered { background: #da3633; color: #fff; }
|
|
941
|
-
.coverage-cell { text-align: center; }
|
|
942
|
-
.coverage-percent { font-weight: 600; }
|
|
943
|
-
.coverage-percent.high { color: #3fb950; }
|
|
944
|
-
.coverage-percent.medium { color: #d29922; }
|
|
945
|
-
.coverage-percent.low { color: #f85149; }
|
|
946
|
-
.coverage-count { font-size: 11px; color: #8b949e; margin-top: 2px; }
|
|
947
|
-
.search-container {
|
|
948
|
-
background: #161b22;
|
|
949
|
-
border: 1px solid #30363d;
|
|
950
|
-
border-radius: 6px;
|
|
951
|
-
padding: 15px;
|
|
952
|
-
margin-bottom: 20px;
|
|
953
|
-
}
|
|
954
|
-
.search-input {
|
|
955
|
-
width: 100%;
|
|
956
|
-
padding: 10px 15px;
|
|
957
|
-
background: #21262d;
|
|
958
|
-
border: 1px solid #30363d;
|
|
959
|
-
border-radius: 6px;
|
|
960
|
-
color: #c9d1d9;
|
|
961
|
-
font-size: 14px;
|
|
962
|
-
font-family: inherit;
|
|
963
|
-
outline: none;
|
|
964
|
-
transition: border-color 0.2s ease;
|
|
965
|
-
}
|
|
966
|
-
.search-input:focus {
|
|
967
|
-
border-color: #58a6ff;
|
|
968
|
-
}
|
|
969
|
-
.search-input::placeholder {
|
|
970
|
-
color: #8b949e;
|
|
971
|
-
}
|
|
972
|
-
.hidden { display: none !important; }
|
|
973
|
-
.no-results {
|
|
974
|
-
padding: 20px;
|
|
975
|
-
text-align: center;
|
|
976
|
-
color: #8b949e;
|
|
977
|
-
font-size: 14px;
|
|
978
|
-
}
|
|
979
|
-
</style>
|
|
980
|
-
</head>
|
|
981
|
-
<body>
|
|
982
|
-
<div class="container">
|
|
983
|
-
<h1>Coverage Report</h1>
|
|
984
|
-
|
|
985
|
-
<div class="overall-bar">
|
|
986
|
-
<div class="overall-bar-inner">
|
|
987
|
-
<div class="overall-bar-label">Overall Coverage</div>
|
|
988
|
-
<div class="overall-bar-visual">
|
|
989
|
-
<div class="overall-bar-fill" style="width: ${overallPct}%"></div>
|
|
990
|
-
<div class="overall-bar-text">${overallPct.toFixed(2)}%</div>
|
|
991
|
-
</div>
|
|
992
|
-
</div>
|
|
993
|
-
<div class="files-info"><span>${coveredFiles}</span> of ${totalFiles} files covered</div>
|
|
994
|
-
</div>
|
|
995
|
-
|
|
996
|
-
<div class="summary">
|
|
997
|
-
<div class="summary-title">Coverage Metrics</div>
|
|
998
|
-
<div class="metrics">
|
|
999
|
-
<div class="metric">
|
|
1000
|
-
<div class="metric-label">Statements</div>
|
|
1001
|
-
<div class="metric-value ${pctStmts >= 80 ? 'high' : pctStmts >= 50 ? 'medium' : 'low'}">${pctStmts.toFixed(2)}%</div>
|
|
1002
|
-
<div class="progress-bar">
|
|
1003
|
-
<div class="progress-fill ${pctStmts >= 80 ? 'high' : pctStmts >= 50 ? 'medium' : 'low'}" style="width: ${pctStmts}%"></div>
|
|
1004
|
-
</div>
|
|
1005
|
-
<div class="metric-count">${coveredStatements}/${totalStatements}</div>
|
|
1006
|
-
</div>
|
|
1007
|
-
<div class="metric">
|
|
1008
|
-
<div class="metric-label">Branches</div>
|
|
1009
|
-
<div class="metric-value ${pctBranch >= 80 ? 'high' : pctBranch >= 50 ? 'medium' : 'low'}">${pctBranch.toFixed(2)}%</div>
|
|
1010
|
-
<div class="progress-bar">
|
|
1011
|
-
<div class="progress-fill ${pctBranch >= 80 ? 'high' : pctBranch >= 50 ? 'medium' : 'low'}" style="width: ${pctBranch}%"></div>
|
|
1012
|
-
</div>
|
|
1013
|
-
<div class="metric-count">${coveredBranches}/${totalBranches}</div>
|
|
1014
|
-
</div>
|
|
1015
|
-
<div class="metric">
|
|
1016
|
-
<div class="metric-label">Functions</div>
|
|
1017
|
-
<div class="metric-value ${pctFunc >= 80 ? 'high' : pctFunc >= 50 ? 'medium' : 'low'}">${pctFunc.toFixed(2)}%</div>
|
|
1018
|
-
<div class="progress-bar">
|
|
1019
|
-
<div class="progress-fill ${pctFunc >= 80 ? 'high' : pctFunc >= 50 ? 'medium' : 'low'}" style="width: ${pctFunc}%"></div>
|
|
1020
|
-
</div>
|
|
1021
|
-
<div class="metric-count">${coveredFunctions}/${totalFunctions}</div>
|
|
1022
|
-
</div>
|
|
1023
|
-
<div class="metric">
|
|
1024
|
-
<div class="metric-label">Lines</div>
|
|
1025
|
-
<div class="metric-value ${pctLines >= 80 ? 'high' : pctLines >= 50 ? 'medium' : 'low'}">${pctLines.toFixed(2)}%</div>
|
|
1026
|
-
<div class="progress-bar">
|
|
1027
|
-
<div class="progress-fill ${pctLines >= 80 ? 'high' : pctLines >= 50 ? 'medium' : 'low'}" style="width: ${pctLines}%"></div>
|
|
1028
|
-
</div>
|
|
1029
|
-
<div class="metric-count">${coveredLines}/${totalLines}</div>
|
|
1030
|
-
</div>
|
|
1031
|
-
</div>
|
|
1032
|
-
</div>
|
|
1033
|
-
|
|
1034
|
-
<div class="search-container">
|
|
1035
|
-
<input type="text" id="search-input" class="search-input" placeholder="🔍 Search files..." autocomplete="off">
|
|
1036
|
-
</div>
|
|
1037
|
-
|
|
1038
|
-
<div class="file-list">
|
|
1039
|
-
<div class="file-header">
|
|
1040
|
-
<div>File</div>
|
|
1041
|
-
<div style="text-align: center">Stmts</div>
|
|
1042
|
-
<div style="text-align: center">Branch</div>
|
|
1043
|
-
<div style="text-align: center">Funcs</div>
|
|
1044
|
-
<div style="text-align: center">Lines</div>
|
|
1045
|
-
</div>
|
|
1046
|
-
<div id="file-rows">
|
|
1047
|
-
${Array.from(coverageMap.entries()).map(([filePath, coverage]) => {
|
|
1048
|
-
const stats = calculateFileCoverage(coverage);
|
|
1049
|
-
const fileName = toRelative(filePath);
|
|
1050
|
-
// Create a safe filename for the HTML file
|
|
1051
|
-
const safeFileName = fileName.replace(/[\/\\]/g, '_') + '.html';
|
|
1052
|
-
const isCovered = coverage.coveredStatements > 0;
|
|
1053
|
-
return `
|
|
1054
|
-
<div class="file-row" onclick="window.location.href='${safeFileName}'">
|
|
1055
|
-
<div>
|
|
1056
|
-
<span class="file-name">${fileName}</span>
|
|
1057
|
-
${isCovered ? '<span class="badge covered">Covered</span>' : '<span class="badge uncovered">Not Covered</span>'}
|
|
1058
|
-
</div>
|
|
1059
|
-
<div class="coverage-cell">
|
|
1060
|
-
<div class="coverage-percent ${stats.statements.percentage >= 80 ? 'high' : stats.statements.percentage >= 50 ? 'medium' : 'low'}">${stats.statements.percentage.toFixed(2)}%</div>
|
|
1061
|
-
<div class="coverage-count">${coverage.coveredStatements}/${coverage.statements}</div>
|
|
1062
|
-
</div>
|
|
1063
|
-
<div class="coverage-cell">
|
|
1064
|
-
<div class="coverage-percent ${stats.branches.percentage >= 80 ? 'high' : stats.branches.percentage >= 50 ? 'medium' : 'low'}">${stats.branches.percentage.toFixed(2)}%</div>
|
|
1065
|
-
<div class="coverage-count">${coverage.coveredBranches}/${coverage.branches}</div>
|
|
1066
|
-
</div>
|
|
1067
|
-
<div class="coverage-cell">
|
|
1068
|
-
<div class="coverage-percent ${stats.functions.percentage >= 80 ? 'high' : stats.functions.percentage >= 50 ? 'medium' : 'low'}">${stats.functions.percentage.toFixed(2)}%</div>
|
|
1069
|
-
<div class="coverage-count">${coverage.coveredFunctions}/${coverage.functions}</div>
|
|
1070
|
-
</div>
|
|
1071
|
-
<div class="coverage-cell">
|
|
1072
|
-
<div class="coverage-percent ${stats.lines.percentage >= 80 ? 'high' : stats.lines.percentage >= 50 ? 'medium' : 'low'}">${stats.lines.percentage.toFixed(2)}%</div>
|
|
1073
|
-
<div class="coverage-count">${coverage.coveredLines}/${coverage.lines}</div>
|
|
1074
|
-
</div>
|
|
1075
|
-
</div>
|
|
1076
|
-
`;
|
|
1077
|
-
}).join('')}
|
|
1078
|
-
</div>
|
|
1079
|
-
<div id="no-results" class="no-results hidden">No files found matching your search</div>
|
|
1080
|
-
</div>
|
|
1081
|
-
</div>
|
|
1082
|
-
|
|
1083
|
-
<script>
|
|
1084
|
-
const searchInput = document.getElementById('search-input');
|
|
1085
|
-
const fileRows = document.getElementById('file-rows');
|
|
1086
|
-
const noResults = document.getElementById('no-results');
|
|
1087
|
-
const fileRowElements = fileRows.querySelectorAll('.file-row');
|
|
1088
|
-
|
|
1089
|
-
searchInput.addEventListener('input', function() {
|
|
1090
|
-
const searchTerm = this.value.toLowerCase().trim();
|
|
1091
|
-
let visibleCount = 0;
|
|
1092
|
-
|
|
1093
|
-
fileRowElements.forEach(function(row) {
|
|
1094
|
-
const fileName = row.querySelector('.file-name').textContent.toLowerCase();
|
|
1095
|
-
if (fileName.includes(searchTerm)) {
|
|
1096
|
-
row.classList.remove('hidden');
|
|
1097
|
-
visibleCount++;
|
|
1098
|
-
} else {
|
|
1099
|
-
row.classList.add('hidden');
|
|
1100
|
-
}
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
if (visibleCount === 0) {
|
|
1104
|
-
noResults.classList.remove('hidden');
|
|
1105
|
-
} else {
|
|
1106
|
-
noResults.classList.add('hidden');
|
|
1107
|
-
}
|
|
1108
|
-
});
|
|
1109
|
-
</script>
|
|
1110
|
-
</body>
|
|
1111
|
-
</html>`;
|
|
1112
|
-
|
|
1113
|
-
writeFileSync(join(reportsDir, 'index.html'), indexHtml, 'utf-8');
|
|
1114
|
-
|
|
1115
|
-
// Generate individual file detail pages
|
|
1116
|
-
for (const [filePath, coverage] of coverageMap.entries()) {
|
|
1117
|
-
generateFileDetailPage(filePath, coverage, reportsDir, toRelative);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
/**
|
|
1122
|
-
* Generate an individual file detail page with line-by-line coverage
|
|
1123
|
-
*/
|
|
1124
|
-
function generateFileDetailPage(
|
|
1125
|
-
filePath: string,
|
|
1126
|
-
coverage: FileCoverage,
|
|
1127
|
-
reportsDir: string,
|
|
1128
|
-
toRelative: (path: string) => string
|
|
1129
|
-
): void {
|
|
1130
|
-
const fileName = toRelative(filePath);
|
|
1131
|
-
const safeFileName = fileName.replace(/[\/\\]/g, '_') + '.html';
|
|
1132
|
-
const stats = calculateFileCoverage(coverage);
|
|
1133
|
-
|
|
1134
|
-
// Read source file
|
|
1135
|
-
let sourceLines: string[] = [];
|
|
1136
|
-
try {
|
|
1137
|
-
const sourceCode = readFileSync(filePath, 'utf-8').toString();
|
|
1138
|
-
sourceLines = sourceCode.split('\n');
|
|
1139
|
-
} catch (e) {
|
|
1140
|
-
sourceLines = ['// Unable to read source file'];
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// Build uncovered lines set for quick lookup
|
|
1144
|
-
const uncoveredSet = new Set(coverage.uncoveredLines || []);
|
|
1145
|
-
|
|
1146
|
-
const fileHtml = `<!DOCTYPE html>
|
|
1147
|
-
<html>
|
|
1148
|
-
<head>
|
|
1149
|
-
<meta charset="utf-8">
|
|
1150
|
-
<title>Coverage: ${fileName}</title>
|
|
1151
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%236366f1'/%3E%3Cstop offset='100%25' stop-color='%238b5cf6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='100' height='100' rx='20' fill='url(%23grad)'/%3E%3Crect x='28' y='25' width='44' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='46' width='32' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='67' width='44' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='25' width='8' height='50' rx='4' fill='white'/%3E%3Ccircle cx='72' cy='50' r='6' fill='white' opacity='0.5'/%3E%3C/svg%3E">
|
|
1152
|
-
<style>
|
|
1153
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1154
|
-
body {
|
|
1155
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
1156
|
-
background: #0d1117;
|
|
1157
|
-
color: #c9d1d9;
|
|
1158
|
-
padding: 20px;
|
|
1159
|
-
}
|
|
1160
|
-
.container { max-width: 1400px; margin: 0 auto; }
|
|
1161
|
-
a { color: #58a6ff; text-decoration: none; }
|
|
1162
|
-
a:hover { text-decoration: underline; }
|
|
1163
|
-
h1 {
|
|
1164
|
-
font-size: 24px;
|
|
1165
|
-
font-weight: 600;
|
|
1166
|
-
margin-bottom: 10px;
|
|
1167
|
-
color: #58a6ff;
|
|
1168
|
-
}
|
|
1169
|
-
.breadcrumb {
|
|
1170
|
-
font-size: 14px;
|
|
1171
|
-
color: #8b949e;
|
|
1172
|
-
margin-bottom: 20px;
|
|
1173
|
-
}
|
|
1174
|
-
.breadcrumb a { color: #58a6ff; }
|
|
1175
|
-
.summary {
|
|
1176
|
-
background: #161b22;
|
|
1177
|
-
border: 1px solid #30363d;
|
|
1178
|
-
border-radius: 6px;
|
|
1179
|
-
padding: 20px;
|
|
1180
|
-
margin-bottom: 20px;
|
|
1181
|
-
}
|
|
1182
|
-
.summary-title {
|
|
1183
|
-
font-size: 16px;
|
|
1184
|
-
font-weight: 600;
|
|
1185
|
-
margin-bottom: 15px;
|
|
1186
|
-
color: #c9d1d9;
|
|
1187
|
-
}
|
|
1188
|
-
.metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }
|
|
1189
|
-
.metric {
|
|
1190
|
-
background: #21262d;
|
|
1191
|
-
border: 1px solid #30363d;
|
|
1192
|
-
border-radius: 6px;
|
|
1193
|
-
padding: 15px;
|
|
1194
|
-
text-align: center;
|
|
1195
|
-
}
|
|
1196
|
-
.metric-label { font-size: 12px; color: #8b949e; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
1197
|
-
.metric-value { font-size: 24px; font-weight: 700; }
|
|
1198
|
-
.metric-value.high { color: #3fb950; }
|
|
1199
|
-
.metric-value.medium { color: #d29922; }
|
|
1200
|
-
.metric-value.low { color: #f85149; }
|
|
1201
|
-
.progress-bar {
|
|
1202
|
-
height: 8px;
|
|
1203
|
-
background: #21262d;
|
|
1204
|
-
border-radius: 4px;
|
|
1205
|
-
overflow: hidden;
|
|
1206
|
-
margin-top: 8px;
|
|
1207
|
-
}
|
|
1208
|
-
.progress-fill { height: 100%; transition: width 0.3s ease; }
|
|
1209
|
-
.progress-fill.high { background: #3fb950; }
|
|
1210
|
-
.progress-fill.medium { background: #d29922; }
|
|
1211
|
-
.progress-fill.low { background: #f85149; }
|
|
1212
|
-
.metric-count { font-size: 11px; color: #8b949e; margin-top: 5px; }
|
|
1213
|
-
.code-container {
|
|
1214
|
-
background: #161b22;
|
|
1215
|
-
border: 1px solid #30363d;
|
|
1216
|
-
border-radius: 6px;
|
|
1217
|
-
overflow: hidden;
|
|
1218
|
-
}
|
|
1219
|
-
.code-header {
|
|
1220
|
-
padding: 10px 15px;
|
|
1221
|
-
background: #21262d;
|
|
1222
|
-
border-bottom: 1px solid #30363d;
|
|
1223
|
-
font-size: 13px;
|
|
1224
|
-
color: #8b949e;
|
|
1225
|
-
display: flex;
|
|
1226
|
-
justify-content: space-between;
|
|
1227
|
-
}
|
|
1228
|
-
.legend { display: flex; gap: 15px; font-size: 12px; }
|
|
1229
|
-
.legend-item { display: flex; align-items: center; gap: 5px; }
|
|
1230
|
-
.legend-box { width: 12px; height: 12px; border-radius: 2px; }
|
|
1231
|
-
.legend-box.covered { background: rgba(63, 185, 80, 0.2); border: 1px solid #3fb950; }
|
|
1232
|
-
.legend-box.uncovered { background: rgba(248, 81, 73, 0.2); border: 1px solid #f85149; }
|
|
1233
|
-
.code-table { width: 100%; border-collapse: collapse; }
|
|
1234
|
-
.code-table td { padding: 0; }
|
|
1235
|
-
.line-number {
|
|
1236
|
-
width: 50px;
|
|
1237
|
-
text-align: right;
|
|
1238
|
-
padding: 0 15px;
|
|
1239
|
-
color: #8b949e;
|
|
1240
|
-
font-size: 12px;
|
|
1241
|
-
user-select: none;
|
|
1242
|
-
border-right: 1px solid #30363d;
|
|
1243
|
-
}
|
|
1244
|
-
.line-content {
|
|
1245
|
-
padding: 0 15px;
|
|
1246
|
-
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
1247
|
-
font-size: 13px;
|
|
1248
|
-
line-height: 20px;
|
|
1249
|
-
white-space: pre;
|
|
1250
|
-
}
|
|
1251
|
-
tr.covered .line-content { background: rgba(63, 185, 80, 0.1); }
|
|
1252
|
-
tr.uncovered .line-content { background: rgba(248, 81, 73, 0.15); }
|
|
1253
|
-
tr.uncovered .line-number { color: #f85149; }
|
|
1254
|
-
tr:hover td { background: rgba(88, 166, 255, 0.1); }
|
|
1255
|
-
</style>
|
|
1256
|
-
</head>
|
|
1257
|
-
<body>
|
|
1258
|
-
<div class="container">
|
|
1259
|
-
<div class="breadcrumb">
|
|
1260
|
-
<a href="index.html">← Back to Coverage Report</a>
|
|
1261
|
-
</div>
|
|
1262
|
-
|
|
1263
|
-
<h1>${fileName}</h1>
|
|
1264
|
-
|
|
1265
|
-
<div class="summary">
|
|
1266
|
-
<div class="summary-title">Coverage Metrics</div>
|
|
1267
|
-
<div class="metrics">
|
|
1268
|
-
<div class="metric">
|
|
1269
|
-
<div class="metric-label">Statements</div>
|
|
1270
|
-
<div class="metric-value ${stats.statements.percentage >= 80 ? 'high' : stats.statements.percentage >= 50 ? 'medium' : 'low'}">${stats.statements.percentage.toFixed(2)}%</div>
|
|
1271
|
-
<div class="progress-bar">
|
|
1272
|
-
<div class="progress-fill ${stats.statements.percentage >= 80 ? 'high' : stats.statements.percentage >= 50 ? 'medium' : 'low'}" style="width: ${stats.statements.percentage}%"></div>
|
|
1273
|
-
</div>
|
|
1274
|
-
<div class="metric-count">${coverage.coveredStatements}/${coverage.statements}</div>
|
|
1275
|
-
</div>
|
|
1276
|
-
<div class="metric">
|
|
1277
|
-
<div class="metric-label">Branches</div>
|
|
1278
|
-
<div class="metric-value ${stats.branches.percentage >= 80 ? 'high' : stats.branches.percentage >= 50 ? 'medium' : 'low'}">${stats.branches.percentage.toFixed(2)}%</div>
|
|
1279
|
-
<div class="progress-bar">
|
|
1280
|
-
<div class="progress-fill ${stats.branches.percentage >= 80 ? 'high' : stats.branches.percentage >= 50 ? 'medium' : 'low'}" style="width: ${stats.branches.percentage}%"></div>
|
|
1281
|
-
</div>
|
|
1282
|
-
<div class="metric-count">${coverage.coveredBranches}/${coverage.branches}</div>
|
|
1283
|
-
</div>
|
|
1284
|
-
<div class="metric">
|
|
1285
|
-
<div class="metric-label">Functions</div>
|
|
1286
|
-
<div class="metric-value ${stats.functions.percentage >= 80 ? 'high' : stats.functions.percentage >= 50 ? 'medium' : 'low'}">${stats.functions.percentage.toFixed(2)}%</div>
|
|
1287
|
-
<div class="progress-bar">
|
|
1288
|
-
<div class="progress-fill ${stats.functions.percentage >= 80 ? 'high' : stats.functions.percentage >= 50 ? 'medium' : 'low'}" style="width: ${stats.functions.percentage}%"></div>
|
|
1289
|
-
</div>
|
|
1290
|
-
<div class="metric-count">${coverage.coveredFunctions}/${coverage.functions}</div>
|
|
1291
|
-
</div>
|
|
1292
|
-
<div class="metric">
|
|
1293
|
-
<div class="metric-label">Lines</div>
|
|
1294
|
-
<div class="metric-value ${stats.lines.percentage >= 80 ? 'high' : stats.lines.percentage >= 50 ? 'medium' : 'low'}">${stats.lines.percentage.toFixed(2)}%</div>
|
|
1295
|
-
<div class="progress-bar">
|
|
1296
|
-
<div class="progress-fill ${stats.lines.percentage >= 80 ? 'high' : stats.lines.percentage >= 50 ? 'medium' : 'low'}" style="width: ${stats.lines.percentage}%"></div>
|
|
1297
|
-
</div>
|
|
1298
|
-
<div class="metric-count">${coverage.coveredLines}/${coverage.lines}</div>
|
|
1299
|
-
</div>
|
|
1300
|
-
</div>
|
|
1301
|
-
</div>
|
|
1302
|
-
|
|
1303
|
-
${coverage.uncoveredLines && coverage.uncoveredLines.length > 0 ? `
|
|
1304
|
-
<div class="summary">
|
|
1305
|
-
<div class="summary-title">Uncovered Lines</div>
|
|
1306
|
-
<div style="font-size: 13px; color: #f85149;">${formatUncoveredLines(coverage.uncoveredLines)}</div>
|
|
1307
|
-
</div>
|
|
1308
|
-
` : ''}
|
|
1309
|
-
|
|
1310
|
-
<div class="code-container">
|
|
1311
|
-
<div class="code-header">
|
|
1312
|
-
<span>Source Code</span>
|
|
1313
|
-
<div class="legend">
|
|
1314
|
-
<div class="legend-item"><div class="legend-box covered"></div><span>Covered</span></div>
|
|
1315
|
-
<div class="legend-item"><div class="legend-box uncovered"></div><span>Uncovered</span></div>
|
|
1316
|
-
</div>
|
|
1317
|
-
</div>
|
|
1318
|
-
<table class="code-table">
|
|
1319
|
-
${sourceLines.map((line, index) => {
|
|
1320
|
-
const lineNum = index + 1;
|
|
1321
|
-
const isUncovered = uncoveredSet.has(lineNum);
|
|
1322
|
-
const isExecutable = coverage.lines > 0; // Has executable lines
|
|
1323
|
-
const rowClass = isExecutable ? (isUncovered ? 'uncovered' : 'covered') : '';
|
|
1324
|
-
return `
|
|
1325
|
-
<tr class="${rowClass}">
|
|
1326
|
-
<td class="line-number">${lineNum}</td>
|
|
1327
|
-
<td class="line-content">${escapeHtml(line) || ' '}</td>
|
|
1328
|
-
</tr>
|
|
1329
|
-
`;
|
|
1330
|
-
}).join('')}
|
|
1331
|
-
</table>
|
|
1332
|
-
</div>
|
|
1333
|
-
</div>
|
|
1334
|
-
</body>
|
|
1335
|
-
</html>`;
|
|
1336
|
-
|
|
1337
|
-
writeFileSync(join(reportsDir, safeFileName), fileHtml, 'utf-8');
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
/**
|
|
1341
|
-
* Escape HTML special characters
|
|
1342
|
-
*/
|
|
1343
|
-
function escapeHtml(text: string): string {
|
|
1344
|
-
return text
|
|
1345
|
-
.replace(/&/g, '&')
|
|
1346
|
-
.replace(/</g, '<')
|
|
1347
|
-
.replace(/>/g, '>')
|
|
1348
|
-
.replace(/"/g, '"')
|
|
1349
|
-
.replace(/'/g, ''');
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* Escape XML special characters
|
|
1354
|
-
*/
|
|
1355
|
-
function escapeXml(text: string): string {
|
|
1356
|
-
return text
|
|
1357
|
-
.replace(/&/g, '&')
|
|
1358
|
-
.replace(/</g, '<')
|
|
1359
|
-
.replace(/>/g, '>')
|
|
1360
|
-
.replace(/"/g, '"')
|
|
1361
|
-
.replace(/'/g, ''');
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
/**
|
|
1365
|
-
* Generate coverage-final.json (Code Climate/Codecov format)
|
|
1366
|
-
*/
|
|
1367
|
-
export function generateCoverageFinalJson(
|
|
1368
|
-
coverageMap: Map<string, FileCoverage>,
|
|
1369
|
-
reportsDir: string
|
|
1370
|
-
): void {
|
|
1371
|
-
const coverageData: Record<string, any> = {};
|
|
1372
|
-
|
|
1373
|
-
for (const [filePath, coverage] of coverageMap.entries()) {
|
|
1374
|
-
const relativePath = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
1375
|
-
|
|
1376
|
-
// Build line coverage map
|
|
1377
|
-
const lineMap: Record<number, number> = {};
|
|
1378
|
-
const executableLines = getExecutableLines(filePath);
|
|
1379
|
-
|
|
1380
|
-
for (const line of executableLines) {
|
|
1381
|
-
// If file is covered, mark all lines as covered (1), otherwise not covered (0)
|
|
1382
|
-
const isCovered = coverage.coveredStatements > 0;
|
|
1383
|
-
lineMap[line] = isCovered ? 1 : 0;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
coverageData[relativePath] = {
|
|
1387
|
-
lines: lineMap,
|
|
1388
|
-
};
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
const jsonData = JSON.stringify(coverageData, null, 2);
|
|
1392
|
-
writeFileSync(join(reportsDir, 'coverage-final.json'), jsonData, 'utf-8');
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
/**
|
|
1396
|
-
* Generate clover.xml report
|
|
1397
|
-
*/
|
|
1398
|
-
export function generateCloverXml(
|
|
1399
|
-
coverageMap: Map<string, FileCoverage>,
|
|
1400
|
-
reportsDir: string
|
|
1401
|
-
): void {
|
|
1402
|
-
const timestamp = Date.now();
|
|
1403
|
-
|
|
1404
|
-
// Calculate totals
|
|
1405
|
-
let totalFiles = 0;
|
|
1406
|
-
let totalClasses = 0;
|
|
1407
|
-
let totalElements = 0; // statements + branches + functions
|
|
1408
|
-
let coveredElements = 0;
|
|
1409
|
-
let totalStatements = 0;
|
|
1410
|
-
let coveredStatements = 0;
|
|
1411
|
-
let totalBranches = 0;
|
|
1412
|
-
let coveredBranches = 0;
|
|
1413
|
-
let totalFunctions = 0;
|
|
1414
|
-
let coveredFunctions = 0;
|
|
1415
|
-
let totalLines = 0;
|
|
1416
|
-
let coveredLines = 0;
|
|
1417
|
-
|
|
1418
|
-
const fileEntries: string[] = [];
|
|
1419
|
-
|
|
1420
|
-
for (const [filePath, coverage] of coverageMap.entries()) {
|
|
1421
|
-
const relativePath = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
1422
|
-
|
|
1423
|
-
totalFiles++;
|
|
1424
|
-
totalClasses++; // Assume one class per file
|
|
1425
|
-
totalStatements += coverage.statements;
|
|
1426
|
-
coveredStatements += coverage.coveredStatements;
|
|
1427
|
-
totalBranches += coverage.branches;
|
|
1428
|
-
coveredBranches += coverage.coveredBranches;
|
|
1429
|
-
totalFunctions += coverage.functions;
|
|
1430
|
-
coveredFunctions += coverage.coveredFunctions;
|
|
1431
|
-
totalLines += coverage.lines;
|
|
1432
|
-
coveredLines += coverage.coveredLines;
|
|
1433
|
-
|
|
1434
|
-
const fileElements = coverage.statements + coverage.branches + coverage.functions;
|
|
1435
|
-
const fileCoveredElements = coverage.coveredStatements + coverage.coveredBranches + coverage.coveredFunctions;
|
|
1436
|
-
|
|
1437
|
-
totalElements += fileElements;
|
|
1438
|
-
coveredElements += fileCoveredElements;
|
|
1439
|
-
|
|
1440
|
-
const escapedPath = escapeXml(relativePath);
|
|
1441
|
-
|
|
1442
|
-
fileEntries.push(`
|
|
1443
|
-
<file name="${escapedPath}">
|
|
1444
|
-
<class name="${escapedPath}">
|
|
1445
|
-
<metrics complexity="0" elements="${fileElements}" coveredelements="${fileCoveredElements}"
|
|
1446
|
-
methods="${coverage.functions}" coveredmethods="${coverage.coveredFunctions}"
|
|
1447
|
-
statements="${coverage.statements}" coveredstatements="${coverage.coveredStatements}" />
|
|
1448
|
-
</class>
|
|
1449
|
-
<line num="1" type="stmt" count="${coverage.coveredStatements > 0 ? 1 : 0}" />
|
|
1450
|
-
</file>`);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
const complexity = 0; // We don't track cyclomatic complexity
|
|
1454
|
-
const elements = totalElements;
|
|
1455
|
-
|
|
1456
|
-
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1457
|
-
<coverage generated="${timestamp}" clover="3.2.0">
|
|
1458
|
-
<project timestamp="${timestamp}" name="Coverage">
|
|
1459
|
-
<metrics complexity="${complexity}" elements="${elements}" coveredelements="${coveredElements}"
|
|
1460
|
-
conditionals="${totalBranches}" coveredconditionals="${coveredBranches}"
|
|
1461
|
-
statements="${totalStatements}" coveredstatements="${coveredStatements}"
|
|
1462
|
-
methods="${totalFunctions}" coveredmethods="${coveredFunctions}"
|
|
1463
|
-
classes="${totalClasses}" coveredclasses="${coverageMap.size > 0 ? Array.from(coverageMap.values()).filter(c => c.coveredStatements > 0).length : 0}"
|
|
1464
|
-
files="${totalFiles}" loc="${totalLines}" ncloc="${totalLines - coveredLines}"
|
|
1465
|
-
packages="${totalFiles}" classes="${totalClasses}" />
|
|
1466
|
-
<package name="root">
|
|
1467
|
-
<metrics complexity="${complexity}" elements="${elements}" coveredelements="${coveredElements}"
|
|
1468
|
-
conditionals="${totalBranches}" coveredconditionals="${coveredBranches}"
|
|
1469
|
-
statements="${totalStatements}" coveredstatements="${coveredStatements}"
|
|
1470
|
-
methods="${totalFunctions}" coveredmethods="${coveredFunctions}"
|
|
1471
|
-
classes="${totalClasses}" coveredclasses="${coverageMap.size > 0 ? Array.from(coverageMap.values()).filter(c => c.coveredStatements > 0).length : 0}"
|
|
1472
|
-
files="${totalFiles}" loc="${totalLines}" ncloc="${totalLines - coveredLines}" />
|
|
1473
|
-
${fileEntries.join('')}
|
|
1474
|
-
</package>
|
|
1475
|
-
</project>
|
|
1476
|
-
</coverage>`;
|
|
1477
|
-
|
|
1478
|
-
writeFileSync(join(reportsDir, 'clover.xml'), xml, 'utf-8');
|
|
1479
|
-
}
|