elit 3.3.3 → 3.3.5

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.
Files changed (145) hide show
  1. package/dist/build.d.mts +1 -1
  2. package/dist/build.js +1 -0
  3. package/dist/build.js.map +1 -0
  4. package/dist/build.mjs +1 -0
  5. package/dist/build.mjs.map +1 -0
  6. package/dist/chokidar.js +1 -0
  7. package/dist/chokidar.js.map +1 -0
  8. package/dist/chokidar.mjs +1 -0
  9. package/dist/chokidar.mjs.map +1 -0
  10. package/dist/cli.js +4720 -37
  11. package/dist/config.d.mts +3 -1
  12. package/dist/config.d.ts +3 -1
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +1 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/config.mjs +1 -0
  17. package/dist/config.mjs.map +1 -0
  18. package/dist/coverage.d.mts +85 -0
  19. package/dist/coverage.d.ts +76 -0
  20. package/dist/coverage.d.ts.map +1 -0
  21. package/dist/coverage.js +1549 -0
  22. package/dist/coverage.js.map +1 -0
  23. package/dist/coverage.mjs +1520 -0
  24. package/dist/coverage.mjs.map +1 -0
  25. package/dist/database.d.mts +31 -6
  26. package/dist/database.d.ts +31 -6
  27. package/dist/database.d.ts.map +1 -1
  28. package/dist/database.js +60 -33
  29. package/dist/database.js.map +1 -0
  30. package/dist/database.mjs +60 -33
  31. package/dist/database.mjs.map +1 -0
  32. package/dist/dom.js +1 -0
  33. package/dist/dom.js.map +1 -0
  34. package/dist/dom.mjs +1 -0
  35. package/dist/dom.mjs.map +1 -0
  36. package/dist/el.js +1 -0
  37. package/dist/el.js.map +1 -0
  38. package/dist/el.mjs +1 -0
  39. package/dist/el.mjs.map +1 -0
  40. package/dist/fs.js +1 -0
  41. package/dist/fs.js.map +1 -0
  42. package/dist/fs.mjs +1 -0
  43. package/dist/fs.mjs.map +1 -0
  44. package/dist/hmr.js +1 -0
  45. package/dist/hmr.js.map +1 -0
  46. package/dist/hmr.mjs +1 -0
  47. package/dist/hmr.mjs.map +1 -0
  48. package/dist/http.js +1 -0
  49. package/dist/http.js.map +1 -0
  50. package/dist/http.mjs +1 -0
  51. package/dist/http.mjs.map +1 -0
  52. package/dist/https.d.mts +1 -1
  53. package/dist/https.js +1 -0
  54. package/dist/https.js.map +1 -0
  55. package/dist/https.mjs +1 -0
  56. package/dist/https.mjs.map +1 -0
  57. package/dist/index.d.mts +1 -1
  58. package/dist/index.js +1 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/index.mjs +1 -0
  61. package/dist/index.mjs.map +1 -0
  62. package/dist/mime-types.js +1 -0
  63. package/dist/mime-types.js.map +1 -0
  64. package/dist/mime-types.mjs +1 -0
  65. package/dist/mime-types.mjs.map +1 -0
  66. package/dist/path.js +1 -0
  67. package/dist/path.js.map +1 -0
  68. package/dist/path.mjs +1 -0
  69. package/dist/path.mjs.map +1 -0
  70. package/dist/router.js +1 -0
  71. package/dist/router.js.map +1 -0
  72. package/dist/router.mjs +1 -0
  73. package/dist/router.mjs.map +1 -0
  74. package/dist/runtime.js +1 -0
  75. package/dist/runtime.js.map +1 -0
  76. package/dist/runtime.mjs +1 -0
  77. package/dist/runtime.mjs.map +1 -0
  78. package/dist/{server-Cz3z-5ls.d.mts → server-BFTzgJpO.d.mts} +62 -1
  79. package/dist/{server-BG2CaVMh.d.ts → server-CIXtexNS.d.ts} +62 -1
  80. package/dist/server.d.mts +1 -1
  81. package/dist/server.d.ts +9 -0
  82. package/dist/server.d.ts.map +1 -1
  83. package/dist/server.js +45 -3
  84. package/dist/server.js.map +1 -0
  85. package/dist/server.mjs +45 -3
  86. package/dist/server.mjs.map +1 -0
  87. package/dist/state.d.mts +1 -1
  88. package/dist/state.js +1 -0
  89. package/dist/state.js.map +1 -0
  90. package/dist/state.mjs +1 -0
  91. package/dist/state.mjs.map +1 -0
  92. package/dist/style.js +1 -0
  93. package/dist/style.js.map +1 -0
  94. package/dist/style.mjs +1 -0
  95. package/dist/style.mjs.map +1 -0
  96. package/dist/test-globals.d.ts +184 -0
  97. package/dist/test-reporter.d.mts +77 -0
  98. package/dist/test-reporter.d.ts +73 -0
  99. package/dist/test-reporter.d.ts.map +1 -0
  100. package/dist/test-reporter.js +726 -0
  101. package/dist/test-reporter.js.map +1 -0
  102. package/dist/test-reporter.mjs +696 -0
  103. package/dist/test-reporter.mjs.map +1 -0
  104. package/dist/test-runtime.d.mts +122 -0
  105. package/dist/test-runtime.d.ts +120 -0
  106. package/dist/test-runtime.d.ts.map +1 -0
  107. package/dist/test-runtime.js +1292 -0
  108. package/dist/test-runtime.js.map +1 -0
  109. package/dist/test-runtime.mjs +1269 -0
  110. package/dist/test-runtime.mjs.map +1 -0
  111. package/dist/test.d.mts +39 -0
  112. package/dist/test.d.ts +38 -0
  113. package/dist/test.d.ts.map +1 -0
  114. package/dist/test.js +4966 -0
  115. package/dist/test.js.map +1 -0
  116. package/dist/test.mjs +4944 -0
  117. package/dist/test.mjs.map +1 -0
  118. package/dist/types.d.mts +62 -1
  119. package/dist/types.d.ts +52 -0
  120. package/dist/types.d.ts.map +1 -1
  121. package/dist/types.js +1 -0
  122. package/dist/types.js.map +1 -0
  123. package/dist/types.mjs +1 -0
  124. package/dist/types.mjs.map +1 -0
  125. package/dist/ws.d.ts.map +1 -1
  126. package/dist/ws.js +24 -2
  127. package/dist/ws.js.map +1 -0
  128. package/dist/ws.mjs +24 -2
  129. package/dist/ws.mjs.map +1 -0
  130. package/dist/wss.js +24 -2
  131. package/dist/wss.js.map +1 -0
  132. package/dist/wss.mjs +24 -2
  133. package/dist/wss.mjs.map +1 -0
  134. package/package.json +37 -5
  135. package/src/cli.ts +165 -1
  136. package/src/config.ts +3 -1
  137. package/src/coverage.ts +1479 -0
  138. package/src/database.ts +71 -35
  139. package/src/server.ts +25 -1
  140. package/src/test-globals.d.ts +184 -0
  141. package/src/test-reporter.ts +609 -0
  142. package/src/test-runtime.ts +1359 -0
  143. package/src/test.ts +368 -0
  144. package/src/types.ts +59 -0
  145. package/src/ws.ts +32 -2
