qa360 2.0.12 → 2.1.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 (66) hide show
  1. package/dist/commands/ai.js +26 -14
  2. package/dist/commands/ask.d.ts +75 -23
  3. package/dist/commands/ask.js +413 -265
  4. package/dist/commands/crawl.d.ts +24 -0
  5. package/dist/commands/crawl.js +121 -0
  6. package/dist/commands/history.js +38 -3
  7. package/dist/commands/init.d.ts +89 -95
  8. package/dist/commands/init.js +282 -200
  9. package/dist/commands/run.d.ts +1 -0
  10. package/dist/core/adapters/playwright-native-adapter.d.ts +121 -0
  11. package/dist/core/adapters/playwright-native-adapter.js +339 -0
  12. package/dist/core/adapters/playwright-ui.d.ts +83 -7
  13. package/dist/core/adapters/playwright-ui.js +525 -59
  14. package/dist/core/artifacts/index.d.ts +6 -0
  15. package/dist/core/artifacts/index.js +6 -0
  16. package/dist/core/artifacts/ui-artifacts.d.ts +133 -0
  17. package/dist/core/artifacts/ui-artifacts.js +304 -0
  18. package/dist/core/assertions/engine.d.ts +51 -0
  19. package/dist/core/assertions/engine.js +530 -0
  20. package/dist/core/assertions/index.d.ts +11 -0
  21. package/dist/core/assertions/index.js +11 -0
  22. package/dist/core/assertions/types.d.ts +121 -0
  23. package/dist/core/assertions/types.js +37 -0
  24. package/dist/core/crawler/index.d.ts +57 -0
  25. package/dist/core/crawler/index.js +281 -0
  26. package/dist/core/crawler/journey-generator.d.ts +49 -0
  27. package/dist/core/crawler/journey-generator.js +412 -0
  28. package/dist/core/crawler/page-analyzer.d.ts +88 -0
  29. package/dist/core/crawler/page-analyzer.js +709 -0
  30. package/dist/core/crawler/selector-generator.d.ts +34 -0
  31. package/dist/core/crawler/selector-generator.js +240 -0
  32. package/dist/core/crawler/types.d.ts +353 -0
  33. package/dist/core/crawler/types.js +6 -0
  34. package/dist/core/generation/crawler-pack-generator.d.ts +44 -0
  35. package/dist/core/generation/crawler-pack-generator.js +231 -0
  36. package/dist/core/generation/index.d.ts +2 -0
  37. package/dist/core/generation/index.js +2 -0
  38. package/dist/core/index.d.ts +9 -0
  39. package/dist/core/index.js +13 -0
  40. package/dist/core/parallel/index.d.ts +6 -0
  41. package/dist/core/parallel/index.js +6 -0
  42. package/dist/core/parallel/parallel-runner.d.ts +107 -0
  43. package/dist/core/parallel/parallel-runner.js +192 -0
  44. package/dist/core/reporting/html-reporter.d.ts +119 -0
  45. package/dist/core/reporting/html-reporter.js +737 -0
  46. package/dist/core/reporting/index.d.ts +6 -0
  47. package/dist/core/reporting/index.js +6 -0
  48. package/dist/core/runner/phase3-runner.js +5 -1
  49. package/dist/core/types/pack-v1.d.ts +90 -0
  50. package/dist/core/vault/cas.d.ts +5 -1
  51. package/dist/core/vault/cas.js +6 -0
  52. package/dist/core/visual/index.d.ts +6 -0
  53. package/dist/core/visual/index.js +6 -0
  54. package/dist/core/visual/visual-regression.d.ts +113 -0
  55. package/dist/core/visual/visual-regression.js +236 -0
  56. package/dist/index.js +6 -2
  57. package/examples/README.md +38 -0
  58. package/examples/accessibility.yml +39 -16
  59. package/examples/api-basic.yml +19 -14
  60. package/examples/complete.yml +134 -42
  61. package/examples/crawler.yml +38 -0
  62. package/examples/fullstack.yml +66 -31
  63. package/examples/security.yml +47 -15
  64. package/examples/ui-advanced.yml +49 -0
  65. package/examples/ui-basic.yml +16 -12
  66. package/package.json +1 -1
