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.
- package/dist/commands/scan.d.ts +5 -0
- package/dist/commands/scan.js +155 -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 +38 -0
- package/dist/core/adapters/playwright-ui.js +164 -4
- 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/core/coverage/analyzer.d.ts +101 -0
- package/dist/core/core/coverage/analyzer.js +415 -0
- package/dist/core/core/coverage/collector.d.ts +74 -0
- package/dist/core/core/coverage/collector.js +459 -0
- package/dist/core/core/coverage/config.d.ts +37 -0
- package/dist/core/core/coverage/config.js +156 -0
- package/dist/core/core/coverage/index.d.ts +11 -0
- package/dist/core/core/coverage/index.js +15 -0
- package/dist/core/core/coverage/types.d.ts +267 -0
- package/dist/core/core/coverage/types.js +6 -0
- package/dist/core/core/coverage/vault.d.ts +95 -0
- package/dist/core/core/coverage/vault.js +405 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +9 -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 +29 -4
- 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/generators/index.d.ts +5 -0
- package/dist/generators/index.js +5 -0
- package/dist/generators/json-reporter.d.ts +10 -0
- package/dist/generators/json-reporter.js +12 -0
- package/dist/generators/test-generator.d.ts +18 -0
- package/dist/generators/test-generator.js +78 -0
- package/dist/index.js +3 -0
- package/dist/scanners/dom-scanner.d.ts +52 -0
- package/dist/scanners/dom-scanner.js +296 -0
- package/dist/scanners/index.d.ts +4 -0
- package/dist/scanners/index.js +4 -0
- package/dist/types/scan.d.ts +68 -0
- package/dist/types/scan.js +4 -0
- package/examples/README.md +38 -0
- package/examples/crawler.yml +38 -0
- package/examples/ui-advanced.yml +49 -0
- 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 {};
|