@hyperfixi/testing-framework 2.0.0

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 (37) hide show
  1. package/dist/assertions-BImuwZsB.d.mts +458 -0
  2. package/dist/assertions-BImuwZsB.d.ts +458 -0
  3. package/dist/assertions.d.mts +1 -0
  4. package/dist/assertions.d.ts +1 -0
  5. package/dist/assertions.js +625 -0
  6. package/dist/assertions.js.map +1 -0
  7. package/dist/assertions.mjs +621 -0
  8. package/dist/assertions.mjs.map +1 -0
  9. package/dist/index.d.mts +246 -0
  10. package/dist/index.d.ts +246 -0
  11. package/dist/index.js +1465 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/index.mjs +1435 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +121 -0
  16. package/src/assertions.test.ts +490 -0
  17. package/src/assertions.ts +779 -0
  18. package/src/index.test.ts +490 -0
  19. package/src/index.ts +601 -0
  20. package/src/jsdom.d.ts +7 -0
  21. package/src/multilingual/README.md +295 -0
  22. package/src/multilingual/bundle-builder.ts +345 -0
  23. package/src/multilingual/cli.ts +224 -0
  24. package/src/multilingual/index.ts +68 -0
  25. package/src/multilingual/orchestrator.ts +275 -0
  26. package/src/multilingual/pattern-loader.ts +176 -0
  27. package/src/multilingual/reporters/console-reporter.ts +294 -0
  28. package/src/multilingual/reporters/json-reporter.ts +99 -0
  29. package/src/multilingual/reporters/regression-reporter.ts +263 -0
  30. package/src/multilingual/tools/analyze-failures.ts +326 -0
  31. package/src/multilingual/types.ts +268 -0
  32. package/src/multilingual/validators/parse-diagnostics.ts +337 -0
  33. package/src/multilingual/validators/parse-validator.ts +193 -0
  34. package/src/multilingual/validators/size-validator.ts +187 -0
  35. package/src/runner.test.ts +813 -0
  36. package/src/runner.ts +639 -0
  37. package/src/types.ts +505 -0