@@ -0,0 +1,231 @@
1
+ /**
2
+ * QA360 Crawler Pack Generator
3
+ *
4
+ * Crawls a website and generates a complete pack.yml with E2E tests
5
+ */
6
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
7
+ import { resolve, dirname } from 'path';
8
+ import { crawlWebsite } from '../crawler/index.js';
9
+ /**
10
+ * Generate pack.yml from crawled website
11
+ */
12
+ export async function generatePackFromCrawl(options) {
13
+ try {
14
+ console.log(`šŸ•·ļø Crawling ${options.baseUrl}...`);
15
+ // Crawl the website
16
+ const crawlResult = await crawlWebsite({
17
+ baseUrl: options.baseUrl,
18
+ maxDepth: options.crawl?.maxDepth || 2,
19
+ maxPages: options.crawl?.maxPages || 20,
20
+ headless: options.crawl?.headless ?? true,
21
+ screenshots: false,
22
+ timeout: 30000,
23
+ excludePatterns: options.crawl?.excludePatterns,
24
+ });
25
+ if (!crawlResult.success) {
26
+ return {
27
+ success: false,
28
+ result: crawlResult,
29
+ error: 'Crawling failed',
30
+ };
31
+ }
32
+ console.log(` āœ… Discovered ${crawlResult.siteMap.metadata.pagesCrawled} pages`);
33
+ console.log(` āœ… Found ${crawlResult.forms.length} forms`);
34
+ console.log(` āœ… Generated ${crawlResult.userJourneys.length} user journeys`);
35
+ // Generate pack.yml
36
+ const pack = generatePackFromCrawlResult(crawlResult, options);
37
+ // Write pack file
38
+ const packPath = options.output || resolve(process.cwd(), 'pack.yml');
39
+ const packDir = dirname(packPath);
40
+ if (!existsSync(packDir)) {
41
+ mkdirSync(packDir, { recursive: true });
42
+ }
43
+ writeFileSync(packPath, pack, 'utf-8');
44
+ console.log(`\nšŸ“¦ Generated pack: ${packPath}`);
45
+ return {
46
+ success: true,
47
+ packPath,
48
+ result: crawlResult,
49
+ };
50
+ }
51
+ catch (error) {
52
+ return {
53
+ success: false,
54
+ error: error instanceof Error ? error.message : String(error),
55
+ };
56
+ }
57
+ }
58
+ /**
59
+ * Generate pack.yml YAML from crawl result
60
+ */
61
+ function generatePackFromCrawlResult(crawlResult, options) {
62
+ const { siteMap, userJourneys, forms } = crawlResult;
63
+ const packName = options.packName || `${new URL(options.baseUrl).hostname.replace(/\./g, '-')}-tests`;
64
+ // Build gates array
65
+ const gates = ['ui'];
66
+ if (options.includeA11y)
67
+ gates.push('a11y');
68
+ // Build UI tests from user journeys
69
+ const uiTests = userJourneys.map((journey, index) => ({
70
+ name: options.journeyNames?.[journey.name] || journey.name,
71
+ description: journey.description,
72
+ path: new URL(journey.entryPoint).pathname,
73
+ steps: journey.steps.map((step, stepIndex) => journeyStepToUiTestStep(step, stepIndex)),
74
+ }));
75
+ // Also add form-based tests
76
+ for (const form of forms) {
77
+ if (form.purpose !== 'login' && form.purpose !== 'signup') {
78
+ uiTests.push({
79
+ name: `${form.purpose.charAt(0).toUpperCase() + form.purpose.slice(1)} Form Test`,
80
+ description: `Test the ${form.purpose} form`,
81
+ path: siteMap.pages.find(p => p.elements.forms.includes(form))?.path || '/',
82
+ steps: generateFormTestSteps(form),
83
+ });
84
+ }
85
+ }
86
+ // Generate YAML
87
+ let yaml = `# QA360 Pack - Generated by crawler
88
+ # Source: ${options.baseUrl}
89
+ # Generated: ${new Date().toISOString()}
90
+
91
+ version: 1
92
+ name: "${packName}"
93
+ description: "Auto-generated E2E tests for ${new URL(options.baseUrl).hostname}"
94
+
95
+ gates:
96
+ ${gates.map(g => ` - ${g}`).join('\n')}
97
+
98
+ targets:
99
+ web:
100
+ baseUrl: "${options.baseUrl}"
101
+ headless: true
102
+ screenshot: "only-on-fail"
103
+ video: "retain-on-fail"
104
+
105
+ # Pages discovered by crawler
106
+ pages:
107
+ ${siteMap.pages.map(p => ` - "${p.path}"`).join('\n')}
108
+
109
+ # E2E UI tests generated from discovered user journeys
110
+ uiTests:
111
+ ${uiTests.map(test => generateUiTestYaml(test)).join('\n')}
112
+ `;
113
+ if (siteMap.metadata.pagesCrawled > 0) {
114
+ yaml += `
115
+ budgets:
116
+ a11y_min: 80
117
+ perf_p95_ms: ${Math.round(siteMap.metadata.avgLoadTime * 1.5)}
118
+ `;
119
+ }
120
+ return yaml;
121
+ }
122
+ /**
123
+ * Convert journey step to UI test step
124
+ */
125
+ function journeyStepToUiTestStep(step, index) {
126
+ const uiStep = {
127
+ order: index + 1,
128
+ action: step.action,
129
+ description: step.description,
130
+ selector: step.selector,
131
+ value: step.value,
132
+ wait: 500,
133
+ };
134
+ if (step.expected) {
135
+ uiStep.expected = {};
136
+ if (step.expected.url)
137
+ uiStep.expected.url = step.expected.url;
138
+ if (step.expected.visible)
139
+ uiStep.expected.visible = step.expected.visible;
140
+ }
141
+ return uiStep;
142
+ }
143
+ /**
144
+ * Generate test steps for a form
145
+ */
146
+ function generateFormTestSteps(form) {
147
+ const steps = [];
148
+ // Navigate to form
149
+ steps.push({
150
+ order: 1,
151
+ action: 'navigate',
152
+ description: 'Navigate to form',
153
+ value: form.selector,
154
+ });
155
+ // Fill form fields
156
+ for (const field of form.fields) {
157
+ if (field.required) {
158
+ const step = {
159
+ order: steps.length + 1,
160
+ action: field.inputType === 'select-one' ? 'select' : 'fill',
161
+ description: `Fill ${field.name || field.inputType}`,
162
+ selector: field.selector,
163
+ };
164
+ if (field.inputType === 'select-one' && field.options && field.options.length > 0) {
165
+ step.value = field.options[0];
166
+ }
167
+ else if (field.inputType === 'email') {
168
+ step.value = 'test@example.com';
169
+ }
170
+ else if (field.inputType === 'tel') {
171
+ step.value = '+1234567890';
172
+ }
173
+ else if (field.inputType === 'checkbox') {
174
+ step.action = 'check';
175
+ }
176
+ else {
177
+ step.value = 'Test value';
178
+ }
179
+ steps.push(step);
180
+ }
181
+ }
182
+ // Submit form
183
+ if (form.submitButton) {
184
+ steps.push({
185
+ order: steps.length + 1,
186
+ action: 'click',
187
+ description: 'Submit form',
188
+ selector: form.submitButton.selector,
189
+ wait: 1000,
190
+ });
191
+ }
192
+ return steps;
193
+ }
194
+ /**
195
+ * Generate YAML for a UI test definition
196
+ */
197
+ function generateUiTestYaml(test) {
198
+ const steps = test.steps.map(step => {
199
+ let yaml = ` - action: ${step.action}`;
200
+ if (step.description)
201
+ yaml += `\n description: "${step.description}"`;
202
+ if (step.selector)
203
+ yaml += `\n selector: "${step.selector}"`;
204
+ if (step.value)
205
+ yaml += `\n value: "${step.value}"`;
206
+ if (step.expected)
207
+ yaml += `\n expected: ${JSON.stringify(step.expected)}`;
208
+ if (step.wait)
209
+ yaml += `\n wait: ${step.wait}`;
210
+ return yaml;
211
+ }).join('\n');
212
+ return ` - name: "${test.name}"
213
+ description: "${test.description || ''}"
214
+ path: "${test.path || '/'}"
215
+ steps:
216
+ ${steps}`;
217
+ }
218
+ /**
219
+ * Quick crawl - generates pack without detailed analysis
220
+ */
221
+ export async function quickCrawl(baseUrl, outputPath) {
222
+ return generatePackFromCrawl({
223
+ baseUrl,
224
+ output: outputPath,
225
+ crawl: {
226
+ maxDepth: 1,
227
+ maxPages: 10,
228
+ },
229
+ includeA11y: true,
230
+ });
231
+ }
@@ -28,3 +28,5 @@ export type { PackGeneratorOptions, PackGenerationResult } from './pack-generato
28
28
  export type { SourceAnalysis, SourceAnalyzerOptions, TestSuggestion } from './source-analyzer.js';
