qa360 2.0.13 → 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.
@@ -0,0 +1,304 @@
1
+ /**
2
+ * QA360 UI Artifacts Manager
3
+ *
4
+ * Manages screenshots, videos, traces, and other test artifacts
5
+ * with content-addressable storage (CAS) integration
6
+ */
7
+ import { mkdirSync, existsSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { ContentAddressableStorage } from '../vault/cas.js';
10
+ /**
11
+ * UI Artifacts Manager
12
+ *
13
+ * Handles:
14
+ * - Automatic screenshots (before/after steps, on error)
15
+ * - Video recording
16
+ * - Trace files
17
+ * - Network captures
18
+ * - Console logs
19
+ * - Storage in CAS for deduplication
20
+ */
21
+ export class UIArtifactsManager {
22
+ artifactDir;
23
+ casDir;
24
+ currentRunId;
25
+ currentTestId;
26
+ artifacts = new Map();
27
+ cas;
28
+ constructor(artifactDir = '.qa360/artifacts/ui', casDir = '.qa360/runs/cas') {
29
+ this.artifactDir = artifactDir;
30
+ this.casDir = casDir;
31
+ this.currentRunId = `run-${Date.now()}`;
32
+ this.cas = new ContentAddressableStorage(casDir);
33
+ this.ensureDirectories();
34
+ }
35
+ ensureDirectories() {
36
+ const dirs = [
37
+ this.artifactDir,
38
+ `${this.artifactDir}/screenshots`,
39
+ `${this.artifactDir}/videos`,
40
+ `${this.artifactDir}/traces`,
41
+ `${this.artifactDir}/network`,
42
+ `${this.artifactDir}/console`,
43
+ `${this.artifactDir}/coverage`,
44
+ this.casDir,
45
+ `${this.casDir}/art`,
46
+ ];
47
+ for (const dir of dirs) {
48
+ if (!existsSync(dir)) {
49
+ mkdirSync(dir, { recursive: true });
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Start a new test session
55
+ */
56
+ startTest(testId) {
57
+ this.currentTestId = testId;
58
+ }
59
+ /**
60
+ * End current test session
61
+ */
62
+ endTest() {
63
+ this.currentTestId = undefined;
64
+ }
65
+ /**
66
+ * Take a screenshot and store it
67
+ */
68
+ async takeScreenshot(page, // Playwright Page
69
+ options = {}, metadata) {
70
+ const testId = this.currentTestId || 'unknown';
71
+ const timestamp = new Date().toISOString();
72
+ const filename = `${testId}-${Date.now()}.png`;
73
+ const localPath = join(this.artifactDir, 'screenshots', filename);
74
+ // Take screenshot
75
+ const screenshot = await page.screenshot({
76
+ path: localPath,
77
+ type: options.type || 'png',
78
+ fullPage: options.fullPage || false,
79
+ animations: options.animations || 'disabled',
80
+ });
81
+ // Get image dimensions
82
+ const viewportSize = page.viewportSize();
83
+ const dimensions = await page.evaluate(() => ({
84
+ width: document.documentElement.scrollWidth,
85
+ height: document.documentElement.scrollHeight,
86
+ }));
87
+ // Calculate hash and store in CAS
88
+ const hash = ContentAddressableStorage.hashContent(screenshot);
89
+ const casPath = this.getCasPath(hash);
90
+ // Store in CAS if not exists
91
+ if (!existsSync(casPath)) {
92
+ writeFileSync(casPath, screenshot);
93
+ }
94
+ const artifact = {
95
+ type: 'screenshot',
96
+ format: options.type || 'png',
97
+ casPath,
98
+ localPath,
99
+ hash,
100
+ width: options.fullPage ? dimensions.width : (viewportSize?.width || 1280),
101
+ height: options.fullPage ? dimensions.height : (viewportSize?.height || 720),
102
+ size: screenshot.length,
103
+ metadata: {
104
+ testId,
105
+ timestamp,
106
+ type: 'screenshot',
107
+ ...metadata,
108
+ },
109
+ };
110
+ this.artifacts.set(`${testId}-screenshot-${Date.now()}`, artifact);
111
+ return artifact;
112
+ }
113
+ /**
114
+ * Take screenshot before a step
115
+ */
116
+ async takeBeforeScreenshot(page, stepName, stepIndex) {
117
+ return this.takeScreenshot(page, {}, {
118
+ stepIndex,
119
+ stepName: `${stepName}-before`,
120
+ tags: ['before-step'],
121
+ });
122
+ }
123
+ /**
124
+ * Take screenshot after a step
125
+ */
126
+ async takeAfterScreenshot(page, stepName, stepIndex, success) {
127
+ return this.takeScreenshot(page, {}, {
128
+ stepIndex,
129
+ stepName: `${stepName}-after`,
130
+ status: success ? 'passed' : 'failed',
131
+ tags: ['after-step', success ? 'success' : 'failure'],
132
+ });
133
+ }
134
+ /**
135
+ * Take screenshot on error
136
+ */
137
+ async takeErrorScreenshot(page, error, stepName) {
138
+ const filename = `error-${Date.now()}.png`;
139
+ const localPath = join(this.artifactDir, 'screenshots', filename);
140
+ await page.screenshot({
141
+ path: localPath,
142
+ fullPage: true,
143
+ });
144
+ const screenshot = readFileSync(localPath);
145
+ const hash = ContentAddressableStorage.hashContent(screenshot);
146
+ const casPath = this.getCasPath(hash);
147
+ if (!existsSync(casPath)) {
148
+ writeFileSync(casPath, screenshot);
149
+ }
150
+ const artifact = {
151
+ type: 'screenshot',
152
+ format: 'png',
153
+ casPath,
154
+ localPath,
155
+ hash,
156
+ width: 0,
157
+ height: 0,
158
+ size: screenshot.length,
159
+ metadata: {
160
+ testId: this.currentTestId || 'unknown',
161
+ timestamp: new Date().toISOString(),
162
+ type: 'screenshot',
163
+ status: 'failed',
164
+ stepName: stepName || 'error',
165
+ tags: ['error', 'failure-screenshot'],
166
+ },
167
+ };
168
+ this.artifacts.set(`error-${Date.now()}`, artifact);
169
+ return artifact;
170
+ }
171
+ /**
172
+ * Save video artifact
173
+ */
174
+ async saveVideo(videoPath, metadata) {
175
+ const testId = this.currentTestId || 'unknown';
176
+ const filename = `${testId}-${Date.now()}.webm`;
177
+ const localPath = join(this.artifactDir, 'videos', filename);
178
+ // Read video file
179
+ const video = readFileSync(videoPath);
180
+ const hash = ContentAddressableStorage.hashContent(video);
181
+ const casPath = this.getCasPath(hash);
182
+ // Store in CAS
183
+ if (!existsSync(casPath)) {
184
+ writeFileSync(casPath, video);
185
+ }
186
+ // Get video duration (basic estimate)
187
+ const size = video.length;
188
+ const duration = Math.round(size / 100000); // Rough estimate: 100KB per second
189
+ const artifact = {
190
+ type: 'video',
191
+ format: 'webm',
192
+ casPath,
193
+ localPath,
194
+ hash,
195
+ duration,
196
+ size,
197
+ metadata: {
198
+ testId,
199
+ timestamp: new Date().toISOString(),
200
+ type: 'video',
201
+ ...metadata,
202
+ },
203
+ };
204
+ this.artifacts.set(`${testId}-video-${Date.now()}`, artifact);
205
+ return artifact;
206
+ }
207
+ /**
208
+ * Save trace artifact
209
+ */
210
+ async saveTrace(tracePath, metadata) {
211
+ const testId = this.currentTestId || 'unknown';
212
+ const filename = `${testId}-${Date.now()}.zip`;
213
+ const localPath = join(this.artifactDir, 'traces', filename);
214
+ const trace = readFileSync(tracePath);
215
+ const hash = ContentAddressableStorage.hashContent(trace);
216
+ const casPath = this.getCasPath(hash);
217
+ if (!existsSync(casPath)) {
218
+ writeFileSync(casPath, trace);
219
+ }
220
+ const artifact = {
221
+ type: 'trace',
222
+ format: 'zip',
223
+ casPath,
224
+ localPath,
225
+ hash,
226
+ size: trace.length,
227
+ metadata: {
228
+ testId,
229
+ timestamp: new Date().toISOString(),
230
+ type: 'trace',
231
+ ...metadata,
232
+ },
233
+ };
234
+ this.artifacts.set(`${testId}-trace-${Date.now()}`, artifact);
235
+ return artifact;
236
+ }
237
+ /**
238
+ * Get artifact by ID
239
+ */
240
+ getArtifact(id) {
241
+ return this.artifacts.get(id);
242
+ }
243
+ /**
244
+ * Get all artifacts for current test
245
+ */
246
+ getTestArtifacts() {
247
+ const testId = this.currentTestId || 'unknown';
248
+ return Array.from(this.artifacts.values()).filter(a => a.metadata.testId === testId);
249
+ }
250
+ /**
251
+ * Get all artifacts by type
252
+ */
253
+ getArtifactsByType(type) {
254
+ return Array.from(this.artifacts.values()).filter(a => a.type === type);
255
+ }
256
+ /**
257
+ * Clean up old artifacts
258
+ */
259
+ cleanup(maxAge = 24 * 60 * 60 * 1000) {
260
+ const now = Date.now();
261
+ for (const [id, artifact] of this.artifacts.entries()) {
262
+ const artifactTime = new Date(artifact.metadata.timestamp).getTime();
263
+ if (now - artifactTime > maxAge) {
264
+ // Remove local file (keep in CAS)
265
+ if (existsSync(artifact.localPath)) {
266
+ unlinkSync(artifact.localPath);
267
+ }
268
+ this.artifacts.delete(id);
269
+ }
270
+ }
271
+ }
272
+ /**
273
+ * Generate CAS path from hash
274
+ */
275
+ getCasPath(hash) {
276
+ // hash is already a hex string from SHA256
277
+ return join(this.casDir, 'art', hash.substring(0, 2), hash.substring(2, 4), hash.substring(4));
278
+ }
279
+ /**
280
+ * Generate artifacts summary for reporting
281
+ */
282
+ generateSummary() {
283
+ const screenshots = this.getArtifactsByType('screenshot').length;
284
+ const videos = this.getArtifactsByType('video').length;
285
+ const traces = this.getArtifactsByType('trace').length;
286
+ const totalSize = Array.from(this.artifacts.values()).reduce((sum, a) => sum + a.size, 0);
287
+ // Calculate CAS savings (unique hashes)
288
+ const uniqueHashes = new Set(Array.from(this.artifacts.values()).map(a => a.hash));
289
+ const casDedupSavings = totalSize - (uniqueHashes.size * 1000); // Rough estimate
290
+ return {
291
+ screenshots,
292
+ videos,
293
+ traces,
294
+ totalSize,
295
+ casDedupSavings,
296
+ };
297
+ }
298
+ }
299
+ /**
300
+ * Create a UI artifacts manager
301
+ */
302
+ export function createUIArtifactsManager(artifactDir, casDir) {
303
+ return new UIArtifactsManager(artifactDir, casDir);
304
+ }
@@ -96,3 +96,9 @@ export * from './regression/index.js';
96
96
  export * from './crawler/index.js';
97
97
  export { AssertionsEngine, createAssertionsEngine, AssertionResult, AssertionGroupResult, AssertionRunOptions, AssertionError, SoftAssertionError, Assertion, AssertionOperator, AssertionSuite, } from './assertions/index.js';
98
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';
@@ -80,3 +80,12 @@ export * from './regression/index.js';
80
80
  export * from './crawler/index.js';
81
81
  // Assertions Engine (Vision 2.0 - UI Testing 100%)
82
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
+ }