qa360 2.0.13 → 2.1.1

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 (56) hide show
  1. package/dist/commands/scan.d.ts +5 -0
  2. package/dist/commands/scan.js +155 -0
  3. package/dist/core/adapters/playwright-native-adapter.d.ts +121 -0
  4. package/dist/core/adapters/playwright-native-adapter.js +339 -0
  5. package/dist/core/adapters/playwright-ui.d.ts +38 -0
  6. package/dist/core/adapters/playwright-ui.js +164 -4
  7. package/dist/core/artifacts/index.d.ts +6 -0
  8. package/dist/core/artifacts/index.js +6 -0
  9. package/dist/core/artifacts/ui-artifacts.d.ts +133 -0
  10. package/dist/core/artifacts/ui-artifacts.js +304 -0
  11. package/dist/core/core/coverage/analyzer.d.ts +101 -0
  12. package/dist/core/core/coverage/analyzer.js +415 -0
  13. package/dist/core/core/coverage/collector.d.ts +74 -0
  14. package/dist/core/core/coverage/collector.js +459 -0
  15. package/dist/core/core/coverage/config.d.ts +37 -0
  16. package/dist/core/core/coverage/config.js +156 -0
  17. package/dist/core/core/coverage/index.d.ts +11 -0
  18. package/dist/core/core/coverage/index.js +15 -0
  19. package/dist/core/core/coverage/types.d.ts +267 -0
  20. package/dist/core/core/coverage/types.js +6 -0
  21. package/dist/core/core/coverage/vault.d.ts +95 -0
  22. package/dist/core/core/coverage/vault.js +405 -0
  23. package/dist/core/index.d.ts +6 -0
  24. package/dist/core/index.js +9 -0
  25. package/dist/core/parallel/index.d.ts +6 -0
  26. package/dist/core/parallel/index.js +6 -0
  27. package/dist/core/parallel/parallel-runner.d.ts +107 -0
  28. package/dist/core/parallel/parallel-runner.js +192 -0
  29. package/dist/core/reporting/html-reporter.d.ts +119 -0
  30. package/dist/core/reporting/html-reporter.js +737 -0
  31. package/dist/core/reporting/index.d.ts +6 -0
  32. package/dist/core/reporting/index.js +6 -0
  33. package/dist/core/runner/phase3-runner.js +29 -4
  34. package/dist/core/vault/cas.d.ts +5 -1
  35. package/dist/core/vault/cas.js +6 -0
  36. package/dist/core/visual/index.d.ts +6 -0
  37. package/dist/core/visual/index.js +6 -0
  38. package/dist/core/visual/visual-regression.d.ts +113 -0
  39. package/dist/core/visual/visual-regression.js +236 -0
  40. package/dist/generators/index.d.ts +5 -0
  41. package/dist/generators/index.js +5 -0
  42. package/dist/generators/json-reporter.d.ts +10 -0
  43. package/dist/generators/json-reporter.js +12 -0
  44. package/dist/generators/test-generator.d.ts +18 -0
  45. package/dist/generators/test-generator.js +78 -0
  46. package/dist/index.js +3 -0
  47. package/dist/scanners/dom-scanner.d.ts +52 -0
  48. package/dist/scanners/dom-scanner.js +296 -0
  49. package/dist/scanners/index.d.ts +4 -0
  50. package/dist/scanners/index.js +4 -0
  51. package/dist/types/scan.d.ts +68 -0
  52. package/dist/types/scan.js +4 -0
  53. package/examples/README.md +38 -0
  54. package/examples/crawler.yml +38 -0
  55. package/examples/ui-advanced.yml +49 -0
  56. package/package.json +2 -2
@@ -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
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Coverage Analyzer
3
+ *
4
+ * Analyzes coverage data to provide insights, trends, and recommendations.
5
+ */
6
+ import type { FileCoverage, CoverageMetrics, CoverageResult, CoverageTrend, CoverageGap, CoverageComparison, CoverageThreshold, CoverageType, CoverageReport } from './types.js';
7
+ /**
8
+ * Historical coverage data point
9
+ */
10
+ interface HistoricalCoverage {
11
+ runId: string;
12
+ timestamp: number;
13
+ metrics: CoverageMetrics;
14
+ }
15
+ /**
16
+ * Coverage Analyzer class
17
+ */
18
+ export declare class CoverageAnalyzer {
19
+ private history;
20
+ /**
21
+ * Analyze coverage and generate insights
22
+ */
23
+ analyze(result: CoverageResult, threshold?: CoverageThreshold): CoverageReport;
24
+ /**
25
+ * Check if coverage meets thresholds
26
+ */
27
+ checkThresholds(metrics: CoverageMetrics, threshold?: CoverageThreshold): boolean;
28
+ /**
29
+ * Check if a single file meets thresholds
30
+ */
31
+ checkFileThresholds(file: FileCoverage, threshold?: CoverageThreshold): boolean;
32
+ /**
33
+ * Find coverage gaps
34
+ */
35
+ findGaps(files: Record<string, FileCoverage>, threshold?: CoverageThreshold): CoverageGap[];
36
+ /**
37
+ * Calculate priority for covering a file
38
+ */
39
+ private calculatePriority;
40
+ /**
41
+ * Estimate effort to cover a file
42
+ */
43
+ private estimateEffort;
44
+ /**
45
+ * Generate test suggestions for a file
46
+ */
47
+ private generateSuggestions;
48
+ /**
49
+ * Group consecutive numbers into ranges
50
+ */
51
+ private groupConsecutiveNumbers;
52
+ /**
53
+ * Get top and bottom files by coverage
54
+ */
55
+ getTopFiles(files: Record<string, FileCoverage>, limit?: number): Array<{
56
+ path: string;
57
+ coverage: number;
58
+ type: 'best' | 'worst';
59
+ }>;
60
+ /**
61
+ * Compare two coverage results
62
+ */
63
+ compare(baseResult: CoverageResult, compareResult: CoverageResult): CoverageComparison;
64
+ /**
65
+ * Add historical coverage data
66
+ */
67
+ addHistory(key: string, data: HistoricalCoverage): void;
68
+ /**
69
+ * Get coverage trends
70
+ */
71
+ getTrends(key: string, type?: CoverageType, limit?: number): CoverageTrend[];
72
+ /**
73
+ * Calculate trend direction
74
+ */
75
+ getTrendDirection(trends: CoverageTrend[]): 'improving' | 'stable' | 'declining';
76
+ /**
77
+ * Predict future coverage based on trends
78
+ */
79
+ predictCoverage(key: string, type: CoverageType | undefined, targetCoverage: number): {
80
+ predictedReach: number | null;
81
+ projectedCoverage: number;
82
+ confidence: 'high' | 'medium' | 'low';
83
+ };
84
+ /**
85
+ * Generate coverage summary text
86
+ */
87
+ generateSummary(metrics: CoverageMetrics): string;
88
+ /**
89
+ * Format coverage percentage with color indicator
90
+ */
91
+ formatCoverage(percentage: number, threshold?: number): string;
92
+ /**
93
+ * Clear history
94
+ */
95
+ clearHistory(key?: string): void;
96
+ }
97
+ /**
98
+ * Create a coverage analyzer
99
+ */
100
+ export declare function createCoverageAnalyzer(): CoverageAnalyzer;
101
+ export {};