elit 3.5.6 → 3.5.7

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 (113) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +1 -1
  3. package/desktop/build.rs +83 -0
  4. package/desktop/icon.rs +106 -0
  5. package/desktop/lib.rs +2 -0
  6. package/desktop/main.rs +235 -0
  7. package/desktop/native_main.rs +128 -0
  8. package/desktop/native_renderer/action_widgets.rs +184 -0
  9. package/desktop/native_renderer/app_models.rs +171 -0
  10. package/desktop/native_renderer/app_runtime.rs +140 -0
  11. package/desktop/native_renderer/container_rendering.rs +610 -0
  12. package/desktop/native_renderer/content_widgets.rs +634 -0
  13. package/desktop/native_renderer/css_models.rs +371 -0
  14. package/desktop/native_renderer/embedded_surfaces.rs +414 -0
  15. package/desktop/native_renderer/form_controls.rs +516 -0
  16. package/desktop/native_renderer/interaction_dispatch.rs +89 -0
  17. package/desktop/native_renderer/runtime_support.rs +135 -0
  18. package/desktop/native_renderer/utilities.rs +495 -0
  19. package/desktop/native_renderer/vector_drawing.rs +491 -0
  20. package/desktop/native_renderer.rs +4122 -0
  21. package/desktop/runtime/external.rs +422 -0
  22. package/desktop/runtime/mod.rs +67 -0
  23. package/desktop/runtime/quickjs.rs +106 -0
  24. package/desktop/window.rs +383 -0
  25. package/package.json +6 -3
  26. package/dist/build.d.mts +0 -20
  27. package/dist/chokidar.d.mts +0 -134
  28. package/dist/cli.d.mts +0 -81
  29. package/dist/config.d.mts +0 -254
  30. package/dist/coverage.d.mts +0 -85
  31. package/dist/database.d.mts +0 -52
  32. package/dist/desktop.d.mts +0 -68
  33. package/dist/dom.d.mts +0 -87
  34. package/dist/el.d.mts +0 -208
  35. package/dist/fs.d.mts +0 -255
  36. package/dist/hmr.d.mts +0 -38
  37. package/dist/http.d.mts +0 -169
  38. package/dist/https.d.mts +0 -108
  39. package/dist/index.d.mts +0 -13
  40. package/dist/mime-types.d.mts +0 -48
  41. package/dist/native.d.mts +0 -136
  42. package/dist/path.d.mts +0 -163
  43. package/dist/router.d.mts +0 -49
  44. package/dist/runtime.d.mts +0 -97
  45. package/dist/server-D0Dp4R5z.d.mts +0 -449
  46. package/dist/server.d.mts +0 -7
  47. package/dist/state.d.mts +0 -117
  48. package/dist/style.d.mts +0 -232
  49. package/dist/test-reporter.d.mts +0 -77
  50. package/dist/test-runtime.d.mts +0 -122
  51. package/dist/test.d.mts +0 -39
  52. package/dist/types.d.mts +0 -586
  53. package/dist/universal.d.mts +0 -21
  54. package/dist/ws.d.mts +0 -200
  55. package/dist/wss.d.mts +0 -108
  56. package/src/build.ts +0 -362
  57. package/src/chokidar.ts +0 -427
  58. package/src/cli.ts +0 -1162
  59. package/src/config.ts +0 -509
  60. package/src/coverage.ts +0 -1479
  61. package/src/database.ts +0 -1410
  62. package/src/desktop-auto-render.ts +0 -317
  63. package/src/desktop-cli.ts +0 -1533
  64. package/src/desktop.ts +0 -99
  65. package/src/dev-build.ts +0 -340
  66. package/src/dom.ts +0 -901
  67. package/src/el.ts +0 -183
  68. package/src/fs.ts +0 -609
  69. package/src/hmr.ts +0 -149
  70. package/src/http.ts +0 -856
  71. package/src/https.ts +0 -411
  72. package/src/index.ts +0 -16
  73. package/src/mime-types.ts +0 -222
  74. package/src/mobile-cli.ts +0 -2313
  75. package/src/native-background.ts +0 -444
  76. package/src/native-border.ts +0 -343
  77. package/src/native-canvas.ts +0 -260
  78. package/src/native-cli.ts +0 -414
  79. package/src/native-color.ts +0 -904
  80. package/src/native-estimation.ts +0 -194
  81. package/src/native-grid.ts +0 -590
  82. package/src/native-interaction.ts +0 -1289
  83. package/src/native-layout.ts +0 -568
  84. package/src/native-link.ts +0 -76
  85. package/src/native-render-support.ts +0 -361
  86. package/src/native-spacing.ts +0 -231
  87. package/src/native-state.ts +0 -318
  88. package/src/native-strings.ts +0 -46
  89. package/src/native-transform.ts +0 -120
  90. package/src/native-types.ts +0 -439
  91. package/src/native-typography.ts +0 -254
  92. package/src/native-units.ts +0 -441
  93. package/src/native-vector.ts +0 -910
  94. package/src/native.ts +0 -5606
  95. package/src/path.ts +0 -493
  96. package/src/pm-cli.ts +0 -2498
  97. package/src/preview-build.ts +0 -294
  98. package/src/render-context.ts +0 -138
  99. package/src/router.ts +0 -260
  100. package/src/runtime.ts +0 -97
  101. package/src/server.ts +0 -2294
  102. package/src/state.ts +0 -556
  103. package/src/style.ts +0 -1790
  104. package/src/test-globals.d.ts +0 -184
  105. package/src/test-reporter.ts +0 -609
  106. package/src/test-runtime.ts +0 -1359
  107. package/src/test.ts +0 -368
  108. package/src/types.ts +0 -381
  109. package/src/universal.ts +0 -81
  110. package/src/wapk-cli.ts +0 -3213
  111. package/src/workspace-package.ts +0 -102
  112. package/src/ws.ts +0 -648
  113. package/src/wss.ts +0 -241
