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.
- package/dist/commands/ai.js +26 -14
- package/dist/commands/ask.d.ts +75 -23
- package/dist/commands/ask.js +413 -265
- package/dist/commands/crawl.d.ts +24 -0
- package/dist/commands/crawl.js +121 -0
- package/dist/commands/history.js +38 -3
- package/dist/commands/init.d.ts +89 -95
- package/dist/commands/init.js +282 -200
- package/dist/commands/run.d.ts +1 -0
- package/dist/core/adapters/playwright-native-adapter.d.ts +121 -0
- package/dist/core/adapters/playwright-native-adapter.js +339 -0
- package/dist/core/adapters/playwright-ui.d.ts +83 -7
- package/dist/core/adapters/playwright-ui.js +525 -59
- package/dist/core/artifacts/index.d.ts +6 -0
- package/dist/core/artifacts/index.js +6 -0
- package/dist/core/artifacts/ui-artifacts.d.ts +133 -0
- package/dist/core/artifacts/ui-artifacts.js +304 -0
- package/dist/core/assertions/engine.d.ts +51 -0
- package/dist/core/assertions/engine.js +530 -0
- package/dist/core/assertions/index.d.ts +11 -0
- package/dist/core/assertions/index.js +11 -0
- package/dist/core/assertions/types.d.ts +121 -0
- package/dist/core/assertions/types.js +37 -0
- package/dist/core/crawler/index.d.ts +57 -0
- package/dist/core/crawler/index.js +281 -0
- package/dist/core/crawler/journey-generator.d.ts +49 -0
- package/dist/core/crawler/journey-generator.js +412 -0
- package/dist/core/crawler/page-analyzer.d.ts +88 -0
- package/dist/core/crawler/page-analyzer.js +709 -0
- package/dist/core/crawler/selector-generator.d.ts +34 -0
- package/dist/core/crawler/selector-generator.js +240 -0
- package/dist/core/crawler/types.d.ts +353 -0
- package/dist/core/crawler/types.js +6 -0
- package/dist/core/generation/crawler-pack-generator.d.ts +44 -0
- package/dist/core/generation/crawler-pack-generator.js +231 -0
- package/dist/core/generation/index.d.ts +2 -0
- package/dist/core/generation/index.js +2 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +13 -0
- package/dist/core/parallel/index.d.ts +6 -0
- package/dist/core/parallel/index.js +6 -0
- package/dist/core/parallel/parallel-runner.d.ts +107 -0
- package/dist/core/parallel/parallel-runner.js +192 -0
- package/dist/core/reporting/html-reporter.d.ts +119 -0
- package/dist/core/reporting/html-reporter.js +737 -0
- package/dist/core/reporting/index.d.ts +6 -0
- package/dist/core/reporting/index.js +6 -0
- package/dist/core/runner/phase3-runner.js +5 -1
- package/dist/core/types/pack-v1.d.ts +90 -0
- package/dist/core/vault/cas.d.ts +5 -1
- package/dist/core/vault/cas.js +6 -0
- package/dist/core/visual/index.d.ts +6 -0
- package/dist/core/visual/index.js +6 -0
- package/dist/core/visual/visual-regression.d.ts +113 -0
- package/dist/core/visual/visual-regression.js +236 -0
- package/dist/index.js +6 -2
- package/examples/README.md +38 -0
- package/examples/accessibility.yml +39 -16
- package/examples/api-basic.yml +19 -14
- package/examples/complete.yml +134 -42
- package/examples/crawler.yml +38 -0
- package/examples/fullstack.yml +66 -31
- package/examples/security.yml +47 -15
- package/examples/ui-advanced.yml +49 -0
- package/examples/ui-basic.yml +16 -12
- 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';
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/core/index.js
CHANGED
|
@@ -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,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
|
+
}
|