@@ -0,0 +1,609 @@
1
+ /**
2
+ * Jest-style Reporters for Elit Test Framework
3
+ *
4
+ * Provides beautiful Jest-compatible output formats:
5
+ * - Default reporter with colored output
6
+ * - Dot reporter for CI/CD
7
+ * - JSON reporter for machine parsing
8
+ */
9
+
10
+ import type { TestResult } from './test-runtime';
11
+ import { relative } from './path';
12
+
13
+ // ANSI color codes
14
+ const colors = {
15
+ reset: '\x1b[0m',
16
+ bold: '\x1b[1m',
17
+ dim: '\x1b[2m',
18
+ red: '\x1b[31m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m',
21
+ blue: '\x1b[34m',
22
+ cyan: '\x1b[36m',
23
+ white: '\x1b[37m',
24
+ };
25
+
26
+ // ============================================================================
27
+ // Helper Functions (safe from ReDoS)
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Extract argument from function call using safe string operations
32
+ * Example: extractArg(".toBe('value')", "toBe") returns "'value'"
33
+ */
34
+ function extractArg(code: string, functionName: string): string | null {
35
+ const searchStr = `.${functionName}(`;
36
+ const startIndex = code.indexOf(searchStr);
37
+ if (startIndex === -1) return null;
38
+
39
+ let parenCount = 0;
40
+ let inString = false;
41
+ let stringChar = '';
42
+ let argStart = startIndex + searchStr.length;
43
+
44
+ for (let i = argStart; i < code.length; i++) {
45
+ const char = code[i];
46
+
47
+ if (!inString) {
48
+ if (char === '(') parenCount++;
49
+ else if (char === ')') {
50
+ parenCount--;
51
+ if (parenCount < 0) {
52
+ return code.slice(argStart, i);
53
+ }
54
+ } else if (char === '"' || char === "'" || char === '`') {
55
+ inString = true;
56
+ stringChar = char;
57
+ }
58
+ } else {
59
+ if (char === '\\' && i + 1 < code.length) {
60
+ i++; // Skip escaped character
61
+ } else if (char === stringChar) {
62
+ inString = false;
63
+ }
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Extract received value from error message using safe string operations
72
+ */
73
+ function extractReceivedValue(errorMsg: string): string | null {
74
+ const receivedIndex = errorMsg.indexOf('Received:');
75
+ if (receivedIndex === -1) return null;
76
+
77
+ const afterReceived = errorMsg.slice(receivedIndex + 9).trimStart();
78
+ const newlineIndex = afterReceived.indexOf('\n');
79
+ if (newlineIndex !== -1) {
80
+ return afterReceived.slice(0, newlineIndex).trimEnd();
81
+ }
82
+ return afterReceived.trimEnd();
83
+ }
84
+
85
+ /**
86
+ * Check if a string is quoted and extract quote and content
87
+ * Returns null if not quoted
88
+ */
89
+ function parseQuotedString(str: string): { quote: string; content: string } | null {
90
+ if (str.length < 2) return null;
91
+ const firstChar = str[0];
92
+ const lastChar = str[str.length - 1];
93
+
94
+ if ((firstChar === '"' || firstChar === "'" || firstChar === '`') &&
95
+ firstChar === lastChar) {
96
+ return {
97
+ quote: firstChar,
98
+ content: str.slice(1, -1)
99
+ };
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Strip quotes from a string (first and last matching quotes)
106
+ */
107
+ function stripQuotes(str: string): string {
108
+ if (str.length < 2) return str;
109
+ const firstChar = str[0];
110
+ const lastChar = str[str.length - 1];
111
+
112
+ if ((firstChar === '"' || firstChar === "'" || firstChar === '`') &&
113
+ firstChar === lastChar) {
114
+ return str.slice(1, -1);
115
+ }
116
+ return str;
117
+ }
118
+
119
+ // ============================================================================
120
+ // Default Jest-style Reporter
121
+ // ============================================================================
122
+
123
+ export interface TestReporterOptions {
124
+ verbose?: boolean;
125
+ colors?: boolean;
126
+ }
127
+
128
+ export class TestReporter {
129
+ private options: TestReporterOptions;
130
+ private startTime: number = 0;
131
+ private currentFile: string | undefined = undefined;
132
+ private fileTestCount: number = 0;
133
+ private totalFiles: number = 0;
134
+
135
+ constructor(options: TestReporterOptions = {}) {
136
+ this.options = {
137
+ verbose: false,
138
+ colors: true,
139
+ ...options,
140
+ };
141
+ }
142
+
143
+ private c(color: keyof typeof colors, text: string): string {
144
+ return this.options.colors !== false ? colors[color] + text + colors.reset : text;
145
+ }
146
+
147
+ onRunStart(files: string[]) {
148
+ this.startTime = Date.now();
149
+ this.totalFiles = files.length;
150
+ console.log(`\n${this.c('bold', 'Test Files')}: ${files.length}`);
151
+ console.log(`${this.c('dim', '─'.repeat(50))}\n`);
152
+ }
153
+
154
+ onTestResult(result: TestResult) {
155
+ // Format file path as relative with forward slashes (safe from ReDoS)
156
+ const filePath = result.file
157
+ ? relative(process.cwd(), result.file).split('\\').join('/')
158
+ : undefined;
159
+
160
+ // Print file header when file changes
161
+ if (filePath !== this.currentFile) {
162
+ // Print count for previous file if any tests ran
163
+ if (this.currentFile && this.fileTestCount > 0) {
164
+ console.log('');
165
+ }
166
+
167
+ this.currentFile = filePath;
168
+ this.fileTestCount = 0;
169
+
170
+ if (filePath) {
171
+ console.log(`${this.c('cyan', '●')} ${this.c('bold', filePath)}`);
172
+ console.log(`${this.c('dim', '┄'.repeat(50))}`);
173
+ }
174
+ }
175
+
176
+ this.fileTestCount++;
177
+
178
+ if (result.status === 'pass') {
179
+ console.log(` ${this.c('green', '✓')} ${this.c('dim', result.suite + ' > ')}${result.name} ${this.c('dim', `(${result.duration}ms)`)}`);
180
+ } else if (result.status === 'fail') {
181
+ console.log(` ${this.c('red', '✕')} ${this.c('dim', result.suite + ' > ')}${result.name}`);
182
+ if (result.error) {
183
+ // Show file path with line number
184
+ const filePath = result.file;
185
+ if (filePath) {
186
+ // Convert to relative path from current working directory (safe from ReDoS)
187
+ const relativePath = relative(process.cwd(), filePath).split('\\').join('/');
188
+ const lineSuffix = result.lineNumber ? `:${result.lineNumber}` : '';
189
+ console.log(` ${this.c('cyan', `📄 ${relativePath}${lineSuffix}`)}`);
190
+ }
191
+
192
+ // Format error message to highlight Expected/Received
193
+ const lines = result.error.message.split('\n');
194
+ for (const line of lines) {
195
+ if (line.includes('Expected:')) {
196
+ console.log(` ${this.c('green', 'Expected:')} ${line.trim().replace('Expected:', '').trim()}`);
197
+ } else if (line.includes('Received:')) {
198
+ console.log(` ${this.c('red', 'Received:')} ${line.trim().replace('Received:', '').trim()}`);
199
+ } else {
200
+ console.log(` ${this.c('red', line.trim())}`);
201
+ }
202
+ }
203
+
204
+ // Show code snippet with suggestion if available
205
+ if (result.codeSnippet) {
206
+ // Generate suggestion based on the error type
207
+ let suggestion = '';
208
+ const code = result.codeSnippet;
209
+
210
+ // Extract the actual (received) value from the error message (safe from ReDoS)
211
+ const errorMsg = result.error?.message || '';
212
+ const receivedValue = extractReceivedValue(errorMsg);
213
+
214
+ // Common patterns for suggestions (safe from ReDoS)
215
+ // Order matters: check longer patterns first to avoid false matches
216
+ if (code.includes('.toBeGreaterThanOrEqual(')) {
217
+ const currentValue = extractArg(code, 'toBeGreaterThanOrEqual');
218
+ if (currentValue && receivedValue) {
219
+ const actualValue = Number(receivedValue);
220
+ if (!isNaN(actualValue)) {
221
+ suggestion = code.replace(
222
+ `.toBeGreaterThanOrEqual(${currentValue})`,
223
+ `.toBeGreaterThanOrEqual(${actualValue})`
224
+ );
225
+ }
226
+ }
227
+ } else if (code.includes('.toBeGreaterThan(')) {
228
+ const currentValue = extractArg(code, 'toBeGreaterThan');
229
+ if (currentValue && receivedValue) {
230
+ const actualValue = Number(receivedValue);
231
+ if (!isNaN(actualValue)) {
232
+ suggestion = code.replace(
233
+ `.toBeGreaterThan(${currentValue})`,
234
+ `.toBeGreaterThan(${actualValue - 1})`
235
+ );
236
+ }
237
+ }
238
+ } else if (code.includes('.toBeLessThanOrEqual(')) {
239
+ const currentValue = extractArg(code, 'toBeLessThanOrEqual');
240
+ if (currentValue && receivedValue) {
241
+ const actualValue = Number(receivedValue);
242
+ if (!isNaN(actualValue)) {
243
+ suggestion = code.replace(
244
+ `.toBeLessThanOrEqual(${currentValue})`,
245
+ `.toBeLessThanOrEqual(${actualValue})`
246
+ );
247
+ }
248
+ }
249
+ } else if (code.includes('.toBeLessThan(')) {
250
+ const currentValue = extractArg(code, 'toBeLessThan');
251
+ if (currentValue && receivedValue) {
252
+ const actualValue = Number(receivedValue);
253
+ if (!isNaN(actualValue)) {
254
+ suggestion = code.replace(
255
+ `.toBeLessThan(${currentValue})`,
256
+ `.toBeLessThan(${actualValue + 1})`
257
+ );
258
+ }
259
+ }
260
+ } else if (code.includes('.toStrictEqual(')) {
261
+ const expectedValue = extractArg(code, 'toStrictEqual');
262
+ if (expectedValue && receivedValue) {
263
+ const quoted = parseQuotedString(expectedValue);
264
+ if (quoted) {
265
+ const strippedReceived = stripQuotes(receivedValue);
266
+ suggestion = code.replace(
267
+ `.toStrictEqual(${expectedValue})`,
268
+ `.toStrictEqual(${quoted.quote}${strippedReceived}${quoted.quote})`
269
+ );
270
+ } else {
271
+ suggestion = code.replace(
272
+ `.toStrictEqual(${expectedValue})`,
273
+ `.toStrictEqual(${receivedValue})`
274
+ );
275
+ }
276
+ }
277
+ } else if (code.includes('.toEqual(')) {
278
+ const expectedValue = extractArg(code, 'toEqual');
279
+ if (expectedValue && receivedValue) {
280
+ const quoted = parseQuotedString(expectedValue);
281
+ if (quoted) {
282
+ const strippedReceived = stripQuotes(receivedValue);
283
+ suggestion = code.replace(
284
+ `.toEqual(${expectedValue})`,
285
+ `.toEqual(${quoted.quote}${strippedReceived}${quoted.quote})`
286
+ );
287
+ } else {
288
+ suggestion = code.replace(
289
+ `.toEqual(${expectedValue})`,
290
+ `.toEqual(${receivedValue})`
291
+ );
292
+ }
293
+ }
294
+ } else if (code.includes('.toMatch(')) {
295
+ const expectedPattern = extractArg(code, 'toMatch');
296
+ if (expectedPattern && receivedValue) {
297
+ const quoted = parseQuotedString(expectedPattern);
298
+ if (quoted) {
299
+ const strippedReceived = stripQuotes(receivedValue);
300
+ suggestion = code.replace(
301
+ `.toMatch(${expectedPattern})`,
302
+ `.toMatch(${quoted.quote}${strippedReceived}${quoted.quote})`
303
+ );
304
+ }
305
+ }
306
+ } else if (code.includes('.toContain(')) {
307
+ const expectedValue = extractArg(code, 'toContain');
308
+ if (expectedValue && receivedValue) {
309
+ const quoted = parseQuotedString(expectedValue);
310
+ if (quoted) {
311
+ const strippedReceived = stripQuotes(receivedValue);
312
+ suggestion = code.replace(
313
+ `.toContain(${expectedValue})`,
314
+ `.toContain(${quoted.quote}${strippedReceived}${quoted.quote})`
315
+ );
316
+ } else {
317
+ suggestion = code.replace(
318
+ `.toContain(${expectedValue})`,
319
+ `.toContain(${receivedValue})`
320
+ );
321
+ }
322
+ }
323
+ } else if (code.includes('.toHaveLength(')) {
324
+ const expectedLength = extractArg(code, 'toHaveLength');
325
+ if (expectedLength && receivedValue) {
326
+ const actualLength = Number(receivedValue);
327
+ if (!isNaN(actualLength)) {
328
+ suggestion = code.replace(
329
+ `.toHaveLength(${expectedLength})`,
330
+ `.toHaveLength(${actualLength})`
331
+ );
332
+ }
333
+ }
334
+ } else if (code.includes('.toBe(')) {
335
+ const expectedValue = extractArg(code, 'toBe');
336
+ if (expectedValue) {
337
+ if (receivedValue) {
338
+ const quoted = parseQuotedString(expectedValue);
339
+ if (quoted) {
340
+ const strippedReceived = stripQuotes(receivedValue);
341
+ suggestion = code.replace(
342
+ `.toBe(${expectedValue})`,
343
+ `.toBe(${quoted.quote}${strippedReceived}${quoted.quote})`
344
+ );
345
+ } else {
346
+ suggestion = code.replace(
347
+ `.toBe(${expectedValue})`,
348
+ `.toBe(${receivedValue})`
349
+ );
350
+ }
351
+ } else if (expectedValue.includes("'") || expectedValue.includes('"')) {
352
+ suggestion = code.replace('.toBe(', '.toEqual(');
353
+ }
354
+ }
355
+ } else if (code.includes('.toBeDefined()')) {
356
+ // Suggest removing or changing to different assertion
357
+ suggestion = code.replace('.toBeDefined()', '.toBeTruthy()');
358
+ } else if (code.includes('.toBeNull()')) {
359
+ // Suggest checking for undefined instead
360
+ suggestion = code.replace('.toBeNull()', '.toBeUndefined()');
361
+ } else if (code.includes('.toBeUndefined()')) {
362
+ // Suggest checking for null instead
363
+ suggestion = code.replace('.toBeUndefined()', '.toBeNull()');
364
+ } else if (code.includes('.toBeTruthy()')) {
365
+ // Suggest checking for defined instead
366
+ suggestion = code.replace('.toBeTruthy()', '.toBeDefined()');
367
+ } else if (code.includes('.toBeFalsy()')) {
368
+ // Suggest checking for undefined or null
369
+ suggestion = code.replace('.toBeFalsy()', '.toBeUndefined()');
370
+ }
371
+
372
+ console.log(` ${this.c('dim', 'Code:')}`);
373
+ console.log(` ${this.c('dim', code)}`);
374
+ if (suggestion && suggestion !== code) {
375
+ console.log(` ${this.c('yellow', 'example →')} ${this.c('green', suggestion)}`);
376
+ }
377
+ }
378
+
379
+ if (this.options.verbose && result.error.stack) {
380
+ const stack = result.error.stack.split('\n').slice(1, 3).join('\n');
381
+ console.log(` ${this.c('dim', stack)}`);
382
+ }
383
+ }
384
+ } else if (result.status === 'skip') {
385
+ console.log(` ${this.c('yellow', '○')} ${this.c('dim', result.suite + ' > ')}${result.name} ${this.c('yellow', '(skipped)')}`);
386
+ } else if (result.status === 'todo') {
387
+ console.log(` ${this.c('cyan', '○')} ${this.c('dim', result.suite + ' > ')}${result.name} ${this.c('cyan', '(todo)')}`);
388
+ }
389
+ }
390
+
391
+ onRunEnd(results: TestResult[]) {
392
+ const duration = Date.now() - this.startTime;
393
+ const passed = results.filter(r => r.status === 'pass').length;
394
+ const failed = results.filter(r => r.status === 'fail').length;
395
+ const skipped = results.filter(r => r.status === 'skip').length;
396
+ const total = results.length;
397
+
398
+ // Add blank line after last file's tests
399
+ if (this.currentFile && this.fileTestCount > 0) {
400
+ console.log('');
401
+ }
402
+
403
+ console.log(`${this.c('dim', '─'.repeat(50))}`);
404
+
405
+ // Jest-style summary
406
+ console.log('');
407
+ console.log(`${this.c('bold', 'Test Suites:')} ${this.c('green', `${this.totalFiles} passed`)}${this.c('dim', `, ${this.totalFiles} total`)}`);
408
+ console.log(`${this.c('bold', 'Tests:')} ${this.c('green', `${passed} passed`)}${failed > 0 ? `, ${this.c('red', `${failed} failed`)}` : ''}${skipped > 0 ? `, ${this.c('yellow', `${skipped} skipped`)}` : ''}${this.c('dim', `, ${total} total`)}`);
409
+ console.log(`${this.c('bold', 'Snapshots:')} ${this.c('dim', '0 total')}`);
410
+ console.log(`${this.c('bold', 'Time:')} ${this.c('dim', `${(duration / 1000).toFixed(2)}s`)}`);
411
+ console.log('');
412
+ }
413
+ }
414
+
415
+ // ============================================================================
416
+ // Dot Reporter (minimal output for CI)
417
+ // ============================================================================
418
+
419
+ export class DotReporter {
420
+ private passed = 0;
421
+ private failed = 0;
422
+ private skipped = 0;
423
+ private todo = 0;
424
+ private lineLength = 0;
425
+
426
+ onRunStart(files: string[]) {
427
+ console.log(`\n ${files.length} test files\n`);
428
+ }
429
+
430
+ onTestResult(result: TestResult) {
431
+ const symbol = result.status === 'pass' ? '.' :
432
+ result.status === 'fail' ? this.c('red', 'F') :
433
+ result.status === 'skip' ? this.c('yellow', 'o') :
434
+ this.c('cyan', 'o');
435
+
436
+ process.stdout.write(symbol);
437
+ this.lineLength++;
438
+
439
+ if (result.status === 'pass') this.passed++;
440
+ else if (result.status === 'fail') this.failed++;
441
+ else if (result.status === 'skip') this.skipped++;
442
+ else if (result.status === 'todo') this.todo++;
443
+
444
+ // Wrap every 50 characters
445
+ if (this.lineLength >= 50) {
446
+ process.stdout.write('\n ');
447
+ this.lineLength = 0;
448
+ }
449
+ }
450
+
451
+ onRunEnd(_results: TestResult[]) {
452
+ console.log(`\n\n ${this.c('green', this.passed + ' passed')} ${this.c('dim', '·')} ${this.c('red', this.failed + ' failed')} ${this.c('dim', '·')} ${this.c('yellow', this.skipped + ' skipped')}\n`);
453
+ }
454
+
455
+ private c(color: keyof typeof colors, text: string): string {
456
+ return colors[color] + text + colors.reset;
457
+ }
458
+ }
459
+
460
+ // ============================================================================
461
+ // JSON Reporter (machine-readable)
462
+ // ============================================================================
463
+
464
+ export interface JsonTestResult {
465
+ status: 'passed' | 'failed' | 'skipped' | 'todo';
466
+ name: string;
467
+ suite: string;
468
+ duration: number;
469
+ error?: {
470
+ message: string;
471
+ stack?: string;
472
+ };
473
+ }
474
+
475
+ export interface JsonReport {
476
+ summary: {
477
+ total: number;
478
+ passed: number;
479
+ failed: number;
480
+ skipped: number;
481
+ todo: number;
482
+ duration: number;
483
+ };
484
+ tests: JsonTestResult[];
485
+ }
486
+
487
+ export class JsonReporter {
488
+ private startTime: number = 0;
489
+ private results: TestResult[] = [];
490
+
491
+ onRunStart(_files: string[]) {
492
+ this.startTime = Date.now();
493
+ this.results = [];
494
+ }
495
+
496
+ onTestResult(result: TestResult) {
497
+ this.results.push(result);
498
+ }
499
+
500
+ onRunEnd(results: TestResult[]) {
501
+ const report: JsonReport = {
502
+ summary: {
503
+ total: results.length,
504
+ passed: results.filter(r => r.status === 'pass').length,
505
+ failed: results.filter(r => r.status === 'fail').length,
506
+ skipped: results.filter(r => r.status === 'skip').length,
507
+ todo: results.filter(r => r.status === 'todo').length,
508
+ duration: Date.now() - this.startTime,
509
+ },
510
+ tests: results.map(r => ({
511
+ status: r.status === 'pass' ? 'passed' : r.status === 'fail' ? 'failed' : r.status === 'skip' ? 'skipped' : 'todo',
512
+ name: r.name,
513
+ suite: r.suite,
514
+ duration: r.duration,
515
+ error: r.error ? {
516
+ message: r.error.message,
517
+ stack: r.error.stack,
518
+ } : undefined,
519
+ })),
520
+ };
521
+
522
+ console.log(JSON.stringify(report, null, 2));
523
+ }
524
+ }
525
+
526
+ // ============================================================================
527
+ // Verbose Reporter (detailed output)
528
+ // ============================================================================
529
+
530
+ export class VerboseReporter {
531
+ private currentSuite: string = '';
532
+
533
+ onRunStart(_files: string[]) {
534
+ console.log(`\n${colors.cyan}Running tests${colors.reset}\n`);
535
+ }
536
+
537
+ onTestResult(result: TestResult) {
538
+ // Print suite name when it changes
539
+ if (result.suite !== this.currentSuite) {
540
+ this.currentSuite = result.suite;
541
+ console.log(`\n${colors.dim}${result.suite}${colors.reset}`);
542
+ }
543
+
544
+ const icon = result.status === 'pass' ? colors.green + ' ✓' :
545
+ result.status === 'fail' ? colors.red + ' ✕' :
546
+ result.status === 'skip' ? colors.yellow + ' ⊘' :
547
+ colors.cyan + ' ○';
548
+
549
+ console.log(`${icon}${colors.reset} ${result.name}${colors.dim} (${result.duration}ms)${colors.reset}`);
550
+
551
+ if (result.status === 'fail' && result.error) {
552
+ console.log(`\n${colors.red} ${result.error.message}${colors.reset}`);
553
+ if (result.error.stack) {
554
+ const lines = result.error.stack.split('\n').slice(1, 4);
555
+ lines.forEach(line => console.log(`${colors.dim} ${line}${colors.reset}`));
556
+ }
557
+ }
558
+ }
559
+
560
+ onRunEnd(results: TestResult[]) {
561
+ const passed = results.filter(r => r.status === 'pass').length;
562
+ const failed = results.filter(r => r.status === 'fail').length;
563
+ const skipped = results.filter(r => r.status === 'skip').length;
564
+
565
+ console.log(`\n${colors.dim}${'─'.repeat(50)}${colors.reset}\n`);
566
+
567
+ if (failed === 0) {
568
+ console.log(`${colors.green}All tests passed!${colors.reset}`);
569
+ console.log(`${colors.dim}${passed} tests${colors.reset}\n`);
570
+ } else {
571
+ console.log(`${colors.red}${failed} tests failed${colors.reset}`);
572
+ console.log(`${colors.green}${passed} tests passed${colors.reset}`);
573
+ if (skipped > 0) {
574
+ console.log(`${colors.yellow}${skipped} tests skipped${colors.reset}`);
575
+ }
576
+ console.log('');
577
+ }
578
+ }
579
+ }
580
+
581
+ // ============================================================================
582
+ // Utility function to format error stacks
583
+ // ============================================================================
584
+
585
+ export function formatErrorStack(error: Error): string {
586
+ if (!error.stack) return error.message;
587
+
588
+ const lines = error.stack.split('\n');
589
+ let formatted = `${error.message}\n`;
590
+
591
+ // Skip the first line (error message) and format the rest
592
+ for (const line of lines.slice(1, 6)) {
593
+ formatted += ` ${line.trim()}\n`;
594
+ }
595
+
596
+ return formatted;
597
+ }
598
+
599
+ // ============================================================================
600
+ // Progress bar for watch mode
601
+ // ============================================================================
602
+
603
+ export function formatProgress(current: number, total: number): string {
604
+ const percentage = Math.floor((current / total) * 100);
605
+ const filled = Math.floor(percentage / 2);
606
+ const empty = 50 - filled;
607
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
608
+ return `[${bar}] ${percentage}% (${current}/${total})`;
609
+ }