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,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Reporting Module
|
|
3
|
+
*
|
|
4
|
+
* Generates test reports in various formats (HTML, JSON, JUnit)
|
|
5
|
+
*/
|
|
6
|
+
export { HTMLReporter, generateHTMLReport, type ReportData, type TestReport, type StepReport, type ReportScreenshotArtifact as ScreenshotArtifact, type ReportVideoArtifact as VideoArtifact, type ReportTraceArtifact as TraceArtifact, } from './html-reporter.js';
|
|
@@ -508,7 +508,11 @@ export class Phase3Runner {
|
|
|
508
508
|
},
|
|
509
509
|
budgets: gateConfigData.budgets,
|
|
510
510
|
timeout: gateConfigData.timeout,
|
|
511
|
-
auth: credentials
|
|
511
|
+
auth: credentials,
|
|
512
|
+
// Playwright++ features
|
|
513
|
+
artifacts: gateConfigData.artifacts,
|
|
514
|
+
htmlReport: gateConfigData.htmlReport,
|
|
515
|
+
bail: gateConfigData.bail
|
|
512
516
|
};
|
|
513
517
|
const result = await adapter.runSmokeTests(config);
|
|
514
518
|
return {
|
|
@@ -13,6 +13,96 @@ export interface ApiTarget {
|
|
|
13
13
|
export interface WebTarget {
|
|
14
14
|
baseUrl: string;
|
|
15
15
|
pages?: string[];
|
|
16
|
+
/** Browser mode: true for headed (visible), false for headless (default) */
|
|
17
|
+
headless?: boolean;
|
|
18
|
+
/** Slow motion delay in ms between actions (for debugging) */
|
|
19
|
+
slowMo?: number;
|
|
20
|
+
/** Viewport size */
|
|
21
|
+
viewport?: {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
};
|
|
25
|
+
/** Screenshot mode: 'always', 'never', 'only-on-fail' (default) */
|
|
26
|
+
screenshot?: 'always' | 'never' | 'only-on-fail';
|
|
27
|
+
/** Video recording mode: 'always', 'never' (default), 'retain-on-fail' */
|
|
28
|
+
video?: 'always' | 'never' | 'retain-on-fail';
|
|
29
|
+
/** Trace mode: 'always', 'never' (default), 'retain-on-fail' */
|
|
30
|
+
trace?: 'always' | 'never' | 'retain-on-fail';
|
|
31
|
+
/** Device emulation */
|
|
32
|
+
device?: 'desktop' | 'mobile' | 'tablet';
|
|
33
|
+
/** Browser to use: 'chromium' (default), 'firefox', 'webkit' */
|
|
34
|
+
browser?: 'chromium' | 'firefox' | 'webkit';
|
|
35
|
+
/** UI test definitions */
|
|
36
|
+
uiTests?: UiTestDefinition[];
|
|
37
|
+
}
|
|
38
|
+
/** UI Test Step Action */
|
|
39
|
+
export type UiAction = 'navigate' | 'click' | 'dblClick' | 'rightClick' | 'hover' | 'focus' | 'fill' | 'type' | 'clear' | 'select' | 'check' | 'uncheck' | 'upload' | 'press' | 'waitFor' | 'waitForSelector' | 'waitForNavigation' | 'waitForTimeout' | 'scroll' | 'dragAndDrop' | 'tap';
|
|
40
|
+
/** UI Test Step */
|
|
41
|
+
export interface UiTestStep {
|
|
42
|
+
/** Step order */
|
|
43
|
+
order?: number;
|
|
44
|
+
/** Action to perform */
|
|
45
|
+
action: UiAction;
|
|
46
|
+
/** CSS selector for element */
|
|
47
|
+
selector?: string;
|
|
48
|
+
/** Value to input */
|
|
49
|
+
value?: string;
|
|
50
|
+
/** Options for the action */
|
|
51
|
+
options?: Record<string, any>;
|
|
52
|
+
/** Expected outcome after this step */
|
|
53
|
+
expected?: {
|
|
54
|
+
/** Expected URL */
|
|
55
|
+
url?: string;
|
|
56
|
+
/** URL contains string */
|
|
57
|
+
urlContains?: string;
|
|
58
|
+
/** Expected visible element */
|
|
59
|
+
visible?: string;
|
|
60
|
+
/** Expected hidden element */
|
|
61
|
+
hidden?: string;
|
|
62
|
+
/** Expected text content */
|
|
63
|
+
text?: string;
|
|
64
|
+
/** Expected element text content */
|
|
65
|
+
elementText?: {
|
|
66
|
+
selector: string;
|
|
67
|
+
text: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/** Wait time in ms after action */
|
|
71
|
+
wait?: number;
|
|
72
|
+
/** Description of what this step does */
|
|
73
|
+
description?: string;
|
|
74
|
+
}
|
|
75
|
+
/** UI Test Definition */
|
|
76
|
+
export interface UiTestDefinition {
|
|
77
|
+
/** Test name */
|
|
78
|
+
name: string;
|
|
79
|
+
/** Starting path (relative to baseUrl) */
|
|
80
|
+
path?: string;
|
|
81
|
+
/** Full URL (overrides baseUrl + path) */
|
|
82
|
+
url?: string;
|
|
83
|
+
/** Test steps */
|
|
84
|
+
steps: UiTestStep[];
|
|
85
|
+
/** Description */
|
|
86
|
+
description?: string;
|
|
87
|
+
/** Whether this test is enabled */
|
|
88
|
+
enabled?: boolean;
|
|
89
|
+
/** Test timeout in ms */
|
|
90
|
+
timeout?: number;
|
|
91
|
+
/** Tags for organizing tests */
|
|
92
|
+
tags?: string[];
|
|
93
|
+
}
|
|
94
|
+
/** Assertion for UI tests */
|
|
95
|
+
export interface UiAssertion {
|
|
96
|
+
/** Assertion type */
|
|
97
|
+
type: 'visible' | 'hidden' | 'text' | 'contains' | 'value' | 'attribute' | 'class' | 'count' | 'url' | 'title' | 'enabled' | 'disabled' | 'checked' | 'focused';
|
|
98
|
+
/** CSS selector */
|
|
99
|
+
selector?: string;
|
|
100
|
+
/** Expected value */
|
|
101
|
+
expected?: any;
|
|
102
|
+
/** Whether to negate */
|
|
103
|
+
not?: boolean;
|
|
104
|
+
/** Timeout in ms */
|
|
105
|
+
timeout?: number;
|
|
16
106
|
}
|
|
17
107
|
export interface PackTargets {
|
|
18
108
|
api?: ApiTarget;
|
package/dist/core/vault/cas.d.ts
CHANGED
|
@@ -58,7 +58,11 @@ export declare class ContentAddressableStorage {
|
|
|
58
58
|
/**
|
|
59
59
|
* Calculate SHA256 hash of content
|
|
60
60
|
*/
|
|
61
|
-
|
|
61
|
+
calculateHash(content: Buffer): string;
|
|
62
|
+
/**
|
|
63
|
+
* Calculate SHA256 hash of content (static utility)
|
|
64
|
+
*/
|
|
65
|
+
static hashContent(content: Buffer): string;
|
|
62
66
|
/**
|
|
63
67
|
* Get CAS file path for a given hash (with sharding)
|
|
64
68
|
*/
|
package/dist/core/vault/cas.js
CHANGED
|
@@ -165,6 +165,12 @@ export class ContentAddressableStorage {
|
|
|
165
165
|
calculateHash(content) {
|
|
166
166
|
return createHash('sha256').update(content).digest('hex');
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Calculate SHA256 hash of content (static utility)
|
|
170
|
+
*/
|
|
171
|
+
static hashContent(content) {
|
|
172
|
+
return createHash('sha256').update(content).digest('hex');
|
|
173
|
+
}
|
|
168
174
|
/**
|
|
169
175
|
* Get CAS file path for a given hash (with sharding)
|
|
170
176
|
*/
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Visual Regression Module
|
|
3
|
+
*
|
|
4
|
+
* Visual comparison testing for UI changes
|
|
5
|
+
*/
|
|
6
|
+
export { VisualRegressionTester, createVisualRegressionTester, runVisualRegressionTests, type VisualRegressionConfig, type VisualRegressionResult, type ScreenshotData, } from './visual-regression.js';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Visual Regression Testing
|
|
3
|
+
*
|
|
4
|
+
* Compares screenshots against baselines to detect visual changes.
|
|
5
|
+
* Uses pixel diffing with configurable thresholds.
|
|
6
|
+
*/
|
|
7
|
+
export interface VisualRegressionConfig {
|
|
8
|
+
/** Directory to store baseline images */
|
|
9
|
+
baselineDir?: string;
|
|
10
|
+
/** Directory to store actual images */
|
|
11
|
+
actualDir?: string;
|
|
12
|
+
/** Directory to store diff images */
|
|
13
|
+
diffDir?: string;
|
|
14
|
+
/** Maximum allowed pixel difference (0-1) */
|
|
15
|
+
maxDiffThreshold?: number;
|
|
16
|
+
/** Maximum allowed different pixels (0-1) */
|
|
17
|
+
maxDifferentPixels?: number;
|
|
18
|
+
/** Whether to update baselines */
|
|
19
|
+
updateBaselines?: boolean;
|
|
20
|
+
/** Whether to ignore anti-aliasing differences */
|
|
21
|
+
ignoreAntiAliasing?: boolean;
|
|
22
|
+
/** Color to highlight differences */
|
|
23
|
+
diffColor?: {
|
|
24
|
+
r: number;
|
|
25
|
+
g: number;
|
|
26
|
+
b: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface VisualRegressionResult {
|
|
30
|
+
name: string;
|
|
31
|
+
status: 'pass' | 'fail' | 'updated';
|
|
32
|
+
diffPixels: number;
|
|
33
|
+
diffPercentage: number;
|
|
34
|
+
diffImagePath?: string;
|
|
35
|
+
baselinePath: string;
|
|
36
|
+
actualPath: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ScreenshotData {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
data: Buffer;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Visual Regression Tester
|
|
46
|
+
*
|
|
47
|
+
* Captures screenshots and compares them against baseline images.
|
|
48
|
+
*/
|
|
49
|
+
export declare class VisualRegressionTester {
|
|
50
|
+
private baselineDir;
|
|
51
|
+
private actualDir;
|
|
52
|
+
private diffDir;
|
|
53
|
+
private maxDiffThreshold;
|
|
54
|
+
private maxDifferentPixels;
|
|
55
|
+
private updateBaselines;
|
|
56
|
+
private ignoreAntiAliasing;
|
|
57
|
+
private diffColor;
|
|
58
|
+
constructor(config?: VisualRegressionConfig);
|
|
59
|
+
private ensureDirectories;
|
|
60
|
+
/**
|
|
61
|
+
* Compare a screenshot against baseline
|
|
62
|
+
*/
|
|
63
|
+
compare(name: string, screenshot: Buffer, width: number, height: number): Promise<VisualRegressionResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Compare two image buffers
|
|
66
|
+
*/
|
|
67
|
+
private compareBuffers;
|
|
68
|
+
/**
|
|
69
|
+
* Generate a diff image highlighting differences
|
|
70
|
+
*/
|
|
71
|
+
private generateDiffImage;
|
|
72
|
+
/**
|
|
73
|
+
* Get baseline file path for a test
|
|
74
|
+
*/
|
|
75
|
+
private getBaselinePath;
|
|
76
|
+
/**
|
|
77
|
+
* Get actual file path for a test
|
|
78
|
+
*/
|
|
79
|
+
private getActualPath;
|
|
80
|
+
/**
|
|
81
|
+
* Get diff file path for a test
|
|
82
|
+
*/
|
|
83
|
+
private getDiffPath;
|
|
84
|
+
/**
|
|
85
|
+
* Get hash of a screenshot for deduplication
|
|
86
|
+
*/
|
|
87
|
+
getScreenshotHash(screenshot: Buffer): string;
|
|
88
|
+
/**
|
|
89
|
+
* Clean up old actual screenshots
|
|
90
|
+
*/
|
|
91
|
+
cleanup(maxAge?: number): void;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a visual regression tester
|
|
95
|
+
*/
|
|
96
|
+
export declare function createVisualRegressionTester(config?: VisualRegressionConfig): VisualRegressionTester;
|
|
97
|
+
/**
|
|
98
|
+
* Run visual regression tests
|
|
99
|
+
*/
|
|
100
|
+
export declare function runVisualRegressionTests(tests: Array<{
|
|
101
|
+
name: string;
|
|
102
|
+
screenshot: Buffer;
|
|
103
|
+
width: number;
|
|
104
|
+
height: number;
|
|
105
|
+
}>, config?: VisualRegressionConfig): Promise<{
|
|
106
|
+
results: VisualRegressionResult[];
|
|
107
|
+
summary: {
|
|
108
|
+
total: number;
|
|
109
|
+
passed: number;
|
|
110
|
+
failed: number;
|
|
111
|
+
updated: number;
|
|
112
|
+
};
|
|
113
|
+
}>;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Visual Regression Testing
|
|
3
|
+
*
|
|
4
|
+
* Compares screenshots against baselines to detect visual changes.
|
|
5
|
+
* Uses pixel diffing with configurable thresholds.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
/**
|
|
11
|
+
* Visual Regression Tester
|
|
12
|
+
*
|
|
13
|
+
* Captures screenshots and compares them against baseline images.
|
|
14
|
+
*/
|
|
15
|
+
export class VisualRegressionTester {
|
|
16
|
+
baselineDir;
|
|
17
|
+
actualDir;
|
|
18
|
+
diffDir;
|
|
19
|
+
maxDiffThreshold;
|
|
20
|
+
maxDifferentPixels;
|
|
21
|
+
updateBaselines;
|
|
22
|
+
ignoreAntiAliasing;
|
|
23
|
+
diffColor;
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.baselineDir = config.baselineDir || '.qa360/visual/baselines';
|
|
26
|
+
this.actualDir = config.actualDir || '.qa360/visual/actual';
|
|
27
|
+
this.diffDir = config.diffDir || '.qa360/visual/diffs';
|
|
28
|
+
this.maxDiffThreshold = config.maxDiffThreshold ?? 0.01; // 1% max diff per pixel
|
|
29
|
+
this.maxDifferentPixels = config.maxDifferentPixels ?? 0.001; // 0.1% of pixels can differ
|
|
30
|
+
this.updateBaselines = config.updateBaselines ?? false;
|
|
31
|
+
this.ignoreAntiAliasing = config.ignoreAntiAliasing ?? true;
|
|
32
|
+
this.diffColor = config.diffColor || { r: 255, g: 0, b: 0 }; // Red
|
|
33
|
+
this.ensureDirectories();
|
|
34
|
+
}
|
|
35
|
+
ensureDirectories() {
|
|
36
|
+
for (const dir of [this.baselineDir, this.actualDir, this.diffDir]) {
|
|
37
|
+
if (!existsSync(dir)) {
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Compare a screenshot against baseline
|
|
44
|
+
*/
|
|
45
|
+
async compare(name, screenshot, width, height) {
|
|
46
|
+
const baselinePath = this.getBaselinePath(name);
|
|
47
|
+
const actualPath = this.getActualPath(name);
|
|
48
|
+
// Save actual screenshot
|
|
49
|
+
writeFileSync(actualPath, screenshot);
|
|
50
|
+
// Check if baseline exists
|
|
51
|
+
if (!existsSync(baselinePath)) {
|
|
52
|
+
if (this.updateBaselines) {
|
|
53
|
+
writeFileSync(baselinePath, screenshot);
|
|
54
|
+
return {
|
|
55
|
+
name,
|
|
56
|
+
status: 'updated',
|
|
57
|
+
diffPixels: 0,
|
|
58
|
+
diffPercentage: 0,
|
|
59
|
+
baselinePath,
|
|
60
|
+
actualPath,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
status: 'fail',
|
|
66
|
+
diffPixels: -1,
|
|
67
|
+
diffPercentage: 100,
|
|
68
|
+
baselinePath,
|
|
69
|
+
actualPath,
|
|
70
|
+
error: 'No baseline found. Run with --update-baselines to create one.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Load baseline
|
|
74
|
+
const baseline = readFileSync(baselinePath);
|
|
75
|
+
// Compare images
|
|
76
|
+
const diff = this.compareBuffers(baseline, screenshot, width, height);
|
|
77
|
+
// Determine if test passes
|
|
78
|
+
const pixelDiffRatio = diff.diffPixels / (width * height);
|
|
79
|
+
const passes = pixelDiffRatio <= this.maxDifferentPixels &&
|
|
80
|
+
diff.maxDiffValue <= this.maxDiffThreshold;
|
|
81
|
+
// Generate diff image if failed
|
|
82
|
+
let diffImagePath;
|
|
83
|
+
if (!passes || this.updateBaselines) {
|
|
84
|
+
diffImagePath = this.getDiffPath(name);
|
|
85
|
+
this.generateDiffImage(baseline, screenshot, width, height, diff.diffBuffer, diffImagePath);
|
|
86
|
+
}
|
|
87
|
+
// Update baseline if requested
|
|
88
|
+
if (this.updateBaselines && !passes) {
|
|
89
|
+
writeFileSync(baselinePath, screenshot);
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
status: 'updated',
|
|
93
|
+
diffPixels: diff.diffPixels,
|
|
94
|
+
diffPercentage: (diff.diffPixels / (width * height)) * 100,
|
|
95
|
+
diffImagePath,
|
|
96
|
+
baselinePath,
|
|
97
|
+
actualPath,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
name,
|
|
102
|
+
status: passes ? 'pass' : 'fail',
|
|
103
|
+
diffPixels: diff.diffPixels,
|
|
104
|
+
diffPercentage: (diff.diffPixels / (width * height)) * 100,
|
|
105
|
+
diffImagePath,
|
|
106
|
+
baselinePath,
|
|
107
|
+
actualPath,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Compare two image buffers
|
|
112
|
+
*/
|
|
113
|
+
compareBuffers(baseline, actual, width, height) {
|
|
114
|
+
// Assume PNG format (simplified - would use PNG decoder in production)
|
|
115
|
+
// For now, just compare raw buffer sizes as a simple check
|
|
116
|
+
if (baseline.length !== actual.length) {
|
|
117
|
+
return {
|
|
118
|
+
diffPixels: width * height,
|
|
119
|
+
maxDiffValue: 1,
|
|
120
|
+
diffBuffer: Buffer.alloc(width * height * 4),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const baselineData = new Uint8ClampedArray(baseline);
|
|
124
|
+
const actualData = new Uint8ClampedArray(actual);
|
|
125
|
+
const diffBuffer = new Uint8ClampedArray(baseline.length);
|
|
126
|
+
let diffPixels = 0;
|
|
127
|
+
let maxDiffValue = 0;
|
|
128
|
+
// Compare pixel by pixel (assuming RGBA)
|
|
129
|
+
for (let i = 0; i < baselineData.length; i += 4) {
|
|
130
|
+
const r1 = baselineData[i];
|
|
131
|
+
const g1 = baselineData[i + 1];
|
|
132
|
+
const b1 = baselineData[i + 2];
|
|
133
|
+
const a1 = baselineData[i + 3];
|
|
134
|
+
const r2 = actualData[i];
|
|
135
|
+
const g2 = actualData[i + 1];
|
|
136
|
+
const b2 = actualData[i + 2];
|
|
137
|
+
const a2 = actualData[i + 3];
|
|
138
|
+
// Calculate color difference
|
|
139
|
+
const diffR = Math.abs(r1 - r2);
|
|
140
|
+
const diffG = Math.abs(g1 - g2);
|
|
141
|
+
const diffB = Math.abs(b1 - b2);
|
|
142
|
+
const diffA = Math.abs(a1 - a2);
|
|
143
|
+
// Overall difference (normalized 0-1)
|
|
144
|
+
const diffValue = Math.max(diffR, diffG, diffB, diffA) / 255;
|
|
145
|
+
maxDiffValue = Math.max(maxDiffValue, diffValue);
|
|
146
|
+
// Check if pixel differs significantly
|
|
147
|
+
const isDifferent = diffValue > (this.ignoreAntiAliasing ? 0.05 : 0);
|
|
148
|
+
if (isDifferent) {
|
|
149
|
+
diffPixels++;
|
|
150
|
+
// Store diff color in diff buffer
|
|
151
|
+
diffBuffer[i] = this.diffColor.r;
|
|
152
|
+
diffBuffer[i + 1] = this.diffColor.g;
|
|
153
|
+
diffBuffer[i + 2] = this.diffColor.b;
|
|
154
|
+
diffBuffer[i + 3] = 255;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Store original pixel
|
|
158
|
+
diffBuffer[i] = r2;
|
|
159
|
+
diffBuffer[i + 1] = g2;
|
|
160
|
+
diffBuffer[i + 2] = b2;
|
|
161
|
+
diffBuffer[i + 3] = a2;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
diffPixels,
|
|
166
|
+
maxDiffValue,
|
|
167
|
+
diffBuffer: Buffer.from(diffBuffer),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Generate a diff image highlighting differences
|
|
172
|
+
*/
|
|
173
|
+
generateDiffImage(baseline, actual, width, height, diffBuffer, outputPath) {
|
|
174
|
+
// In production, this would create a proper PNG
|
|
175
|
+
// For now, just save the diff buffer
|
|
176
|
+
writeFileSync(outputPath, diffBuffer);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get baseline file path for a test
|
|
180
|
+
*/
|
|
181
|
+
getBaselinePath(name) {
|
|
182
|
+
const hash = createHash('md5').update(name).digest('hex').substring(0, 8);
|
|
183
|
+
return join(this.baselineDir, `${hash}.png`);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get actual file path for a test
|
|
187
|
+
*/
|
|
188
|
+
getActualPath(name) {
|
|
189
|
+
const hash = createHash('md5').update(name).digest('hex').substring(0, 8);
|
|
190
|
+
return join(this.actualDir, `${hash}.png`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get diff file path for a test
|
|
194
|
+
*/
|
|
195
|
+
getDiffPath(name) {
|
|
196
|
+
const hash = createHash('md5').update(name).digest('hex').substring(0, 8);
|
|
197
|
+
return join(this.diffDir, `${hash}.png`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get hash of a screenshot for deduplication
|
|
201
|
+
*/
|
|
202
|
+
getScreenshotHash(screenshot) {
|
|
203
|
+
return createHash('sha256').update(screenshot).digest('hex');
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Clean up old actual screenshots
|
|
207
|
+
*/
|
|
208
|
+
cleanup(maxAge = 24 * 60 * 60 * 1000) {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
// Implementation would clean up old files
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Create a visual regression tester
|
|
215
|
+
*/
|
|
216
|
+
export function createVisualRegressionTester(config) {
|
|
217
|
+
return new VisualRegressionTester(config);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Run visual regression tests
|
|
221
|
+
*/
|
|
222
|
+
export async function runVisualRegressionTests(tests, config) {
|
|
223
|
+
const tester = new VisualRegressionTester(config);
|
|
224
|
+
const results = [];
|
|
225
|
+
for (const test of tests) {
|
|
226
|
+
const result = await tester.compare(test.name, test.screenshot, test.width, test.height);
|
|
227
|
+
results.push(result);
|
|
228
|
+
}
|
|
229
|
+
const summary = {
|
|
230
|
+
total: results.length,
|
|
231
|
+
passed: results.filter((r) => r.status === 'pass').length,
|
|
232
|
+
failed: results.filter((r) => r.status === 'fail').length,
|
|
233
|
+
updated: results.filter((r) => r.status === 'updated').length,
|
|
234
|
+
};
|
|
235
|
+
return { results, summary };
|
|
236
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -32,9 +32,10 @@ import { coverageCommand } from './commands/coverage.js';
|
|
|
32
32
|
import { sloCommand } from './commands/slo.js';
|
|
33
33
|
import { regressionCommand } from './commands/regression.js';
|
|
34
34
|
import { createRetryCommands } from './commands/retry.js';
|
|
35
|
+
import { createCrawlCommand } from './commands/crawl.js';
|
|
35
36
|
// import { createMonitorCommands } from './commands/monitor.js'; // TODO: Re-enable monitor imports
|
|
36
37
|
// import { createOllamaCommands } from './commands/ollama.js'; // TODO: Re-enable when Ollama exports from core are fixed
|
|
37
|
-
|
|
38
|
+
import { createGenerateCommands } from './commands/generate.js';
|
|
38
39
|
// import { createRepairCommand } from './commands/repair.js'; // TODO: fix repair imports
|
|
39
40
|
const program = new Command();
|
|
40
41
|
program
|
|
@@ -99,6 +100,7 @@ program
|
|
|
99
100
|
.option('--retries <number>', 'Number of retries for failed tests', '2')
|
|
100
101
|
.option('--timeout <ms>', 'Global timeout for tests in milliseconds', '30000')
|
|
101
102
|
.option('--docker', 'Use Docker fallback for missing tools')
|
|
103
|
+
.option('--headed', 'Run UI tests in headed mode (visible browser)')
|
|
102
104
|
.action(async (pack, options) => {
|
|
103
105
|
await runCommand(pack, options);
|
|
104
106
|
});
|
|
@@ -212,7 +214,7 @@ program.addCommand(createFlakinessCommands());
|
|
|
212
214
|
program.addCommand(createAICommands());
|
|
213
215
|
// program.addCommand(createOllamaCommands()); // TODO: Re-enable when Ollama exports from core are fixed
|
|
214
216
|
// Generate AI commands (Phase 4)
|
|
215
|
-
|
|
217
|
+
program.addCommand(createGenerateCommands());
|
|
216
218
|
// Repair Commands (Phase 7 - Auto-Repair)
|
|
217
219
|
// program.addCommand(createRepairCommand()); // TODO: Re-enable after fixing imports
|
|
218
220
|
// Monitor Commands (Phase 8 - TUI & Dashboard)
|
|
@@ -226,6 +228,8 @@ program.addCommand(sloCommand);
|
|
|
226
228
|
program.addCommand(regressionCommand);
|
|
227
229
|
// Retry Commands (Vision 2.0 - Phase 2 - F8 Smart Retry)
|
|
228
230
|
program.addCommand(createRetryCommands());
|
|
231
|
+
// Crawl Command (UI Testing 100% - Auto-generate packs from websites)
|
|
232
|
+
program.addCommand(createCrawlCommand());
|
|
229
233
|
// Show banner
|
|
230
234
|
console.log(chalk.bold.blue('QA360 Core v' + version));
|
|
231
235
|
console.log(chalk.gray('Transform software testing into verifiable, signed, and traceable proofs\n'));
|
package/examples/README.md
CHANGED
|
@@ -45,6 +45,44 @@ qa360 run examples/ui-basic.yml
|
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
+
### 2b. **ui-advanced.yml** - UI Tests with Playwright++
|
|
49
|
+
Advanced UI testing with artifacts, video, screenshots, and HTML reports.
|
|
50
|
+
|
|
51
|
+
**Gates**: `ui`
|
|
52
|
+
|
|
53
|
+
**Features**:
|
|
54
|
+
- Automatic screenshots on failure
|
|
55
|
+
- Video recording
|
|
56
|
+
- Trace capture for debugging
|
|
57
|
+
- Interactive HTML report generation
|
|
58
|
+
- Configurable bail on failures
|
|
59
|
+
|
|
60
|
+
**Use case**: Full-featured UI testing with rich artifacts
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
qa360 run examples/ui-advanced.yml
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### 2c. **crawler.yml** - Web Crawler
|
|
69
|
+
Auto-generate tests by crawling a web application.
|
|
70
|
+
|
|
71
|
+
**Gates**: `ui`
|
|
72
|
+
|
|
73
|
+
**Features**:
|
|
74
|
+
- Automatic discovery of pages and forms
|
|
75
|
+
- Generate test packs from existing sites
|
|
76
|
+
- Smart selector generation
|
|
77
|
+
|
|
78
|
+
**Use case**: Quick start with existing applications
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
qa360 run examples/crawler.yml
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
48
86
|
### 3. **fullstack.yml** - Full Stack Tests
|
|
49
87
|
API + UI + Performance testing.
|
|
50
88
|
|
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
# QA360 Example: Accessibility Tests
|
|
2
2
|
# WCAG compliance and accessibility testing
|
|
3
3
|
|
|
4
|
-
version:
|
|
4
|
+
version: 2
|
|
5
5
|
name: accessibility-tests
|
|
6
|
+
description: WCAG compliance and accessibility testing
|
|
6
7
|
|
|
7
8
|
gates:
|
|
8
|
-
-
|
|
9
|
-
|
|
9
|
+
ui-smoke:
|
|
10
|
+
adapter: playwright-ui
|
|
11
|
+
enabled: true
|
|
12
|
+
config:
|
|
13
|
+
baseUrl: "https://example.com"
|
|
14
|
+
pages:
|
|
15
|
+
- url: "/"
|
|
16
|
+
expectedElements: ["body", "main"]
|
|
17
|
+
- url: "/about"
|
|
18
|
+
expectedElements: ["body"]
|
|
19
|
+
- url: "/contact"
|
|
20
|
+
expectedElements: ["body", "form"]
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
a11y:
|
|
23
|
+
adapter: playwright-ui
|
|
24
|
+
enabled: true
|
|
25
|
+
config:
|
|
26
|
+
baseUrl: "https://example.com"
|
|
27
|
+
pages:
|
|
28
|
+
- url: "/"
|
|
29
|
+
a11yRules:
|
|
30
|
+
- wcag2a
|
|
31
|
+
- wcag2aa
|
|
32
|
+
- url: "/about"
|
|
33
|
+
a11yRules:
|
|
34
|
+
- wcag2a
|
|
35
|
+
- wcag2aa
|
|
36
|
+
- url: "/contact"
|
|
37
|
+
a11yRules:
|
|
38
|
+
- wcag2a
|
|
39
|
+
- wcag2aa
|
|
40
|
+
budgets:
|
|
41
|
+
violations: 5
|
|
42
|
+
a11y_score: 90
|
|
22
43
|
|
|
23
44
|
execution:
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
default_timeout: 60000
|
|
46
|
+
default_retries: 1
|
|
47
|
+
on_failure: continue
|
|
48
|
+
parallel: true
|