29
29
  export type { TestSpec, ApiTestSpec, UiTestSpec, PerfTestSpec, UnitTestSpec, GenerationContext, GeneratedTest, GenerationSource, GenerationOptions, } from './types.js';
30
30
  export { generateTests, generateApiTestsFromOpenAPI, generateUiTestsFromUrl, generatePerfTests, generateUnitTests, checkGenerationAvailability, } from './generator.js';
31
+ export { generatePackFromCrawl, quickCrawl, } from './crawler-pack-generator.js';
32
+ export type { CrawlerPackGeneratorOptions } from './crawler-pack-generator.js';
@@ -26,3 +26,5 @@ export { PackGenerator, generatePackFromApiSpec, generatePackFromUiSpec, generat
26
26
  export { SourceAnalyzer, analyzeSourceFile, analyzeSourceDirectory, generateTestSpec } from './source-analyzer.js';
27
27
  // Convenience functions
28
28
  export { generateTests, generateApiTestsFromOpenAPI, generateUiTestsFromUrl, generatePerfTests, generateUnitTests, checkGenerationAvailability, } from './generator.js';
29
+ // Crawler Pack Generator (UI Testing 100%)
30
+ export { generatePackFromCrawl, quickCrawl, } from './crawler-pack-generator.js';
@@ -93,3 +93,12 @@ export * from './self-healing/index.js';
93
93
  export * from './coverage/index.js';
94
94
  export * from './slo/index.js';
95
95
  export * from './regression/index.js';
96
+ export * from './crawler/index.js';
97
+ export { AssertionsEngine, createAssertionsEngine, AssertionResult, AssertionGroupResult, AssertionRunOptions, AssertionError, SoftAssertionError, Assertion, AssertionOperator, AssertionSuite, } from './assertions/index.js';
98
+ export type { AssertionType as UiAssertionType } from './assertions/types.js';
99
+ export * from './artifacts/index.js';
100
+ export { HTMLReporter, generateHTMLReport } from './reporting/index.js';
101
+ export type { ReportData, TestReport, StepReport, } from './reporting/index.js';
102
+ export type { ScreenshotArtifact as ReportScreenshotArtifact, VideoArtifact as ReportVideoArtifact, TraceArtifact as ReportTraceArtifact, } from './reporting/index.js';
103
+ export * from './parallel/index.js';
104
+ export * from './visual/index.js';
@@ -76,3 +76,16 @@ export * from './coverage/index.js';
76
76
  export * from './slo/index.js';
77
77
  // Regression Detection Module (Vision 2.0 - Phase 2 - F12)
78
78
  export * from './regression/index.js';
79
+ // Crawler Module (Vision 2.0 - UI Testing 100%)
80
+ export * from './crawler/index.js';
81
+ // Assertions Engine (Vision 2.0 - UI Testing 100%)
82
+ export { AssertionsEngine, createAssertionsEngine, AssertionError, SoftAssertionError, } from './assertions/index.js';
83
+ // Artifacts Module (Vision 2.0 - UI Testing Artifacts)
84
+ export * from './artifacts/index.js';
85
+ // Reporting Module (Vision 2.0 - HTML Reports)
86
+ // Note: Using selective exports to avoid naming conflicts with artifacts module
87
+ export { HTMLReporter, generateHTMLReport } from './reporting/index.js';
88
+ // Parallel Module (Vision 2.0 - Parallel Test Execution)
89
+ export * from './parallel/index.js';
90
+ // Visual Regression Module (Vision 2.0 - Visual Testing)
91
+ export * from './visual/index.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QA360 Parallel Module
3
+ *
4
+ * Parallel test execution for faster feedback
5
+ */
6
+ export { ParallelTestRunner, createParallelRunner, runParallelTests, type ParallelTestConfig, type ParallelTestResult, type ParallelRunResult, type WorkerMessage, type WorkerJob, } from './parallel-runner.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QA360 Parallel Module
3
+ *
4
+ * Parallel test execution for faster feedback
5
+ */
6
+ export { ParallelTestRunner, createParallelRunner, runParallelTests, } from './parallel-runner.js';
@@ -0,0 +1,107 @@
1
+ /**
2
+ * QA360 Parallel Test Runner
3
+ *
4
+ * Runs multiple UI tests concurrently for faster execution.
5
+ * Supports worker pools, load balancing, and resource limits.
6
+ */
7
+ export interface ParallelTestConfig {
8
+ /** Test definition to run */
9
+ test: any;
10
+ /** Target configuration */
11
+ target: any;
12
+ /** Worker timeout in ms */
13
+ timeout?: number;
14
+ /** Maximum concurrent workers (default: CPU count) */
15
+ maxWorkers?: number;
16
+ /** Number of retries for failed tests */
17
+ retries?: number;
18
+ }
19
+ export interface ParallelTestResult {
20
+ test: any;
21
+ success: boolean;
22
+ duration: number;
23
+ error?: string;
24
+ artifacts?: any;
25
+ workerId?: number;
26
+ }
27
+ export interface ParallelRunResult {
28
+ success: boolean;
29
+ totalTests: number;
30
+ passed: number;
31
+ failed: number;
32
+ skipped: number;
33
+ duration: number;
34
+ results: ParallelTestResult[];
35
+ workerStats: {
36
+ totalWorkers: number;
37
+ totalJobs: number;
38
+ avgJobDuration: number;
39
+ };
40
+ }
41
+ export interface WorkerMessage {
42
+ type: 'run' | 'result' | 'error' | 'timeout';
43
+ testId: string;
44
+ data?: any;
45
+ error?: string;
46
+ }
47
+ export interface WorkerJob {
48
+ id: string;
49
+ test: any;
50
+ target: any;
51
+ retries: number;
52
+ workerId?: number;
53
+ }
54
+ /**
55
+ * Parallel Test Runner
56
+ *
57
+ * Executes multiple tests concurrently using worker threads.
58
+ * Each test runs in isolation with its own browser instance.
59
+ */
60
+ export declare class ParallelTestRunner {
61
+ private maxWorkers;
62
+ private timeout;
63
+ private retries;
64
+ private workerPool;
65
+ private activeJobs;
66
+ constructor(config?: {
67
+ maxWorkers?: number;
68
+ timeout?: number;
69
+ retries?: number;
70
+ });
71
+ /**
72
+ * Run tests in parallel
73
+ */
74
+ runParallel(tests: any[], target: any): Promise<ParallelRunResult>;
75
+ /**
76
+ * Run a single worker job
77
+ */
78
+ private runWorker;
79
+ /**
80
+ * Sleep utility
81
+ */
82
+ private sleep;
83
+ /**
84
+ * Clean up all workers
85
+ */
86
+ cleanup(): Promise<void>;
87
+ /**
88
+ * Get optimal worker count based on resources
89
+ */
90
+ static getOptimalWorkerCount(): number;
91
+ }
92
+ /**
93
+ * Create a parallel test runner
94
+ */
95
+ export declare function createParallelRunner(config?: {
96
+ maxWorkers?: number;
97
+ timeout?: number;
98
+ retries?: number;
99
+ }): ParallelTestRunner;
100
+ /**
101
+ * Run tests in parallel (convenience function)
102
+ */
103
+ export declare function runParallelTests(tests: any[], target: any, config?: {
104
+ maxWorkers?: number;
105
+ timeout?: number;
106
+ retries?: number;
107
+ }): Promise<ParallelRunResult>;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * QA360 Parallel Test Runner
3
+ *
4
+ * Runs multiple UI tests concurrently for faster execution.
5
+ * Supports worker pools, load balancing, and resource limits.
6
+ */
7
+ import { cpus } from 'os';
8
+ /**
9
+ * Parallel Test Runner
10
+ *
11
+ * Executes multiple tests concurrently using worker threads.
12
+ * Each test runs in isolation with its own browser instance.
13
+ */
14
+ export class ParallelTestRunner {
15
+ maxWorkers;
16
+ timeout;
17
+ retries;
18
+ workerPool;
19
+ activeJobs;
20
+ constructor(config) {
21
+ this.maxWorkers = config?.maxWorkers || Math.max(2, cpus().length - 1);
22
+ this.timeout = config?.timeout || 60000; // 60s default per test
23
+ this.retries = config?.retries || 1;
24
+ this.workerPool = new Map();
25
+ this.activeJobs = new Map();
26
+ }
27
+ /**
28
+ * Run tests in parallel
29
+ */
30
+ async runParallel(tests, target) {
31
+ const startTime = Date.now();
32
+ console.log(`\nšŸš€ Parallel Test Runner`);
33
+ console.log(` Workers: ${this.maxWorkers}`);
34
+ console.log(` Tests: ${tests.length}`);
35
+ const results = [];
36
+ const jobs = tests.map((test, i) => ({
37
+ id: `test-${i}`,
38
+ test,
39
+ target,
40
+ retries: this.retries,
41
+ }));
42
+ // Process jobs in batches
43
+ let completed = 0;
44
+ let workerId = 0;
45
+ while (jobs.length > 0 || this.activeJobs.size > 0) {
46
+ // Start new jobs up to max workers
47
+ while (this.activeJobs.size < this.maxWorkers && jobs.length > 0) {
48
+ const job = jobs.shift();
49
+ job.workerId = workerId;
50
+ this.activeJobs.set(job.id, job);
51
+ this.runWorker(workerId, job)
52
+ .then((result) => {
53
+ results.push(result);
54
+ completed++;
55
+ const progress = Math.round((completed / tests.length) * 100);
56
+ console.log(` [${completed}/${tests.length}] ${progress}% - ${result.test.name || job.id}: ${result.success ? 'PASS' : 'FAIL'}`);
57
+ })
58
+ .catch((error) => {
59
+ results.push({
60
+ test: job.test,
61
+ success: false,
62
+ duration: 0,
63
+ error: error.message,
64
+ workerId,
65
+ });
66
+ completed++;
67
+ })
68
+ .finally(() => {
69
+ this.activeJobs.delete(job.id);
70
+ });
71
+ workerId = (workerId + 1) % this.maxWorkers;
72
+ }
73
+ // Wait a bit before checking again
74
+ await this.sleep(100);
75
+ }
76
+ const duration = Date.now() - startTime;
77
+ const passed = results.filter((r) => r.success).length;
78
+ const failed = results.filter((r) => !r.success).length;
79
+ const workerStats = {
80
+ totalWorkers: this.maxWorkers,
81
+ totalJobs: results.length,
82
+ avgJobDuration: results.length > 0 ? duration / results.length : 0,
83
+ };
84
+ console.log(`\nāœ… Parallel execution complete`);
85
+ console.log(` Duration: ${duration}ms`);
86
+ console.log(` Throughput: ${Math.round(results.length / (duration / 1000))} tests/sec`);
87
+ return {
88
+ success: failed === 0,
89
+ totalTests: results.length,
90
+ passed,
91
+ failed,
92
+ skipped: 0,
93
+ duration,
94
+ results,
95
+ workerStats,
96
+ };
97
+ }
98
+ /**
99
+ * Run a single worker job
100
+ */
101
+ async runWorker(workerId, job) {
102
+ // For now, run in-process (worker threads setup is complex)
103
+ // In production, this would spawn a worker thread
104
+ const startTime = Date.now();
105
+ try {
106
+ // Import adapter dynamically to run test
107
+ const { PlaywrightNativeAdapter } = await import('../adapters/playwright-native-adapter.js');
108
+ const adapter = new PlaywrightNativeAdapter({
109
+ target: job.target,
110
+ timeout: this.timeout,
111
+ screenshots: 'only-on-failure',
112
+ video: 'never',
113
+ trace: 'never',
114
+ });
115
+ const result = await adapter.runTest(job.test);
116
+ // Retry on failure
117
+ if (!result.success && job.retries > 0) {
118
+ console.log(` šŸ”„ Retrying ${job.test.name || job.id} (${job.retries} retries left)`);
119
+ job.retries--;
120
+ return this.runWorker(workerId, job);
121
+ }
122
+ return {
123
+ test: job.test,
124
+ success: result.success,
125
+ duration: Date.now() - startTime,
126
+ error: result.error,
127
+ artifacts: result.artifacts,
128
+ workerId,
129
+ };
130
+ }
131
+ catch (error) {
132
+ // Retry on error
133
+ if (job.retries > 0) {
134
+ console.log(` šŸ”„ Retrying ${job.test.name || job.id} after error (${job.retries} retries left)`);
135
+ job.retries--;
136
+ return this.runWorker(workerId, job);
137
+ }
138
+ return {
139
+ test: job.test,
140
+ success: false,
141
+ duration: Date.now() - startTime,
142
+ error: error instanceof Error ? error.message : String(error),
143
+ workerId,
144
+ };
145
+ }
146
+ }
147
+ /**
148
+ * Sleep utility
149
+ */
150
+ sleep(ms) {
151
+ return new Promise((resolve) => setTimeout(resolve, ms));
152
+ }
153
+ /**
154
+ * Clean up all workers
155
+ */
156
+ async cleanup() {
157
+ for (const [id, worker] of this.workerPool) {
158
+ await worker.terminate();
159
+ this.workerPool.delete(id);
160
+ }
161
+ this.activeJobs.clear();
162
+ }
163
+ /**
164
+ * Get optimal worker count based on resources
165
+ */
166
+ static getOptimalWorkerCount() {
167
+ const cpuCount = cpus().length;
168
+ const memGB = require('os').totalmem() / (1024 ** 3);
169
+ // Each worker uses ~500MB RAM
170
+ const memBasedWorkers = Math.floor(memGB / 0.5);
171
+ const cpuBasedWorkers = Math.max(2, cpuCount - 1);
172
+ return Math.min(cpuBasedWorkers, memBasedWorkers);
173
+ }
174
+ }
175
+ /**
176
+ * Create a parallel test runner
177
+ */
178
+ export function createParallelRunner(config) {
179
+ return new ParallelTestRunner(config);
180
+ }
181
+ /**
182
+ * Run tests in parallel (convenience function)
183
+ */
184
+ export async function runParallelTests(tests, target, config) {
185
+ const runner = new ParallelTestRunner(config);
186
+ try {
187
+ return await runner.runParallel(tests, target);
188
+ }
189
+ finally {
190
+ await runner.cleanup();
191
+ }
192
+ }