@@ -1,1359 +0,0 @@
1
- /**
2
- * Jest-style Test Runtime for Elit
3
- *
4
- * A modern test library powered by esbuild with Jest-compatible API.
5
- * Features:
6
- * - esbuild for fast TypeScript/JavaScript transpilation
7
- * - Jest-like globals (describe, it, test, expect, etc.)
8
- * - Built-in mocking with vi.fn()
9
- * - Snapshot testing
10
- * - Coverage with V8 provider
11
- */
12
-
13
- import { transformSync } from 'esbuild';
14
- import { readFile, readFileSync } from './fs';
15
- import { dirname } from './path';
16
- import { SourceMapConsumer } from 'source-map';
17
- import type { RawSourceMap } from 'source-map';
18
-
19
- // Export TestResult for use in reporters
20
- export { type TestResult };
21
-
22
- // ============================================================================
23
- // Helper Functions
24
- // ============================================================================
25
-
26
- /**
27
- * Escape special regex characters to prevent regex injection
28
- * This sanitizes user input before using it in RegExp constructor
29
- */
30
- function escapeRegex(str: string): string {
31
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
- }
33
-
34
- // ============================================================================
35
- // Types
36
- // ============================================================================
37
-
38
- export interface TestFunction {
39
- (name: string, fn: () => void, timeout?: number): void;
40
- skip: (name: string, fn: () => void, timeout?: number) => void;
41
- only: (name: string, fn: () => void, timeout?: number) => void;
42
- todo: (name: string, fn: () => void, timeout?: number) => void;
43
- }
44
-
45
- export interface DescribeFunction {
46
- (name: string, fn: () => void): void;
47
- skip: (name: string, fn: () => void) => void;
48
- only: (name: string, fn: () => void) => void;
49
- }
50
-
51
- export interface MockFunction<T extends (...args: any[]) => any> {
52
- (...args: Parameters<T>): ReturnType<T>;
53
- _isMock: boolean;
54
- _calls: Parameters<T>[];
55
- _results: Array<{ type: 'return' | 'throw'; value: any }>;
56
- _implementation: T | null;
57
- mockImplementation(fn: T): MockFunction<T>;
58
- mockReturnValue(value: ReturnType<T>): MockFunction<T>;
59
- mockResolvedValue(value: ReturnType<T>): MockFunction<T>;
60
- mockRejectedValue(value: any): MockFunction<T>;
61
- restore(): void;
62
- clear(): void;
63
- }
64
-
65
- export interface TestMatchers<T> {
66
- toBe(value: T): void;
67
- toEqual(value: T): void;
68
- toBeTruthy(): void;
69
- toBeFalsy(): void;
70
- toBeNull(): void;
71
- toBeUndefined(): void;
72
- toBeDefined(): void;
73
- toBeGreaterThan(value: number): void;
74
- toBeLessThan(value: number): void;
75
- toContain(value: any): void;
76
- toHaveLength(length: number): void;
77
- toThrow(error?: any): void;
78
- toMatch(pattern: RegExp | string): void;
79
- toBeInstanceOf(classType: any): void;
80
- toHaveProperty(path: string | string[], value?: any): void;
81
- toBeCalled(): void;
82
- toBeCalledTimes(times: number): void;
83
- toBeCalledWith(...args: any[]): void;
84
- lastReturnedWith(value: any): void;
85
- // Modifiers
86
- not: TestMatchers<any>;
87
- resolves: TestMatchers<any>;
88
- rejects: TestMatchers<any>;
89
- }
90
-
91
- // ============================================================================
92
- // AssertionError
93
- // ============================================================================
94
-
95
- class AssertionError extends Error {
96
- constructor(
97
- message: string,
98
- public filePath?: string,
99
- public lineNumber?: number,
100
- public columnNumber?: number,
101
- public codeSnippet?: string
102
- ) {
103
- super(message);
104
- this.name = 'AssertionError';
105
- }
106
- }
107
-
108
- // ============================================================================
109
- // Test Suite State
110
- // ============================================================================
111
-
112
- interface TestSuite {
113
- name: string;
114
- tests: Test[];
115
- suites: TestSuite[];
116
- parent?: TestSuite;
117
- skip: boolean;
118
- only: boolean;
119
- }
120
-
121
- interface Test {
122
- name: string;
123
- fn: () => void | Promise<void>;
124
- skip: boolean;
125
- only: boolean;
126
- todo: boolean;
127
- timeout: number;
128
- suite: TestSuite;
129
- }
130
-
131
- interface TestResult {
132
- name: string;
133
- status: 'pass' | 'fail' | 'skip' | 'todo';
134
- duration: number;
135
- error?: Error;
136
- suite: string;
137
- file?: string;
138
- // Additional assertion error details
139
- lineNumber?: number;
140
- codeSnippet?: string;
141
- }
142
-
143
- // ============================================================================
144
- // Global State
145
- // ============================================================================
146
-
147
- let currentSuite: TestSuite = {
148
- name: 'root',
149
- tests: [],
150
- suites: [],
151
- skip: false,
152
- only: false,
153
- };
154
-
155
- const testResults: TestResult[] = [];
156
- let hasOnly = false;
157
-
158
- // Track all source files that are loaded during test execution for coverage
159
- const coveredFiles = new Set<string>();
160
-
161
- // Filter patterns for running specific tests
162
- let describePattern: string | undefined = undefined;
163
- let testPattern: string | undefined = undefined;
164
-
165
- // Current test file being processed (for reporting)
166
- let currentTestFile: string | undefined = undefined;
167
- // Current source map consumer for line number mapping
168
- let currentSourceMapConsumer: SourceMapConsumer | undefined = undefined;
169
- // Line offset due to wrapper code added before test code
170
- let wrapperLineOffset: number = 0;
171
-
172
- // ============================================================================
173
- // esbuild Transpiler
174
- // ============================================================================
175
-
176
- export async function transpileFile(filePath: string): Promise<{ code: string; sourceMap?: RawSourceMap }> {
177
- const source = await readFile(filePath, 'utf-8');
178
-
179
- const result = transformSync(source, {
180
- loader: filePath.endsWith('.ts') || filePath.endsWith('.tsx') ? 'ts' : 'js',
181
- format: 'esm',
182
- sourcemap: 'inline',
183
- target: 'es2020',
184
- tsconfigRaw: {
185
- compilerOptions: {
186
- jsx: 'react',
187
- jsxFactory: 'h',
188
- jsxFragmentFactory: 'Fragment',
189
- },
190
- },
191
- });
192
-
193
- // Extract source map from inline source map comment
194
- let sourceMap: RawSourceMap | undefined;
195
- const sourceMapMatch = result.code.match(/\/\/# sourceMappingURL=data:application\/json;base64,(.+)/);
196
- if (sourceMapMatch) {
197
- const base64 = sourceMapMatch[1];
198
- const json = Buffer.from(base64, 'base64').toString('utf-8');
199
- sourceMap = JSON.parse(json) as RawSourceMap;
200
- }
201
-
202
- return { code: result.code, sourceMap };
203
- }
204
-
205
- // ============================================================================
206
- // Test Functions (Jest-style API)
207
- // ============================================================================
208
-
209
- function createTestFunction(defaultTimeout: number = 5000): TestFunction {
210
- const testFn = function (name: string, fn: () => void, timeout?: number) {
211
- const test: Test = {
212
- name,
213
- fn,
214
- skip: currentSuite.skip,
215
- only: false,
216
- todo: false,
217
- timeout: timeout ?? defaultTimeout,
218
- suite: currentSuite,
219
- };
220
- currentSuite.tests.push(test);
221
- } as TestFunction;
222
-
223
- testFn.skip = (name: string, fn: () => void, timeout?: number) => {
224
- const test: Test = {
225
- name,
226
- fn,
227
- skip: true,
228
- only: false,
229
- todo: false,
230
- timeout: timeout ?? defaultTimeout,
231
- suite: currentSuite,
232
- };
233
- currentSuite.tests.push(test);
234
- };
235
-
236
- testFn.only = (name: string, fn: () => void, timeout?: number) => {
237
- hasOnly = true;
238
- const test: Test = {
239
- name,
240
- fn,
241
- skip: false,
242
- only: true,
243
- todo: false,
244
- timeout: timeout ?? defaultTimeout,
245
- suite: currentSuite,
246
- };
247
- currentSuite.tests.push(test);
248
- };
249
-
250
- testFn.todo = (name: string, fn: () => void, timeout?: number) => {
251
- const test: Test = {
252
- name,
253
- fn,
254
- skip: false,
255
- only: false,
256
- todo: true,
257
- timeout: timeout ?? defaultTimeout,
258
- suite: currentSuite,
259
- };
260
- currentSuite.tests.push(test);
261
- };
262
-
263
- return testFn;
264
- }
265
-
266
- function createDescribeFunction(): DescribeFunction {
267
- const describeFn = function (name: string, fn: () => void) {
268
- const parent = currentSuite;
269
- const suite: TestSuite = {
270
- name,
271
- tests: [],
272
- suites: [],
273
- parent,
274
- skip: parent.skip,
275
- only: parent.only,
276
- };
277
- parent.suites.push(suite);
278
- currentSuite = suite;
279
- fn();
280
- currentSuite = parent;
281
- } as DescribeFunction;
282
-
283
- describeFn.skip = (name: string, fn: () => void) => {
284
- const parent = currentSuite;
285
- const suite: TestSuite = {
286
- name,
287
- tests: [],
288
- suites: [],
289
- parent,
290
- skip: true,
291
- only: false,
292
- };
293
- parent.suites.push(suite);
294
- currentSuite = suite;
295
- fn();
296
- currentSuite = parent;
297
- };
298
-
299
- describeFn.only = (name: string, fn: () => void) => {
300
- hasOnly = true;
301
- const parent = currentSuite;
302
- const suite: TestSuite = {
303
- name,
304
- tests: [],
305
- suites: [],
306
- parent,
307
- skip: false,
308
- only: true,
309
- };
310
- parent.suites.push(suite);
311
- currentSuite = suite;
312
- fn();
313
- currentSuite = parent;
314
- };
315
-
316
- return describeFn;
317
- }
318
-
319
- // ============================================================================
320
- // Expect Implementation (Jest-style matchers)
321
- // ============================================================================
322
-
323
- class Expect implements TestMatchers<any> {
324
- private expected: any;
325
- private _not: TestMatchers<any> | null = null;
326
- private _resolves: TestMatchers<any> | null = null;
327
- private _rejects: TestMatchers<any> | null = null;
328
-
329
- constructor(private actual: any, private isNot = false, private isAsync = false) {
330
- // Modifiers are lazy-initialized
331
- }
332
-
333
- get not(): TestMatchers<any> {
334
- if (!this._not) {
335
- this._not = new Expect(this.actual, !this.isNot, false);
336
- }
337
- return this._not;
338
- }
339
-
340
- get resolves(): TestMatchers<any> {
341
- if (!this._resolves) {
342
- // Mark that this is an async promise that will resolve
343
- this._resolves = new Expect(this.actual, this.isNot, true);
344
- }
345
- return this._resolves!;
346
- }
347
-
348
- get rejects(): TestMatchers<any> {
349
- if (!this._rejects) {
350
- // Mark that this is an async promise that will reject
351
- this._rejects = new Expect(this.actual, this.isNot, true);
352
- }
353
- return this._rejects!;
354
- }
355
-
356
- private assertCondition(condition: boolean, message: string, showExpectedReceived: boolean = true, expectedDisplay?: string, callerStack?: string) {
357
- // Invert condition if using 'not' modifier
358
- if (this.isNot) {
359
- condition = !condition;
360
- }
361
-
362
- if (!condition) {
363
- let errorMsg = message;
364
- if (showExpectedReceived) {
365
- const expectedValue = expectedDisplay ?? this.stringify(this.expected ?? 'truthy');
366
- errorMsg += `\n` +
367
- ` Expected: ${expectedValue}\n` +
368
- ` Received: ${this.stringify(this.actual)}`;
369
- }
370
-
371
- // Use the caller's stack trace if provided, otherwise capture current stack
372
- const stack = callerStack || new Error().stack;
373
- let lineNumber: number | undefined = undefined;
374
- let codeSnippet: string | undefined = undefined;
375
-
376
- // Determine which assertion method was called from the stack trace
377
- // This helps us find the correct line when multiple assertions are on consecutive lines
378
- let assertionMethod: string | undefined = undefined;
379
- if (stack) {
380
- const assertionMatch = stack.match(/at _Expect\.(\w+)/);
381
- if (assertionMatch) {
382
- assertionMethod = assertionMatch[1];
383
- }
384
- }
385
-
386
- if (stack) {
387
- // Parse stack trace to find line number in the dynamically executed code
388
- const lines = stack.split('\n');
389
-
390
- // Find all <anonymous>:line:column patterns in stack trace
391
- const stackFrames: Array<{ line: number; column: number }> = [];
392
- for (const line of lines) {
393
- const match = line.match(/<anonymous>:([0-9]+):([0-9]+)/);
394
- if (match) {
395
- stackFrames.push({
396
- line: parseInt(match[1], 10),
397
- column: parseInt(match[2], 10)
398
- });
399
- }
400
- }
401
-
402
- // Use the second stack frame if available (first is the assertion method itself)
403
- // This gets us to where the assertion was called from
404
- const targetFrame = stackFrames.length > 1 ? stackFrames[1] : stackFrames[0];
405
-
406
- if (targetFrame && currentSourceMapConsumer) {
407
- try {
408
- // The source map was created before the wrapper was added, so we need to adjust
409
- // The wrapper adds lines at the beginning, shifting everything down
410
- const transpiledLine = targetFrame.line - wrapperLineOffset;
411
-
412
- // Try exact mapping first using column number
413
- const originalPosition = currentSourceMapConsumer.originalPositionFor({
414
- line: transpiledLine,
415
- column: targetFrame.column
416
- });
417
-
418
- if (originalPosition.line !== null) {
419
- lineNumber = originalPosition.line;
420
-
421
- // Also try to find the previous line that has a mapping
422
- // Sometimes source maps point to the line after the actual code
423
- // So we check the line before to see if it's an assertion
424
- if (currentTestFile) {
425
- try {
426
- let sourceCode = readFileSync(currentTestFile, 'utf-8');
427
- if (Buffer.isBuffer(sourceCode)) {
428
- sourceCode = sourceCode.toString('utf-8');
429
- }
430
- const sourceLines = (sourceCode as string).split('\n');
431
-
432
- // Determine the pattern to look for based on the assertion method
433
- // Use patterns that include the opening parenthesis to avoid false matches
434
- // e.g., ".toBeDefined" contains ").toBe" which would incorrectly match
435
- let targetPattern = '.toBe('; // default - include opening paren
436
- if (assertionMethod === 'toEqual') targetPattern = '.toEqual(';
437
- else if (assertionMethod === 'toStrictEqual') targetPattern = '.toStrictEqual(';
438
- else if (assertionMethod === 'toMatch') targetPattern = '.toMatch(';
439
- else if (assertionMethod === 'toContain') targetPattern = '.toContain(';
440
- else if (assertionMethod === 'toHaveLength') targetPattern = '.toHaveLength(';
441
- else if (assertionMethod === 'toBeDefined') targetPattern = '.toBeDefined(';
442
- else if (assertionMethod === 'toBeNull') targetPattern = '.toBeNull(';
443
- else if (assertionMethod === 'toBeUndefined') targetPattern = '.toBeUndefined(';
444
- else if (assertionMethod === 'toBeTruthy') targetPattern = '.toBeTruthy(';
445
- else if (assertionMethod === 'toBeFalsy') targetPattern = '.toBeFalsy(';
446
- else if (assertionMethod === 'toThrow') targetPattern = '.toThrow(';
447
- else if (assertionMethod === 'toBeGreaterThan') targetPattern = '.toBeGreaterThan(';
448
- else if (assertionMethod === 'toBeGreaterThanOrEqual') targetPattern = '.toBeGreaterThanOrEqual(';
449
- else if (assertionMethod === 'toBeLessThan') targetPattern = '.toBeLessThan(';
450
- else if (assertionMethod === 'toBeLessThanOrEqual') targetPattern = '.toBeLessThanOrEqual(';
451
-
452
- // Check if the mapped line contains the matching assertion
453
- if (lineNumber > 0 && lineNumber <= sourceLines.length) {
454
- const mappedLine = sourceLines[lineNumber - 1];
455
- const hasMatchingAssertion = mappedLine.includes(targetPattern);
456
-
457
- // If the mapped line doesn't have the matching assertion, check nearby lines
458
- if (!hasMatchingAssertion) {
459
- // Search backward up to 3 lines
460
- for (let i = 1; i <= 3; i++) {
461
- const searchLine = lineNumber - i;
462
- if (searchLine > 0 && searchLine <= sourceLines.length) {
463
- const testLine = sourceLines[searchLine - 1];
464
- if (testLine.includes(targetPattern)) {
465
- lineNumber = searchLine;
466
- break;
467
- }
468
- }
469
- }
470
- }
471
- }
472
- } catch (e) {
473
- // Silently fail - verification is optional
474
- }
475
- }
476
- } else {
477
- // Fallback: try without column (just line)
478
- const posWithoutColumn = currentSourceMapConsumer.originalPositionFor({
479
- line: transpiledLine,
480
- column: 0
481
- });
482
- if (posWithoutColumn.line !== null) {
483
- lineNumber = posWithoutColumn.line;
484
- } else {
485
- // Last resort: search for the closest mapping near this line
486
- const lineMappings: Array<{ line: number; distance: number }> = [];
487
-
488
- currentSourceMapConsumer.eachMapping((mapping) => {
489
- if (mapping.originalLine !== null) {
490
- const distance = Math.abs(mapping.generatedLine - transpiledLine);
491
- lineMappings.push({
492
- line: mapping.originalLine,
493
- distance
494
- });
495
- }
496
- });
497
-
498
- if (lineMappings.length > 0) {
499
- lineMappings.sort((a, b) => a.distance - b.distance);
500
- lineNumber = lineMappings[0].line;
501
- }
502
- }
503
- }
504
- } catch (e) {
505
- // If source map parsing fails, skip
506
- }
507
- }
508
-
509
- // Extract code snippet from source file (after lineNumber is determined)
510
- // This is OUTSIDE the source map block so it always runs if we have file and line number
511
- if (currentTestFile && lineNumber) {
512
- try {
513
- let sourceCode = readFileSync(currentTestFile, 'utf-8');
514
- // Ensure it's a string
515
- if (Buffer.isBuffer(sourceCode)) {
516
- sourceCode = sourceCode.toString('utf-8');
517
- }
518
- const sourceLines = (sourceCode as string).split('\n');
519
-
520
- // Get the code line at the determined line number
521
- if (lineNumber > 0 && lineNumber <= sourceLines.length) {
522
- const codeLine = sourceLines[lineNumber - 1];
523
- if (codeLine) {
524
- codeSnippet = codeLine.trim();
525
- }
526
- }
527
- } catch (e) {
528
- // Silently fail - code snippet extraction is optional
529
- }
530
- }
531
- }
532
-
533
- throw new AssertionError(errorMsg, currentTestFile, lineNumber, undefined, codeSnippet);
534
- }
535
- }
536
-
537
- private stringify(value: any): string {
538
- if (value === undefined) return 'undefined';
539
- if (value === null) return 'null';
540
- if (typeof value === 'string') return `"${value}"`;
541
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
542
- if (typeof value === 'function') return 'Function';
543
- if (Array.isArray(value)) return `[${value.map(v => this.stringify(v)).join(', ')}]`;
544
- if (typeof value === 'object') {
545
- const keys = Object.keys(value);
546
- if (keys.length === 0) return '{}';
547
- return `{ ${keys.slice(0, 3).map(k => `${k}: ${this.stringify(value[k])}`).join(', ')}${keys.length > 3 ? '...' : ''} }`;
548
- }
549
- return String(value);
550
- }
551
-
552
- private async handleAsyncAssertion(value: any, assertion: (actual: any) => void): Promise<any> {
553
- try {
554
- const resolvedValue = await this.actual;
555
- // Promise resolved successfully
556
- if (this.isNot) {
557
- // For .not.rejects - we expected rejection but got resolution - this is success!
558
- // For .not.resolves - we expected rejection but got resolution - this is failure!
559
- throw new Error(`Promise resolved when it should have rejected`);
560
- }
561
- // For .resolves - this is expected, run the assertion
562
- assertion(resolvedValue);
563
- return Promise.resolve(resolvedValue); // Return a promise that resolves
564
- } catch (error: any) {
565
- // Promise rejected
566
- if (this.isNot) {
567
- // For .not.resolves - we expected rejection and got it - success!
568
- // For .not.rejects - we expected resolution but got rejection - failure!
569
- // But since we use .not, we invert the logic
570
- return Promise.resolve(undefined); // Successfully caught the rejection
571
- }
572
- // For .rejects (without .not) - this is expected
573
- // Check error message if value was provided
574
- if (typeof value === 'string') {
575
- this.assertCondition(
576
- error.message?.includes(value),
577
- `Expected error message to include "${value}"`
578
- );
579
- } else if (value instanceof RegExp) {
580
- this.assertCondition(
581
- value.test(error.message),
582
- `Expected error message to match ${value}`
583
- );
584
- }
585
- // If value is undefined (just .toThrow()), we successfully caught the rejection
586
- // Return a resolved promise to indicate success
587
- return Promise.resolve(undefined);
588
- }
589
- }
590
-
591
- toBe(value: any): any {
592
- const stack = new Error().stack;
593
- if (this.isAsync) {
594
- return this.handleAsyncAssertion(value, (actual) => {
595
- this.expected = value;
596
- this.assertCondition(actual === value, `Expected values to be strictly equal (using ===)`, false, undefined, stack);
597
- if (typeof actual !== typeof value) {
598
- throw new Error(`Types don't match: expected ${typeof value} but got ${typeof actual}`);
599
- }
600
- });
601
- }
602
-
603
- this.expected = value;
604
- this.assertCondition(this.actual === value, `Expected values to be strictly equal (using ===)`, true, undefined, stack);
605
- if (typeof this.actual !== typeof value) {
606
- throw new Error(`Types don't match: expected ${typeof value} but got ${typeof this.actual}`);
607
- }
608
- }
609
-
610
- toEqual(value: any) {
611
- const stack = new Error().stack;
612
- this.expected = value;
613
- const isEqual = (a: any, b: any): boolean => {
614
- if (a === b) return true;
615
- if (a == null || b == null) return a === b;
616
- if (typeof a !== typeof b) return false;
617
- if (typeof a !== 'object') return a === b;
618
- if (Array.isArray(a) !== Array.isArray(b)) return false;
619
- if (Array.isArray(a)) {
620
- if (a.length !== b.length) return false;
621
- return a.every((item, i) => isEqual(item, b[i]));
622
- }
623
- const keysA = Object.keys(a);
624
- const keysB = Object.keys(b);
625
- if (keysA.length !== keysB.length) return false;
626
- return keysA.every(key => isEqual(a[key], b[key]));
627
- };
628
- this.assertCondition(isEqual(this.actual, value), 'Expected values to be deeply equal', false, undefined, stack);
629
- }
630
-
631
- toBeTruthy() {
632
- const stack = new Error().stack;
633
- this.assertCondition(!!this.actual, `Expected value to be truthy`, false, undefined, stack);
634
- }
635
-
636
- toBeFalsy() {
637
- const stack = new Error().stack;
638
- this.assertCondition(!this.actual, `Expected value to be falsy`, false, undefined, stack);
639
- }
640
-
641
- toBeNull() {
642
- const stack = new Error().stack;
643
- this.assertCondition(this.actual === null, `Expected value to be null`, false, undefined, stack);
644
- }
645
-
646
- toBeUndefined() {
647
- const stack = new Error().stack;
648
- this.assertCondition(this.actual === undefined, `Expected value to be undefined`, false, undefined, stack);
649
- }
650
-
651
- toBeDefined() {
652
- const stack = new Error().stack;
653
- this.assertCondition(this.actual !== undefined, `Expected value to be defined`, false, undefined, stack);
654
- }
655
-
656
- toBeGreaterThan(value: number) {
657
- // Capture stack trace at assertion call site
658
- const stack = new Error().stack;
659
- this.expected = value;
660
- this.assertCondition(typeof this.actual === 'number' && this.actual > value,
661
- `Expected ${this.stringify(this.actual)} to be greater than ${value}`, true, String(value), stack);
662
- }
663
-
664
- toBeGreaterThanOrEqual(value: number) {
665
- const stack = new Error().stack;
666
- this.expected = value;
667
- this.assertCondition(typeof this.actual === 'number' && this.actual >= value,
668
- `Expected ${this.stringify(this.actual)} to be greater than or equal to ${value}`, true, `${value}`, stack);
669
- }
670
-
671
- toBeLessThan(value: number) {
672
- const stack = new Error().stack;
673
- this.expected = value;
674
- this.assertCondition(typeof this.actual === 'number' && this.actual < value,
675
- `Expected ${this.stringify(this.actual)} to be less than ${value}`, true, String(value), stack);
676
- }
677
-
678
- toBeLessThanOrEqual(value: number) {
679
- const stack = new Error().stack;
680
- this.expected = value;
681
- this.assertCondition(typeof this.actual === 'number' && this.actual <= value,
682
- `Expected ${this.stringify(this.actual)} to be less than or equal to ${value}`, true, `${value}`, stack);
683
- }
684
-
685
- toContain(value: any) {
686
- const stack = new Error().stack;
687
- this.expected = value;
688
- if (typeof this.actual === 'string') {
689
- this.assertCondition(this.actual.includes(value),
690
- `Expected "${this.actual}" to contain "${value}"`, false, undefined, stack);
691
- } else if (Array.isArray(this.actual)) {
692
- this.assertCondition(this.actual.some(item => this.deepEqual(item, value)),
693
- `Expected array to contain ${this.stringify(value)}`, false, undefined, stack);
694
- } else {
695
- throw new Error(`toContain expects string or array, got ${typeof this.actual}`);
696
- }
697
- }
698
-
699
- toHaveLength(length: number) {
700
- const stack = new Error().stack;
701
- this.expected = length;
702
- const actualLength = this.actual?.length;
703
- this.assertCondition(actualLength === length,
704
- `Expected length to be ${length}, but got ${actualLength}`, false, undefined, stack);
705
- }
706
-
707
- toThrow(error?: any): any {
708
- // For async promises (.resolves/.rejects), use handleAsyncAssertion
709
- if (this.isAsync) {
710
- return this.handleAsyncAssertion(error, () => {
711
- // For async .toThrow, we just need to check that the promise rejected
712
- // The actual error checking is done in handleAsyncAssertion
713
- });
714
- }
715
-
716
- let threw = false;
717
- let thrownError: any = null;
718
- try {
719
- if (typeof this.actual === 'function') {
720
- this.actual();
721
- }
722
- } catch (e) {
723
- threw = true;
724
- thrownError = e;
725
- }
726
- this.assertCondition(threw, `Expected function to throw an error`);
727
- if (error) {
728
- if (typeof error === 'string') {
729
- this.assertCondition(thrownError.message.includes(error),
730
- `Expected error message to include "${error}"`);
731
- } else if (error instanceof RegExp) {
732
- this.assertCondition(error.test(thrownError.message),
733
- `Expected error message to match ${error}`);
734
- }
735
- }
736
- }
737
-
738
- toMatch(pattern: RegExp | string) {
739
- this.expected = pattern;
740
- const str = String(this.actual);
741
- if (pattern instanceof RegExp) {
742
- this.assertCondition(pattern.test(str),
743
- `Expected "${str}" to match ${pattern}`);
744
- } else {
745
- this.assertCondition(str.includes(pattern),
746
- `Expected "${str}" to contain "${pattern}"`);
747
- }
748
- }
749
-
750
- toBeInstanceOf(classType: any) {
751
- this.expected = classType;
752
- this.assertCondition(this.actual instanceof classType,
753
- `Expected value to be instance of ${classType.name}`);
754
- }
755
-
756
- toHaveProperty(path: string | string[], value?: any) {
757
- const keys = Array.isArray(path) ? path : path.split('.');
758
- let obj = this.actual;
759
- for (const key of keys) {
760
- if (obj == null || !Object.hasOwnProperty.call(obj, key)) {
761
- throw new Error(`Expected object to have property "${path}"`);
762
- }
763
- obj = obj[key];
764
- }
765
- if (value !== undefined) {
766
- this.assertCondition(this.deepEqual(obj, value),
767
- `Expected property "${path}" to equal ${this.stringify(value)}`);
768
- }
769
- }
770
-
771
- // Mock function matchers
772
- toBeCalled() {
773
- this.assertCondition(this.actual._isMock && this.actual._calls.length > 0,
774
- `Expected mock function to have been called`);
775
- }
776
-
777
- toBeCalledTimes(times: number) {
778
- this.assertCondition(this.actual._isMock && this.actual._calls.length === times,
779
- `Expected mock to be called ${times} times, but was called ${this.actual._calls?.length || 0} times`);
780
- }
781
-
782
- toBeCalledWith(...args: any[]) {
783
- this.assertCondition(this.actual._isMock && this.actual._calls.some((call: any[]) =>
784
- this.deepEqual(call, args)), `Expected mock to be called with ${this.stringify(args)}`);
785
- }
786
-
787
- lastReturnedWith(value: any) {
788
- const lastResult = this.actual._results?.[this.actual._results.length - 1];
789
- this.assertCondition(lastResult && this.deepEqual(lastResult.value, value),
790
- `Expected last call to return ${this.stringify(value)}`);
791
- }
792
-
793
- private deepEqual(a: any, b: any): boolean {
794
- return JSON.stringify(a) === JSON.stringify(b);
795
- }
796
- }
797
-
798
- function expect(actual: any): TestMatchers<any> {
799
- return new Expect(actual);
800
- }
801
-
802
- // ============================================================================
803
- // Mock Functions (vi.fn())
804
- // ============================================================================
805
-
806
- function createMockFunction<T extends (...args: any[]) => any>(): MockFunction<T> {
807
- const mock = function (...args: Parameters<T>): ReturnType<T> {
808
- mock._calls.push(args);
809
- try {
810
- const result = mock._implementation ? (mock._implementation as any)(...args) : undefined as any;
811
- mock._results.push({ type: 'return', value: result });
812
- return result;
813
- } catch (error) {
814
- mock._results.push({ type: 'throw', value: error });
815
- throw error;
816
- }
817
- } as MockFunction<T>;
818
-
819
- mock._isMock = true;
820
- mock._calls = [];
821
- mock._results = [];
822
- mock._implementation = null as any;
823
-
824
- mock.mockImplementation = function(fn: T) {
825
- mock._implementation = fn;
826
- return mock;
827
- };
828
-
829
- mock.mockReturnValue = function(value: ReturnType<T>) {
830
- mock._implementation = (() => value) as any;
831
- return mock;
832
- };
833
-
834
- mock.mockResolvedValue = function(value: ReturnType<T>) {
835
- mock._implementation = (() => Promise.resolve(value)) as any;
836
- return mock;
837
- };
838
-
839
- mock.mockRejectedValue = function(value: any) {
840
- mock._implementation = (() => Promise.reject(value)) as any;
841
- return mock;
842
- };
843
-
844
- mock.restore = function() {
845
- mock._calls = [];
846
- mock._results = [];
847
- mock._implementation = null as any;
848
- };
849
-
850
- mock.clear = function() {
851
- mock._calls = [];
852
- mock._results = [];
853
- };
854
-
855
- return mock;
856
- }
857
-
858
- const vi = {
859
- fn: <T extends (...args: any[]) => any>() => createMockFunction<T>(),
860
- spyOn: (obj: any, method: string) => {
861
- const original = obj[method];
862
- const mock = createMockFunction<typeof original>();
863
- mock.mockImplementation(original);
864
- obj[method] = mock;
865
- mock.restore = () => {
866
- obj[method] = original;
867
- };
868
- return mock;
869
- },
870
- clearAllMocks: () => {
871
- // Clear all mock calls
872
- },
873
- restoreAllMocks: () => {
874
- // Restore all mocks
875
- },
876
- };
877
-
878
- // ============================================================================
879
- // Hooks
880
- // ============================================================================
881
-
882
- let beforeAllHooks: Array<() => void | Promise<void>> = [];
883
- let afterAllHooks: Array<() => void | Promise<void>> = [];
884
- let beforeEachHooks: Array<() => void | Promise<void>> = [];
885
- let afterEachHooks: Array<() => void | Promise<void>> = [];
886
-
887
- const beforeAll = (fn: () => void | Promise<void>) => beforeAllHooks.push(fn);
888
- const afterAll = (fn: () => void | Promise<void>) => afterAllHooks.push(fn);
889
- const beforeEach = (fn: () => void | Promise<void>) => beforeEachHooks.push(fn);
890
- const afterEach = (fn: () => void | Promise<void>) => afterEachHooks.push(fn);
891
-
892
- // ============================================================================
893
- // Test Runner
894
- // ============================================================================
895
-
896
- export async function runTests(options: {
897
- files: string[];
898
- timeout?: number;
899
- bail?: boolean;
900
- describePattern?: string;
901
- testPattern?: string;
902
- }): Promise<{
903
- passed: number;
904
- failed: number;
905
- skipped: number;
906
- todo: number;
907
- results: TestResult[];
908
- }> {
909
- const { files, timeout = 5000, bail = false, describePattern: descPattern, testPattern: tPattern } = options;
910
-
911
- // Set filter patterns for executeSuite to use
912
- describePattern = descPattern;
913
- testPattern = tPattern;
914
-
915
- // Reset state
916
- testResults.length = 0;
917
- hasOnly = false;
918
-
919
- for (const file of files) {
920
- // Set current test file for reporting
921
- currentTestFile = file;
922
-
923
- try {
924
- // Read the source file directly
925
- const source = await readFile(file, 'utf-8') as string;
926
-
927
- // Get the directory of the test file for resolving relative imports
928
- const testFileDir = dirname(file);
929
-
930
- // Extract imports before esbuild processing
931
- const importRegex = /import\s+{\s*([^}]+)\s*}\s+from\s+['"]([^'"]+)['"]/g;
932
- const imports: Record<string, { path: string; named: string }> = {};
933
- let importIndex = 0;
934
-
935
- // Remove imports and collect them
936
- // Replace imports with nothing (they'll be injected back later)
937
- let codeWithoutImports = source.replace(importRegex, (_: string, named: string, path: string) => {
938
- const varName = `__import_${importIndex++}`;
939
- // Trim whitespace from the named import
940
- const trimmedNamed = named.trim();
941
- imports[varName] = { path, named: trimmedNamed };
942
- // Return a comment to mark where the import was
943
- return `// ${trimmedNamed} import injected later\n`;
944
- });
945
-
946
- // Transpile the code without imports using esbuild
947
- // We don't use esbuild's module format - we'll handle it ourselves
948
- const result = transformSync(codeWithoutImports, {
949
- loader: file.endsWith('.ts') || file.endsWith('.tsx') ? 'ts' : 'js',
950
- format: 'iife',
951
- sourcemap: 'inline',
952
- target: 'es2020',
953
- tsconfigRaw: {
954
- compilerOptions: {
955
- jsx: 'react',
956
- jsxFactory: 'h',
957
- jsxFragmentFactory: 'Fragment',
958
- },
959
- },
960
- });
961
-
962
- let code = result.code;
963
-
964
- // Extract and store source map for line number mapping
965
- const sourceMapMatch = code.match(/\/\/# sourceMappingURL=data:application\/json;base64,(.+)/);
966
- if (sourceMapMatch) {
967
- const base64 = sourceMapMatch[1];
968
- const json = Buffer.from(base64, 'base64').toString('utf-8');
969
- const sourceMap = JSON.parse(json) as RawSourceMap;
970
- currentSourceMapConsumer = await new SourceMapConsumer(sourceMap);
971
- } else {
972
- currentSourceMapConsumer = undefined;
973
- }
974
-
975
- // Add import helper at the top - resolve relative paths
976
- // Transpile and require imported modules
977
- const importedValues: Record<string, any> = {};
978
- const importParamNames: string[] = [];
979
- const importAssignments: string[] = [];
980
-
981
- // Check if imports were extracted
982
- if (Object.keys(imports).length > 0) {
983
- for (const [, { path, named }] of Object.entries(imports)) {
984
- // Resolve relative imports against the test file's directory
985
- let resolvedPath = path;
986
- if (path.startsWith('.')) {
987
- // Use Node's path.join for proper path resolution
988
- const nodePath = require('path');
989
- resolvedPath = nodePath.resolve(testFileDir, path);
990
- }
991
- // Add .ts extension if not present
992
- if (!resolvedPath.endsWith('.ts') && !resolvedPath.endsWith('.js') && !resolvedPath.endsWith('.mjs') && !resolvedPath.endsWith('.cjs')) {
993
- resolvedPath += '.ts';
994
- }
995
-
996
- // For TypeScript files, we need to transpile them first
997
- if (resolvedPath.endsWith('.ts')) {
998
- try {
999
- const importSource = await readFile(resolvedPath, 'utf-8') as string;
1000
- const transpiled = transformSync(importSource, {
1001
- loader: 'ts',
1002
- format: 'cjs',
1003
- target: 'es2020',
1004
- tsconfigRaw: {
1005
- compilerOptions: {
1006
- jsx: 'react',
1007
- jsxFactory: 'h',
1008
- jsxFragmentFactory: 'Fragment',
1009
- },
1010
- },
1011
- });
1012
-
1013
- // Create a temporary module object to capture exports
1014
- const moduleExports: any = {};
1015
- const moduleObj = { exports: moduleExports };
1016
-
1017
- // Execute the transpiled code with proper require function
1018
- const fn = new Function('module', 'exports', 'require', '__filename', '__dirname', transpiled.code);
1019
- const requireFn = (id: string) => {
1020
- // For 'elit/*' imports, use the actual require
1021
- if (id.startsWith('elit/') || id === 'elit') {
1022
- return require(id);
1023
- }
1024
- // For relative imports, recursively resolve them
1025
- if (id.startsWith('.')) {
1026
- const nodePath = require('path');
1027
- const absPath = nodePath.resolve(dirname(resolvedPath), id);
1028
- // For now, just use require (could add recursion here)
1029
- return require(absPath);
1030
- }
1031
- return require(id);
1032
- };
1033
- fn(moduleObj, moduleExports, requireFn, resolvedPath, dirname(resolvedPath));
1034
-
1035
- // Track this file for coverage (only source files, not test files)
1036
- if (!resolvedPath.includes('.test.') && !resolvedPath.includes('.spec.')) {
1037
- coveredFiles.add(resolvedPath);
1038
- }
1039
-
1040
- // Extract the named export
1041
- // esbuild CommonJS exports can be either directly on exports or on exports.default
1042
- let exportedValue = moduleObj.exports[named];
1043
- if (exportedValue === undefined && moduleObj.exports.default) {
1044
- exportedValue = moduleObj.exports.default[named];
1045
- }
1046
- // If still undefined, check if the exports object itself has the named property
1047
- if (exportedValue === undefined && typeof moduleObj.exports === 'object') {
1048
- exportedValue = (moduleObj.exports as any)[named];
1049
- }
1050
-
1051
- // Store the imported value and create parameter/assignment for it
1052
- const paramKey = `__import_${Math.random().toString(36).substring(2, 11)}`;
1053
- importedValues[paramKey] = exportedValue;
1054
- importParamNames.push(paramKey);
1055
- importAssignments.push(`const ${named} = ${paramKey};`);
1056
- } catch (err) {
1057
- // On error, store null and add error comment
1058
- const paramKey = `__import_${Math.random().toString(36).substring(2, 11)}`;
1059
- importedValues[paramKey] = null;
1060
- importParamNames.push(paramKey);
1061
- importAssignments.push(`const ${named} = ${paramKey}; /* Error importing ${resolvedPath}: ${err} */`);
1062
- }
1063
- } else {
1064
- // For JS files, use regular require()
1065
- const requiredModule = require(resolvedPath);
1066
- const exportedValue = requiredModule[named];
1067
- const paramKey = `__import_${Math.random().toString(36).substring(2, 11)}`;
1068
- importedValues[paramKey] = exportedValue;
1069
- importParamNames.push(paramKey);
1070
- importAssignments.push(`const ${named} = ${paramKey};`);
1071
- }
1072
- }
1073
- }
1074
-
1075
- // Now we need to extract named exports from required modules
1076
- // Add a preamble that handles the import statements
1077
- // Calculate the line offset from the wrapper
1078
- let preamble = '';
1079
- if (Object.keys(imports).length > 0) {
1080
- // Prepend the import assignments directly to the transpiled code
1081
- // The esbuild IIFE format creates a wrapper, so we need to inject our assignments inside it
1082
- // Find the start of the IIFE: (() => { or var <something> = (() => {
1083
- const iifeStartMatch = code.match(/^(\s*(?:var\s+\w+\s*=\s*)?\(\(\)\s*=>\s*\{\n)/);
1084
- if (iifeStartMatch) {
1085
- // Insert our assignments after the IIFE opening
1086
- const iifePrefix = iifeStartMatch[1];
1087
- const assignments = `${importAssignments.join('\n')}\n`;
1088
- preamble = iifePrefix;
1089
- code = iifePrefix + assignments + code.slice(iifeStartMatch[1].length);
1090
- } else {
1091
- // Fallback: just prepend without IIFE manipulation
1092
- preamble = importAssignments.join('\n') + '\n';
1093
- code = preamble + code;
1094
- }
1095
- }
1096
-
1097
- // Count the number of lines added by the wrapper
1098
- // The preamble adds: "(() => {" plus import and export lines
1099
- wrapperLineOffset = preamble.split('\n').length;
1100
-
1101
- // Execute the test code with test globals in context
1102
- // Add the imported values as parameters to the Function
1103
- setupGlobals();
1104
- const allParams = ['describe', 'it', 'test', 'expect', 'beforeAll', 'afterAll', 'beforeEach', 'afterEach', 'vi', 'require', 'module', '__filename', '__dirname', ...importParamNames];
1105
- const allArgs = [describe, it, test, expect, beforeAll, afterAll, beforeEach, afterEach, vi, require, module, file, testFileDir, ...importParamNames.map(p => importedValues[p])];
1106
- const fn = new Function(...allParams, code);
1107
- await fn(...allArgs);
1108
-
1109
- // Run tests
1110
- await executeSuite(currentSuite, timeout, bail);
1111
-
1112
- // Clean up source map consumer
1113
- if (currentSourceMapConsumer) {
1114
- currentSourceMapConsumer.destroy();
1115
- currentSourceMapConsumer = undefined;
1116
- }
1117
-
1118
- // Reset for next file
1119
- currentSuite = {
1120
- name: 'root',
1121
- tests: [],
1122
- suites: [],
1123
- skip: false,
1124
- only: false,
1125
- };
1126
- hasOnly = false;
1127
- beforeAllHooks = [];
1128
- afterAllHooks = [];
1129
- beforeEachHooks = [];
1130
- afterEachHooks = [];
1131
-
1132
- } catch (error) {
1133
- // Clean up source map consumer on error
1134
- if (currentSourceMapConsumer) {
1135
- currentSourceMapConsumer.destroy();
1136
- currentSourceMapConsumer = undefined;
1137
- }
1138
- console.error(`Error loading test file ${file}:`, error);
1139
- }
1140
- }
1141
-
1142
- const passed = testResults.filter(r => r.status === 'pass').length;
1143
- const failed = testResults.filter(r => r.status === 'fail').length;
1144
- const skipped = testResults.filter(r => r.status === 'skip').length;
1145
- const todo = testResults.filter(r => r.status === 'todo').length;
1146
-
1147
- return { passed, failed, skipped, todo, results: testResults };
1148
- }
1149
-
1150
- async function executeSuite(suite: TestSuite, timeout: number, bail: boolean, parentMatched: boolean = false): Promise<void> {
1151
- // Check if this suite directly matches the describe pattern (safe from regex injection)
1152
- let directMatch = false;
1153
- if (describePattern) {
1154
- const escapedPattern = escapeRegex(describePattern);
1155
- const regex = new RegExp(escapedPattern, 'i');
1156
- directMatch = regex.test(suite.name);
1157
- }
1158
-
1159
- // Helper function to check if this suite or any descendant matches the describe pattern (safe from regex injection)
1160
- function suiteOrDescendantMatches(s: TestSuite): boolean {
1161
- if (!describePattern) return true;
1162
-
1163
- const escapedPattern = escapeRegex(describePattern);
1164
- const regex = new RegExp(escapedPattern, 'i');
1165
- // Check if this suite matches
1166
- if (regex.test(s.name)) return true;
1167
-
1168
- // Check if any child suite matches
1169
- for (const child of s.suites) {
1170
- if (suiteOrDescendantMatches(child)) return true;
1171
- }
1172
-
1173
- return false;
1174
- }
1175
-
1176
- // A suite should run if:
1177
- // 1. No pattern (all run), OR
1178
- // 2. This suite directly matches, OR
1179
- // 3. Parent matched and we're checking descendants, OR
1180
- // 4. This suite or any descendant matches (for reaching into matched subtrees)
1181
- const shouldRunSuite = !describePattern || directMatch || parentMatched || suiteOrDescendantMatches(suite);
1182
- if (!shouldRunSuite) {
1183
- return;
1184
- }
1185
-
1186
- // Run child suites (they should run if we matched, to reach deeper descendants)
1187
- if (suite.suites.length > 0) {
1188
- for (const childSuite of suite.suites) {
1189
- await executeSuite(childSuite, timeout, bail, parentMatched || directMatch);
1190
- }
1191
- }
1192
-
1193
- // Only run this suite's tests if:
1194
- // 1. No pattern (all run), OR
1195
- // 2. This suite directly matches, OR
1196
- // 3. Parent matched (we're in a matched subtree), OR
1197
- // 4. This is the root suite (empty name)
1198
- const shouldRunTests = !describePattern || directMatch || parentMatched || suite.name === '';
1199
- if (!shouldRunTests) {
1200
- return;
1201
- }
1202
-
1203
- // Run beforeAll hooks
1204
- for (const hook of beforeAllHooks) {
1205
- await hook();
1206
- }
1207
-
1208
- for (const test of suite.tests) {
1209
- // Skip tests if we have only tests and this isn't one
1210
- if (hasOnly && !test.only && !suite.only) {
1211
- continue;
1212
- }
1213
-
1214
- // Check if this test matches the test name pattern (safe from regex injection)
1215
- let testMatches = true;
1216
- if (testPattern) {
1217
- const escapedPattern = escapeRegex(testPattern);
1218
- const regex = new RegExp(escapedPattern, 'i');
1219
- testMatches = regex.test(test.name);
1220
- }
1221
-
1222
- if (!testMatches) {
1223
- continue;
1224
- }
1225
-
1226
- if (test.skip || suite.skip) {
1227
- testResults.push({
1228
- name: test.name,
1229
- status: 'skip',
1230
- duration: 0,
1231
- suite: suite.name,
1232
- file: currentTestFile,
1233
- });
1234
- continue;
1235
- }
1236
-
1237
- if (test.todo) {
1238
- testResults.push({
1239
- name: test.name,
1240
- status: 'todo',
1241
- duration: 0,
1242
- suite: suite.name,
1243
- file: currentTestFile,
1244
- });
1245
- continue;
1246
- }
1247
-
1248
- // Run beforeEach hooks
1249
- for (const hook of beforeEachHooks) {
1250
- await hook();
1251
- }
1252
-
1253
- const startTime = Date.now();
1254
- try {
1255
- await Promise.race([
1256
- test.fn(),
1257
- new Promise((_, reject) =>
1258
- setTimeout(() => reject(new Error(`Test timed out after ${test.timeout}ms`)), test.timeout)
1259
- ),
1260
- ]);
1261
-
1262
- testResults.push({
1263
- name: test.name,
1264
- status: 'pass',
1265
- duration: Date.now() - startTime,
1266
- suite: suite.name,
1267
- file: currentTestFile,
1268
- });
1269
- } catch (error) {
1270
- // Extract assertion error details
1271
- let lineNumber: number | undefined = undefined;
1272
- let codeSnippet: string | undefined = undefined;
1273
- if (error instanceof AssertionError) {
1274
- lineNumber = error.lineNumber;
1275
- codeSnippet = error.codeSnippet;
1276
- }
1277
-
1278
- testResults.push({
1279
- name: test.name,
1280
- status: 'fail',
1281
- duration: Date.now() - startTime,
1282
- error: error as Error,
1283
- suite: suite.name,
1284
- file: currentTestFile,
1285
- lineNumber,
1286
- codeSnippet,
1287
- });
1288
-
1289
- if (bail) {
1290
- throw error;
1291
- }
1292
- }
1293
-
1294
- // Run afterEach hooks
1295
- for (const hook of afterEachHooks) {
1296
- await hook();
1297
- }
1298
- }
1299
-
1300
- // Run afterAll hooks
1301
- for (const hook of afterAllHooks) {
1302
- await hook();
1303
- }
1304
- }
1305
-
1306
- // ============================================================================
1307
- // Export Globals
1308
- // ============================================================================
1309
-
1310
- export const globals = {
1311
- describe: createDescribeFunction(),
1312
- it: createTestFunction(5000),
1313
- test: createTestFunction(5000),
1314
- expect,
1315
- beforeAll,
1316
- afterAll,
1317
- beforeEach,
1318
- afterEach,
1319
- vi,
1320
- };
1321
-
1322
- export function setupGlobals() {
1323
- (global as any).describe = globals.describe;
1324
- (global as any).it = globals.it;
1325
- (global as any).test = globals.test;
1326
- (global as any).expect = globals.expect;
1327
- (global as any).beforeAll = globals.beforeAll;
1328
- (global as any).afterAll = globals.afterAll;
1329
- (global as any).beforeEach = globals.beforeEach;
1330
- (global as any).afterEach = globals.afterEach;
1331
- (global as any).vi = globals.vi;
1332
- }
1333
-
1334
- export function clearGlobals() {
1335
- delete (global as any).describe;
1336
- delete (global as any).it;
1337
- delete (global as any).test;
1338
- delete (global as any).expect;
1339
- delete (global as any).beforeAll;
1340
- delete (global as any).afterAll;
1341
- delete (global as any).beforeEach;
1342
- delete (global as any).afterEach;
1343
- delete (global as any).vi;
1344
- }
1345
-
1346
- /**
1347
- * Get all source files that were loaded during test execution
1348
- * Used for coverage reporting
1349
- */
1350
- export function getCoveredFiles(): Set<string> {
1351
- return coveredFiles;
1352
- }
1353
-
1354
- /**
1355
- * Reset covered files tracking (call before running tests)
1356
- */
1357
- export function resetCoveredFiles(): void {
1358
- coveredFiles.clear();
1359
- }