package/src/runner.ts ADDED
@@ -0,0 +1,639 @@
1
+ /**
2
+ * Test Runner
3
+ * Core test execution engine with multi-environment support
4
+ */
5
+
6
+ import { JSDOM } from 'jsdom';
7
+ import type {
8
+ TestRunner,
9
+ TestSuite,
10
+ TestCase,
11
+ TestConfig,
12
+ TestContext,
13
+ TestResult,
14
+ TestStatus,
15
+ TestError,
16
+ TestLog,
17
+ BrowserTestOptions,
18
+ PerformanceMetrics,
19
+ } from './types';
20
+ import { createExpectAPI } from './assertions';
21
+ import { createAssertAPI } from './assertions';
22
+
23
+ /**
24
+ * Test execution context
25
+ */
26
+ interface ExecutionContext {
27
+ config: TestConfig;
28
+ browser?: any;
29
+ page?: any;
30
+ logs: TestLog[];
31
+ screenshots: string[];
32
+ }
33
+
34
+ /**
35
+ * Core test runner implementation
36
+ */
37
+ export class CoreTestRunner implements TestRunner {
38
+ private executionContext: ExecutionContext | null = null;
39
+
40
+ /**
41
+ * Run all test suites
42
+ */
43
+ async run(suites: TestSuite[], config: TestConfig): Promise<TestResult[]> {
44
+ const results: TestResult[] = [];
45
+
46
+ // Setup execution context
47
+ this.executionContext = await this.setupExecutionContext(config);
48
+
49
+ try {
50
+ // Run setup files
51
+ await this.runSetupFiles(config.setupFiles);
52
+
53
+ // Run suites
54
+ for (const suite of suites) {
55
+ const suiteResults = await this.runSuite(suite, config);
56
+ results.push(...suiteResults);
57
+ }
58
+
59
+ // Run teardown files
60
+ await this.runTeardownFiles(config.teardownFiles);
61
+ } finally {
62
+ // Cleanup execution context
63
+ await this.cleanupExecutionContext();
64
+ }
65
+
66
+ return results;
67
+ }
68
+
69
+ /**
70
+ * Run a single test suite
71
+ */
72
+ async runSuite(suite: TestSuite, config: TestConfig): Promise<TestResult[]> {
73
+ const results: TestResult[] = [];
74
+
75
+ // Skip suite if marked
76
+ if (suite.skip) {
77
+ for (const test of suite.tests) {
78
+ results.push(this.createSkippedResult(suite.name, test.name));
79
+ }
80
+ return results;
81
+ }
82
+
83
+ try {
84
+ // Run suite setup
85
+ if (suite.setup) {
86
+ await suite.setup();
87
+ }
88
+
89
+ // Run tests
90
+ for (const test of suite.tests) {
91
+ const result = await this.runTest(test, this.createTestContext(suite, test, config));
92
+ results.push(result);
93
+ }
94
+
95
+ // Run nested suites
96
+ for (const nestedSuite of suite.suites) {
97
+ const nestedResults = await this.runSuite(nestedSuite, config);
98
+ results.push(...nestedResults);
99
+ }
100
+ } catch (error) {
101
+ // Suite setup/teardown failed
102
+ const testError = this.formatError(error);
103
+ for (const test of suite.tests) {
104
+ results.push({
105
+ suite: suite.name,
106
+ test: test.name,
107
+ status: 'failed',
108
+ duration: 0,
109
+ error: testError,
110
+ logs: [],
111
+ screenshots: [],
112
+ });
113
+ }
114
+ } finally {
115
+ try {
116
+ // Run suite teardown
117
+ if (suite.teardown) {
118
+ await suite.teardown();
119
+ }
120
+ } catch (error) {
121
+ console.error('Suite teardown failed:', error);
122
+ }
123
+ }
124
+
125
+ return results;
126
+ }
127
+
128
+ /**
129
+ * Run a single test
130
+ */
131
+ async runTest(test: TestCase, context: TestContext): Promise<TestResult> {
132
+ const startTime = Date.now();
133
+
134
+ // Skip test if marked
135
+ if (test.skip) {
136
+ return this.createSkippedResult(context.suite, context.test);
137
+ }
138
+
139
+ let status: TestStatus = 'running';
140
+ let error: TestError | undefined;
141
+ const logs: TestLog[] = [];
142
+ const screenshots: string[] = [];
143
+
144
+ try {
145
+ // Setup test fixtures
146
+ await this.setupTestFixtures(test, context);
147
+
148
+ // Run beforeEach hooks
149
+ await this.runBeforeEachHooks(context);
150
+
151
+ // Set test timeout
152
+ const timeout = test.timeout || this.executionContext?.config.timeout || 5000;
153
+ const timeoutPromise = new Promise<never>((_, reject) => {
154
+ setTimeout(() => reject(new Error(`Test timeout after ${timeout}ms`)), timeout);
155
+ });
156
+
157
+ // Run test function
158
+ await Promise.race([test.fn(context), timeoutPromise]);
159
+
160
+ status = 'passed';
161
+ } catch (err) {
162
+ status = 'failed';
163
+ error = this.formatError(err);
164
+
165
+ // Take screenshot on failure for browser tests
166
+ if (this.executionContext?.page && this.executionContext.config.environment === 'browser') {
167
+ try {
168
+ const screenshotPath = await this.takeScreenshot(context.suite, context.test);
169
+ if (screenshotPath) {
170
+ screenshots.push(screenshotPath);
171
+ }
172
+ } catch (screenshotError) {
173
+ console.warn('Failed to take screenshot:', screenshotError);
174
+ }
175
+ }
176
+ } finally {
177
+ try {
178
+ // Run afterEach hooks
179
+ await this.runAfterEachHooks(context);
180
+
181
+ // Cleanup test fixtures
182
+ await this.cleanupTestFixtures(test, context);
183
+ } catch (cleanupError) {
184
+ console.error('Test cleanup failed:', cleanupError);
185
+ }
186
+ }
187
+
188
+ const duration = Date.now() - startTime;
189
+
190
+ return {
191
+ suite: context.suite,
192
+ test: context.test,
193
+ status,
194
+ duration,
195
+ error,
196
+ logs: [...logs, ...(this.executionContext?.logs || [])],
197
+ screenshots,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Setup execution context
203
+ */
204
+ private async setupExecutionContext(config: TestConfig): Promise<ExecutionContext> {
205
+ const context: ExecutionContext = {
206
+ config,
207
+ logs: [],
208
+ screenshots: [],
209
+ };
210
+
211
+ switch (config.environment) {
212
+ case 'jsdom':
213
+ await this.setupJSDOMContext(context);
214
+ break;
215
+ case 'browser':
216
+ case 'playwright':
217
+ await this.setupBrowserContext(context);
218
+ break;
219
+ case 'puppeteer':
220
+ await this.setupPuppeteerContext(context);
221
+ break;
222
+ case 'node':
223
+ // No additional setup needed for Node.js
224
+ break;
225
+ }
226
+
227
+ return context;
228
+ }
229
+
230
+ /**
231
+ * Setup JSDOM context
232
+ */
233
+ private async setupJSDOMContext(context: ExecutionContext): Promise<void> {
234
+ const { config } = context;
235
+
236
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
237
+ url: config.baseURL || 'http://localhost',
238
+ contentType: 'text/html',
239
+ pretendToBeVisual: true,
240
+ resources: 'usable',
241
+ });
242
+
243
+ // Setup global DOM variables
244
+ global.window = dom.window as any;
245
+ global.document = dom.window.document;
246
+ global.navigator = dom.window.navigator;
247
+ global.location = dom.window.location;
248
+ global.HTMLElement = dom.window.HTMLElement;
249
+ global.Event = dom.window.Event;
250
+ global.CustomEvent = dom.window.CustomEvent;
251
+ }
252
+
253
+ /**
254
+ * Setup browser context (Playwright)
255
+ */
256
+ private async setupBrowserContext(context: ExecutionContext): Promise<void> {
257
+ try {
258
+ const { chromium, firefox, webkit } = await import('playwright');
259
+ const { config } = context;
260
+
261
+ let browserEngine;
262
+ switch (config.browser) {
263
+ case 'firefox':
264
+ browserEngine = firefox;
265
+ break;
266
+ case 'webkit':
267
+ browserEngine = webkit;
268
+ break;
269
+ default:
270
+ browserEngine = chromium;
271
+ }
272
+
273
+ const browser = await browserEngine.launch({
274
+ headless: config.headless,
275
+ });
276
+
277
+ context.browser = browser;
278
+
279
+ const browserContext = await browser.newContext({
280
+ ...(config.viewport && { viewport: config.viewport }),
281
+ ...(config.baseURL && { baseURL: config.baseURL }),
282
+ });
283
+
284
+ context.page = await browserContext.newPage();
285
+
286
+ // Setup console logging
287
+ context.page.on('console', (msg: any) => {
288
+ context.logs.push({
289
+ level: msg.type() as any,
290
+ message: msg.text(),
291
+ timestamp: Date.now(),
292
+ source: 'browser',
293
+ });
294
+ });
295
+
296
+ // Setup error logging
297
+ context.page.on('pageerror', (error: Error) => {
298
+ context.logs.push({
299
+ level: 'error',
300
+ message: error.message,
301
+ timestamp: Date.now(),
302
+ source: 'browser',
303
+ });
304
+ });
305
+ } catch (error) {
306
+ throw new Error(`Failed to setup browser context: ${error}`);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Setup Puppeteer context
312
+ */
313
+ private async setupPuppeteerContext(context: ExecutionContext): Promise<void> {
314
+ try {
315
+ const puppeteer = await import('puppeteer');
316
+ const { config } = context;
317
+
318
+ const browser = await puppeteer.default.launch({
319
+ headless: config.headless,
320
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
321
+ });
322
+
323
+ context.browser = browser;
324
+ context.page = await browser.newPage();
325
+
326
+ await context.page.setViewport(config.viewport);
327
+
328
+ if (config.baseURL) {
329
+ await context.page.goto(config.baseURL);
330
+ }
331
+
332
+ // Setup console logging
333
+ context.page.on('console', (msg: any) => {
334
+ context.logs.push({
335
+ level: msg.type() as any,
336
+ message: msg.text(),
337
+ timestamp: Date.now(),
338
+ source: 'browser',
339
+ });
340
+ });
341
+
342
+ // Setup error logging
343
+ context.page.on('pageerror', (error: Error) => {
344
+ context.logs.push({
345
+ level: 'error',
346
+ message: error.message,
347
+ timestamp: Date.now(),
348
+ source: 'browser',
349
+ });
350
+ });
351
+ } catch (error) {
352
+ throw new Error(`Failed to setup Puppeteer context: ${error}`);
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Create test context
358
+ */
359
+ private createTestContext(suite: TestSuite, test: TestCase, config: TestConfig): TestContext {
360
+ const context: TestContext = {
361
+ suite: suite.name,
362
+ test: test.name,
363
+ environment: config.environment,
364
+ fixtures: { ...config.fixtures, ...test.fixtures },
365
+ expect: createExpectAPI(),
366
+ assert: createAssertAPI(),
367
+ skip: () => {
368
+ throw new Error('SKIP_TEST');
369
+ },
370
+ fail: (message: string) => {
371
+ throw new Error(message);
372
+ },
373
+ timeout: (ms: number) => {
374
+ // Timeout functionality would be implemented here
375
+ },
376
+ };
377
+
378
+ // Add environment-specific context
379
+ if (this.executionContext?.page) {
380
+ context.page = this.executionContext.page;
381
+ context.browser = this.executionContext.browser;
382
+ }
383
+
384
+ if (config.environment === 'jsdom') {
385
+ context.document = global.document;
386
+ context.window = global.window;
387
+ }
388
+
389
+ return context;
390
+ }
391
+
392
+ /**
393
+ * Setup test fixtures
394
+ */
395
+ private async setupTestFixtures(test: TestCase, context: TestContext): Promise<void> {
396
+ // Fixture setup would be implemented here
397
+ // This would load and initialize test fixtures
398
+ }
399
+
400
+ /**
401
+ * Cleanup test fixtures
402
+ */
403
+ private async cleanupTestFixtures(test: TestCase, context: TestContext): Promise<void> {
404
+ // Fixture cleanup would be implemented here
405
+ }
406
+
407
+ /**
408
+ * Run beforeEach hooks
409
+ */
410
+ private async runBeforeEachHooks(context: TestContext): Promise<void> {
411
+ // Hook execution would be implemented here
412
+ }
413
+
414
+ /**
415
+ * Run afterEach hooks
416
+ */
417
+ private async runAfterEachHooks(context: TestContext): Promise<void> {
418
+ // Hook execution would be implemented here
419
+ }
420
+
421
+ /**
422
+ * Run setup files
423
+ */
424
+ private async runSetupFiles(setupFiles: string[]): Promise<void> {
425
+ for (const file of setupFiles) {
426
+ try {
427
+ await import(file);
428
+ } catch (error) {
429
+ console.error(`Failed to run setup file ${file}:`, error);
430
+ throw error;
431
+ }
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Run teardown files
437
+ */
438
+ private async runTeardownFiles(teardownFiles: string[]): Promise<void> {
439
+ for (const file of teardownFiles) {
440
+ try {
441
+ await import(file);
442
+ } catch (error) {
443
+ console.error(`Failed to run teardown file ${file}:`, error);
444
+ }
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Take screenshot
450
+ */
451
+ private async takeScreenshot(suite: string, test: string): Promise<string | null> {
452
+ if (!this.executionContext?.page) return null;
453
+
454
+ try {
455
+ const filename = `${suite}-${test}-${Date.now()}.png`;
456
+ const path = `./screenshots/${filename}`;
457
+
458
+ await this.executionContext.page.screenshot({
459
+ path,
460
+ fullPage: true,
461
+ });
462
+
463
+ return path;
464
+ } catch (error) {
465
+ console.error('Screenshot failed:', error);
466
+ return null;
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Format error for test result
472
+ */
473
+ private formatError(error: any): TestError {
474
+ if (error instanceof Error) {
475
+ return {
476
+ name: error.name,
477
+ message: error.message,
478
+ stack: error.stack,
479
+ };
480
+ }
481
+
482
+ return {
483
+ name: 'Error',
484
+ message: String(error),
485
+ };
486
+ }
487
+
488
+ /**
489
+ * Create skipped test result
490
+ */
491
+ private createSkippedResult(suite: string, test: string): TestResult {
492
+ return {
493
+ suite,
494
+ test,
495
+ status: 'skipped',
496
+ duration: 0,
497
+ logs: [],
498
+ screenshots: [],
499
+ };
500
+ }
501
+
502
+ /**
503
+ * Cleanup execution context
504
+ */
505
+ private async cleanupExecutionContext(): Promise<void> {
506
+ if (!this.executionContext) return;
507
+
508
+ try {
509
+ // Close browser if opened
510
+ if (this.executionContext.browser) {
511
+ await this.executionContext.browser.close();
512
+ }
513
+
514
+ // Clean up JSDOM globals
515
+ if (this.executionContext.config.environment === 'jsdom') {
516
+ delete (global as any).window;
517
+ delete (global as any).document;
518
+ delete (global as any).navigator;
519
+ delete (global as any).location;
520
+ delete (global as any).HTMLElement;
521
+ delete (global as any).Event;
522
+ delete (global as any).CustomEvent;
523
+ }
524
+ } catch (error) {
525
+ console.error('Cleanup failed:', error);
526
+ } finally {
527
+ this.executionContext = null;
528
+ }
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Parallel test runner
534
+ */
535
+ export class ParallelTestRunner extends CoreTestRunner {
536
+ /**
537
+ * Run test suites in parallel
538
+ */
539
+ async run(suites: TestSuite[], config: TestConfig): Promise<TestResult[]> {
540
+ if (!config.parallel || config.maxWorkers <= 1) {
541
+ return super.run(suites, config);
542
+ }
543
+
544
+ const workers = Math.min(config.maxWorkers, suites.length);
545
+ const chunks = this.chunkSuites(suites, workers);
546
+
547
+ const workerPromises = chunks.map(chunk => this.runWorker(chunk, config));
548
+
549
+ const workerResults = await Promise.all(workerPromises);
550
+ return workerResults.flat();
551
+ }
552
+
553
+ /**
554
+ * Split suites into chunks for parallel execution
555
+ */
556
+ private chunkSuites(suites: TestSuite[], chunkCount: number): TestSuite[][] {
557
+ const chunks: TestSuite[][] = [];
558
+ const chunkSize = Math.ceil(suites.length / chunkCount);
559
+
560
+ for (let i = 0; i < suites.length; i += chunkSize) {
561
+ chunks.push(suites.slice(i, i + chunkSize));
562
+ }
563
+
564
+ return chunks;
565
+ }
566
+
567
+ /**
568
+ * Run a worker with assigned test suites
569
+ */
570
+ private async runWorker(suites: TestSuite[], config: TestConfig): Promise<TestResult[]> {
571
+ const worker = new CoreTestRunner();
572
+ return worker.run(suites, config);
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Create test runner based on configuration
578
+ */
579
+ export function createTestRunner(config: TestConfig): TestRunner {
580
+ if (config.parallel && config.maxWorkers > 1) {
581
+ return new ParallelTestRunner();
582
+ }
583
+ return new CoreTestRunner();
584
+ }
585
+
586
+ /**
587
+ * Run performance measurements
588
+ */
589
+ export async function measurePerformance(
590
+ page: any,
591
+ testFn: () => Promise<void>
592
+ ): Promise<PerformanceMetrics> {
593
+ // Clear existing performance entries
594
+ await page.evaluate(() => {
595
+ performance.clearMarks();
596
+ performance.clearMeasures();
597
+ });
598
+
599
+ const startTime = Date.now();
600
+
601
+ // Run the test function
602
+ await testFn();
603
+
604
+ // Collect performance metrics
605
+ const metrics = await page.evaluate(() => {
606
+ const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
607
+ const paint = performance.getEntriesByType('paint');
608
+
609
+ const firstPaint = paint.find(entry => entry.name === 'first-paint')?.startTime || 0;
610
+ const firstContentfulPaint =
611
+ paint.find(entry => entry.name === 'first-contentful-paint')?.startTime || 0;
612
+
613
+ return {
614
+ loadTime: navigation ? navigation.loadEventEnd - navigation.startTime : 0,
615
+ domContentLoaded: navigation ? navigation.domContentLoadedEventEnd - navigation.startTime : 0,
616
+ firstPaint,
617
+ firstContentfulPaint,
618
+ largestContentfulPaint: 0, // Would need additional measurement
619
+ cumulativeLayoutShift: 0, // Would need additional measurement
620
+ firstInputDelay: 0, // Would need additional measurement
621
+ memoryUsage: (performance as any).memory
622
+ ? {
623
+ usedJSHeapSize: (performance as any).memory.usedJSHeapSize,
624
+ totalJSHeapSize: (performance as any).memory.totalJSHeapSize,
625
+ jsHeapSizeLimit: (performance as any).memory.jsHeapSizeLimit,
626
+ }
627
+ : {
628
+ usedJSHeapSize: 0,
629
+ totalJSHeapSize: 0,
630
+ jsHeapSizeLimit: 0,
631
+ },
632
+ };
633
+ });
634
+
635
+ return {
636
+ ...metrics,
637
+ loadTime: metrics.loadTime || Date.now() - startTime,
638
+ };
639
+ }