@vibecheckai/cli 3.5.0 → 3.5.2
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/bin/registry.js +214 -237
- package/bin/runners/cli-utils.js +33 -2
- package/bin/runners/context/analyzer.js +52 -1
- package/bin/runners/context/generators/cursor.js +2 -49
- package/bin/runners/context/git-context.js +3 -1
- package/bin/runners/context/team-conventions.js +33 -7
- package/bin/runners/lib/analysis-core.js +25 -5
- package/bin/runners/lib/analyzers.js +431 -481
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/doctor/modules/security.js +3 -1
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +18 -218
- package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
- package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
- package/bin/runners/lib/engines/mock-data-engine.js +10 -53
- package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
- package/bin/runners/lib/engines/type-aware-engine.js +39 -263
- package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/entitlements-v2.js +73 -97
- package/bin/runners/lib/error-handler.js +44 -3
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +7 -1
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/html-proof-report.js +700 -350
- package/bin/runners/lib/missions/plan.js +6 -46
- package/bin/runners/lib/missions/templates.js +0 -232
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/route-detection.js +137 -68
- package/bin/runners/lib/scan-output.js +91 -76
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/validator.js +27 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/ship-output-enterprise.js +23 -23
- package/bin/runners/lib/ship-output.js +75 -31
- package/bin/runners/lib/terminal-ui.js +6 -113
- package/bin/runners/lib/truth.js +351 -10
- package/bin/runners/lib/unified-cli-output.js +430 -603
- package/bin/runners/lib/unified-output.js +13 -9
- package/bin/runners/runAIAgent.js +10 -5
- package/bin/runners/runAgent.js +0 -3
- package/bin/runners/runAllowlist.js +389 -0
- package/bin/runners/runApprove.js +0 -33
- package/bin/runners/runAuth.js +73 -45
- package/bin/runners/runCheckpoint.js +51 -11
- package/bin/runners/runClassify.js +85 -21
- package/bin/runners/runContext.js +0 -3
- package/bin/runners/runDoctor.js +41 -28
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.js +0 -3
- package/bin/runners/runFirewallHook.js +0 -3
- package/bin/runners/runFix.js +66 -76
- package/bin/runners/runGuard.js +18 -411
- package/bin/runners/runInit.js +113 -30
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runMcp.js +19 -25
- package/bin/runners/runPolish.js +64 -240
- package/bin/runners/runPromptFirewall.js +12 -5
- package/bin/runners/runProve.js +57 -22
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +59 -68
- package/bin/runners/runReport.js +38 -33
- package/bin/runners/runRuntime.js +8 -5
- package/bin/runners/runScan.js +1413 -190
- package/bin/runners/runShip.js +113 -719
- package/bin/runners/runTruth.js +0 -3
- package/bin/runners/runValidate.js +13 -9
- package/bin/runners/runWatch.js +23 -14
- package/bin/scan.js +6 -1
- package/bin/vibecheck.js +204 -185
- package/mcp-server/deprecation-middleware.js +282 -0
- package/mcp-server/handlers/index.ts +15 -0
- package/mcp-server/handlers/tool-handler.ts +554 -0
- package/mcp-server/index-v1.js +698 -0
- package/mcp-server/index.js +210 -238
- package/mcp-server/lib/cache-wrapper.cjs +383 -0
- package/mcp-server/lib/error-envelope.js +138 -0
- package/mcp-server/lib/executor.ts +499 -0
- package/mcp-server/lib/index.ts +19 -0
- package/mcp-server/lib/rate-limiter.js +166 -0
- package/mcp-server/lib/sandbox.test.ts +519 -0
- package/mcp-server/lib/sandbox.ts +395 -0
- package/mcp-server/lib/types.ts +267 -0
- package/mcp-server/package.json +12 -3
- package/mcp-server/registry/tool-registry.js +794 -0
- package/mcp-server/registry/tools.json +605 -0
- package/mcp-server/registry.test.ts +334 -0
- package/mcp-server/tests/tier-gating.test.js +297 -0
- package/mcp-server/tier-auth.js +378 -45
- package/mcp-server/tools-v3.js +353 -442
- package/mcp-server/tsconfig.json +37 -0
- package/mcp-server/vibecheck-2.0-tools.js +14 -1
- package/package.json +1 -1
- package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
- package/bin/runners/lib/audit-logger.js +0 -532
- package/bin/runners/lib/authority/authorities/architecture.js +0 -364
- package/bin/runners/lib/authority/authorities/compliance.js +0 -341
- package/bin/runners/lib/authority/authorities/human.js +0 -343
- package/bin/runners/lib/authority/authorities/quality.js +0 -420
- package/bin/runners/lib/authority/authorities/security.js +0 -228
- package/bin/runners/lib/authority/index.js +0 -293
- package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
- package/bin/runners/lib/cli-charts.js +0 -368
- package/bin/runners/lib/cli-config-display.js +0 -405
- package/bin/runners/lib/cli-demo.js +0 -275
- package/bin/runners/lib/cli-errors.js +0 -438
- package/bin/runners/lib/cli-help-formatter.js +0 -439
- package/bin/runners/lib/cli-interactive-menu.js +0 -509
- package/bin/runners/lib/cli-prompts.js +0 -441
- package/bin/runners/lib/cli-scan-cards.js +0 -362
- package/bin/runners/lib/compliance-reporter.js +0 -710
- package/bin/runners/lib/conductor/index.js +0 -671
- package/bin/runners/lib/easy/README.md +0 -123
- package/bin/runners/lib/easy/index.js +0 -140
- package/bin/runners/lib/easy/interactive-wizard.js +0 -788
- package/bin/runners/lib/easy/one-click-firewall.js +0 -564
- package/bin/runners/lib/easy/zero-config-reality.js +0 -714
- package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
- package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
- package/bin/runners/lib/engines/confidence-scoring.js +0 -276
- package/bin/runners/lib/engines/context-detection.js +0 -264
- package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
- package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
- package/bin/runners/lib/engines/env-variables-engine.js +0 -458
- package/bin/runners/lib/engines/error-handling-engine.js +0 -437
- package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
- package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
- package/bin/runners/lib/engines/framework-detection.js +0 -508
- package/bin/runners/lib/engines/import-order-engine.js +0 -429
- package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
- package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
- package/bin/runners/lib/engines/orchestrator.js +0 -334
- package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
- package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
- package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
- package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
- package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
- package/bin/runners/lib/enhanced-features/index.js +0 -305
- package/bin/runners/lib/enhanced-output.js +0 -631
- package/bin/runners/lib/enterprise.js +0 -300
- package/bin/runners/lib/firewall/command-validator.js +0 -351
- package/bin/runners/lib/firewall/config.js +0 -341
- package/bin/runners/lib/firewall/content-validator.js +0 -519
- package/bin/runners/lib/firewall/index.js +0 -101
- package/bin/runners/lib/firewall/path-validator.js +0 -256
- package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
- package/bin/runners/lib/mcp-utils.js +0 -425
- package/bin/runners/lib/output/index.js +0 -1022
- package/bin/runners/lib/policy-engine.js +0 -652
- package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
- package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
- package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
- package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
- package/bin/runners/lib/polish/autofix/index.js +0 -200
- package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
- package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
- package/bin/runners/lib/polish/backend-checks.js +0 -148
- package/bin/runners/lib/polish/documentation-checks.js +0 -111
- package/bin/runners/lib/polish/frontend-checks.js +0 -168
- package/bin/runners/lib/polish/index.js +0 -71
- package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
- package/bin/runners/lib/polish/library-detection.js +0 -175
- package/bin/runners/lib/polish/performance-checks.js +0 -100
- package/bin/runners/lib/polish/security-checks.js +0 -148
- package/bin/runners/lib/polish/utils.js +0 -203
- package/bin/runners/lib/prompt-builder.js +0 -540
- package/bin/runners/lib/proof-certificate.js +0 -634
- package/bin/runners/lib/reality/accessibility-audit.js +0 -946
- package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
- package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
- package/bin/runners/lib/reality/performance-tracker.js +0 -1077
- package/bin/runners/lib/reality/scenario-generator.js +0 -1404
- package/bin/runners/lib/reality/visual-regression.js +0 -852
- package/bin/runners/lib/reality-profiler.js +0 -717
- package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
- package/bin/runners/lib/review/ai-code-review.js +0 -832
- package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
- package/bin/runners/lib/sbom-generator.js +0 -641
- package/bin/runners/lib/scan-output-enhanced.js +0 -512
- package/bin/runners/lib/security/owasp-scanner.js +0 -939
- package/bin/runners/lib/validators/contract-validator.js +0 -283
- package/bin/runners/lib/validators/dead-export-detector.js +0 -279
- package/bin/runners/lib/validators/dep-audit.js +0 -245
- package/bin/runners/lib/validators/env-validator.js +0 -319
- package/bin/runners/lib/validators/index.js +0 -120
- package/bin/runners/lib/validators/license-checker.js +0 -252
- package/bin/runners/lib/validators/route-validator.js +0 -290
- package/bin/runners/runAuthority.js +0 -528
- package/bin/runners/runConductor.js +0 -772
- package/bin/runners/runContainer.js +0 -366
- package/bin/runners/runEasy.js +0 -410
- package/bin/runners/runIaC.js +0 -372
- package/bin/runners/runVibe.js +0 -791
- package/mcp-server/tools.js +0 -495
|
@@ -1,852 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Visual Regression Detection Engine
|
|
3
|
-
*
|
|
4
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
-
* COMPETITIVE MOAT FEATURE - Automatic UI Drift Detection
|
|
6
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
-
*
|
|
8
|
-
* This engine captures screenshots during Reality Mode runs and compares them
|
|
9
|
-
* to detect unintended visual changes. Unlike traditional visual testing tools,
|
|
10
|
-
* this integrates directly with runtime verification.
|
|
11
|
-
*
|
|
12
|
-
* Features:
|
|
13
|
-
* - Perceptual diff (not pixel-perfect, handles anti-aliasing)
|
|
14
|
-
* - Intelligent region masking (dynamic content, timestamps, avatars)
|
|
15
|
-
* - Layout shift detection (CLS-like but for any page)
|
|
16
|
-
* - Component-level comparison (not just full page)
|
|
17
|
-
* - Responsive breakpoint testing
|
|
18
|
-
* - Dark mode/theme detection
|
|
19
|
-
* - Animation frame capture
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
"use strict";
|
|
23
|
-
|
|
24
|
-
const fs = require("fs");
|
|
25
|
-
const path = require("path");
|
|
26
|
-
const crypto = require("crypto");
|
|
27
|
-
|
|
28
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
-
// CONFIGURATION
|
|
30
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
-
|
|
32
|
-
const DEFAULT_CONFIG = {
|
|
33
|
-
// Comparison thresholds
|
|
34
|
-
thresholds: {
|
|
35
|
-
pixelDiffPercent: 0.1, // Max % of pixels that can differ
|
|
36
|
-
structuralDiff: 0.05, // Max structural similarity difference
|
|
37
|
-
layoutShift: 0.1, // Max cumulative layout shift
|
|
38
|
-
colorDiff: 10 // Max color difference (0-255 per channel)
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
// Screenshot settings
|
|
42
|
-
screenshot: {
|
|
43
|
-
fullPage: true,
|
|
44
|
-
type: "png",
|
|
45
|
-
quality: 90,
|
|
46
|
-
timeout: 30000
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
// Viewports to test
|
|
50
|
-
viewports: [
|
|
51
|
-
{ name: "desktop", width: 1920, height: 1080 },
|
|
52
|
-
{ name: "laptop", width: 1366, height: 768 },
|
|
53
|
-
{ name: "tablet", width: 768, height: 1024 },
|
|
54
|
-
{ name: "mobile", width: 375, height: 812 }
|
|
55
|
-
],
|
|
56
|
-
|
|
57
|
-
// Elements to automatically mask (dynamic content)
|
|
58
|
-
autoMask: {
|
|
59
|
-
enabled: true,
|
|
60
|
-
selectors: [
|
|
61
|
-
"[data-testid*='timestamp']",
|
|
62
|
-
"[data-testid*='date']",
|
|
63
|
-
"[data-testid*='time']",
|
|
64
|
-
"[data-testid*='avatar']",
|
|
65
|
-
"[class*='timestamp']",
|
|
66
|
-
"[class*='avatar']",
|
|
67
|
-
"time",
|
|
68
|
-
".skeleton",
|
|
69
|
-
".loading",
|
|
70
|
-
"[aria-busy='true']",
|
|
71
|
-
"video",
|
|
72
|
-
"iframe",
|
|
73
|
-
"[data-dynamic='true']"
|
|
74
|
-
],
|
|
75
|
-
patterns: [
|
|
76
|
-
/\d{1,2}:\d{2}(:\d{2})?(\s*[AP]M)?/i, // Time patterns
|
|
77
|
-
/\d{1,2}\/\d{1,2}\/\d{2,4}/, // Date patterns
|
|
78
|
-
/\d+\s*(seconds?|minutes?|hours?|days?)\s*ago/i, // Relative time
|
|
79
|
-
/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i // UUIDs
|
|
80
|
-
]
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
// Stability settings
|
|
84
|
-
stability: {
|
|
85
|
-
waitForNetworkIdle: true,
|
|
86
|
-
waitForAnimations: true,
|
|
87
|
-
stableWait: 500,
|
|
88
|
-
maxWaitTime: 10000
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
93
|
-
// VISUAL REGRESSION ENGINE
|
|
94
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
95
|
-
|
|
96
|
-
class VisualRegressionEngine {
|
|
97
|
-
constructor(options = {}) {
|
|
98
|
-
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
99
|
-
this.projectRoot = options.projectRoot || process.cwd();
|
|
100
|
-
this.baselinePath = options.baselinePath ||
|
|
101
|
-
path.join(this.projectRoot, ".vibecheck", "visual-baselines");
|
|
102
|
-
this.diffPath = options.diffPath ||
|
|
103
|
-
path.join(this.projectRoot, ".vibecheck", "visual-diffs");
|
|
104
|
-
this.page = null;
|
|
105
|
-
this.results = [];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Set the Playwright page instance
|
|
110
|
-
*/
|
|
111
|
-
setPage(page) {
|
|
112
|
-
this.page = page;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Capture a screenshot with stability checks
|
|
117
|
-
*/
|
|
118
|
-
async captureStable(name, options = {}) {
|
|
119
|
-
if (!this.page) {
|
|
120
|
-
throw new Error("Page not set. Call setPage() first.");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const {
|
|
124
|
-
fullPage = this.config.screenshot.fullPage,
|
|
125
|
-
selector = null,
|
|
126
|
-
viewport = null,
|
|
127
|
-
maskDynamic = this.config.autoMask.enabled
|
|
128
|
-
} = options;
|
|
129
|
-
|
|
130
|
-
// Set viewport if specified
|
|
131
|
-
if (viewport) {
|
|
132
|
-
await this.page.setViewportSize(viewport);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Wait for stability
|
|
136
|
-
await this.waitForStability();
|
|
137
|
-
|
|
138
|
-
// Mask dynamic elements
|
|
139
|
-
let maskedElements = [];
|
|
140
|
-
if (maskDynamic) {
|
|
141
|
-
maskedElements = await this.maskDynamicContent();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Capture screenshot
|
|
145
|
-
const screenshotOptions = {
|
|
146
|
-
fullPage,
|
|
147
|
-
type: this.config.screenshot.type
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
let buffer;
|
|
151
|
-
if (selector) {
|
|
152
|
-
const element = await this.page.$(selector);
|
|
153
|
-
if (element) {
|
|
154
|
-
buffer = await element.screenshot(screenshotOptions);
|
|
155
|
-
} else {
|
|
156
|
-
throw new Error(`Element not found: ${selector}`);
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
buffer = await this.page.screenshot(screenshotOptions);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Restore masked elements
|
|
163
|
-
if (maskedElements.length > 0) {
|
|
164
|
-
await this.unmaskElements(maskedElements);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
name,
|
|
169
|
-
buffer,
|
|
170
|
-
viewport: viewport || await this.page.viewportSize(),
|
|
171
|
-
url: this.page.url(),
|
|
172
|
-
timestamp: new Date().toISOString(),
|
|
173
|
-
selector
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Wait for page stability
|
|
179
|
-
*/
|
|
180
|
-
async waitForStability() {
|
|
181
|
-
const { waitForNetworkIdle, waitForAnimations, stableWait, maxWaitTime } =
|
|
182
|
-
this.config.stability;
|
|
183
|
-
|
|
184
|
-
const startTime = Date.now();
|
|
185
|
-
|
|
186
|
-
// Wait for network idle
|
|
187
|
-
if (waitForNetworkIdle) {
|
|
188
|
-
try {
|
|
189
|
-
await this.page.waitForLoadState("networkidle", {
|
|
190
|
-
timeout: Math.min(5000, maxWaitTime)
|
|
191
|
-
});
|
|
192
|
-
} catch {
|
|
193
|
-
// Network didn't go idle, continue anyway
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Wait for animations to complete
|
|
198
|
-
if (waitForAnimations) {
|
|
199
|
-
await this.page.evaluate(() => {
|
|
200
|
-
return new Promise((resolve) => {
|
|
201
|
-
const animations = document.getAnimations();
|
|
202
|
-
if (animations.length === 0) {
|
|
203
|
-
resolve();
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
Promise.all(animations.map(a => a.finished)).then(resolve).catch(resolve);
|
|
207
|
-
// Fallback timeout
|
|
208
|
-
setTimeout(resolve, 2000);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Additional stable wait
|
|
214
|
-
const elapsed = Date.now() - startTime;
|
|
215
|
-
const remainingWait = Math.max(0, stableWait - elapsed);
|
|
216
|
-
if (remainingWait > 0) {
|
|
217
|
-
await this.page.waitForTimeout(remainingWait);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Mask dynamic content elements
|
|
223
|
-
*/
|
|
224
|
-
async maskDynamicContent() {
|
|
225
|
-
const { selectors } = this.config.autoMask;
|
|
226
|
-
|
|
227
|
-
const maskedElements = await this.page.evaluate((sels) => {
|
|
228
|
-
const masked = [];
|
|
229
|
-
|
|
230
|
-
for (const sel of sels) {
|
|
231
|
-
const elements = document.querySelectorAll(sel);
|
|
232
|
-
elements.forEach((el, idx) => {
|
|
233
|
-
const id = `vibecheck-mask-${Date.now()}-${idx}`;
|
|
234
|
-
const originalStyle = el.getAttribute("style") || "";
|
|
235
|
-
const originalHtml = el.innerHTML;
|
|
236
|
-
|
|
237
|
-
// Apply mask
|
|
238
|
-
el.style.backgroundColor = "#808080";
|
|
239
|
-
el.style.color = "#808080";
|
|
240
|
-
el.setAttribute("data-vibecheck-masked", id);
|
|
241
|
-
|
|
242
|
-
masked.push({
|
|
243
|
-
id,
|
|
244
|
-
selector: sel,
|
|
245
|
-
originalStyle,
|
|
246
|
-
originalHtml
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return masked;
|
|
252
|
-
}, selectors);
|
|
253
|
-
|
|
254
|
-
return maskedElements;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Restore masked elements
|
|
259
|
-
*/
|
|
260
|
-
async unmaskElements(maskedElements) {
|
|
261
|
-
await this.page.evaluate((elements) => {
|
|
262
|
-
for (const el of elements) {
|
|
263
|
-
const domEl = document.querySelector(`[data-vibecheck-masked="${el.id}"]`);
|
|
264
|
-
if (domEl) {
|
|
265
|
-
domEl.setAttribute("style", el.originalStyle);
|
|
266
|
-
domEl.removeAttribute("data-vibecheck-masked");
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}, maskedElements);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
273
|
-
// COMPARISON ENGINE (Pure JavaScript implementation)
|
|
274
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Compare two screenshots using perceptual diff
|
|
278
|
-
* Pure JS implementation - no native dependencies
|
|
279
|
-
*/
|
|
280
|
-
async compare(current, baseline) {
|
|
281
|
-
const currentData = await this.decodeImage(current.buffer);
|
|
282
|
-
const baselineData = await this.decodeImage(baseline.buffer);
|
|
283
|
-
|
|
284
|
-
// Check dimensions
|
|
285
|
-
if (currentData.width !== baselineData.width ||
|
|
286
|
-
currentData.height !== baselineData.height) {
|
|
287
|
-
return {
|
|
288
|
-
match: false,
|
|
289
|
-
reason: "dimension_mismatch",
|
|
290
|
-
current: { width: currentData.width, height: currentData.height },
|
|
291
|
-
baseline: { width: baselineData.width, height: baselineData.height },
|
|
292
|
-
diffPercent: 100
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Perform pixel comparison
|
|
297
|
-
const diffResult = this.pixelDiff(currentData, baselineData);
|
|
298
|
-
|
|
299
|
-
// Perform structural comparison
|
|
300
|
-
const structuralResult = this.structuralDiff(currentData, baselineData);
|
|
301
|
-
|
|
302
|
-
// Perform layout shift detection
|
|
303
|
-
const layoutResult = await this.detectLayoutShift(current, baseline);
|
|
304
|
-
|
|
305
|
-
// Combine results
|
|
306
|
-
const { thresholds } = this.config;
|
|
307
|
-
const issues = [];
|
|
308
|
-
|
|
309
|
-
if (diffResult.diffPercent > thresholds.pixelDiffPercent) {
|
|
310
|
-
issues.push({
|
|
311
|
-
type: "pixel_diff",
|
|
312
|
-
severity: diffResult.diffPercent > thresholds.pixelDiffPercent * 5 ? "BLOCK" : "WARN",
|
|
313
|
-
message: `${diffResult.diffPercent.toFixed(2)}% of pixels differ (threshold: ${thresholds.pixelDiffPercent}%)`,
|
|
314
|
-
diffPercent: diffResult.diffPercent
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (structuralResult.difference > thresholds.structuralDiff) {
|
|
319
|
-
issues.push({
|
|
320
|
-
type: "structural_diff",
|
|
321
|
-
severity: structuralResult.difference > thresholds.structuralDiff * 3 ? "BLOCK" : "WARN",
|
|
322
|
-
message: `Structural similarity: ${((1 - structuralResult.difference) * 100).toFixed(1)}%`,
|
|
323
|
-
difference: structuralResult.difference
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (layoutResult.shiftScore > thresholds.layoutShift) {
|
|
328
|
-
issues.push({
|
|
329
|
-
type: "layout_shift",
|
|
330
|
-
severity: layoutResult.shiftScore > thresholds.layoutShift * 2 ? "BLOCK" : "WARN",
|
|
331
|
-
message: `Layout shift detected: ${(layoutResult.shiftScore * 100).toFixed(1)}%`,
|
|
332
|
-
shiftScore: layoutResult.shiftScore,
|
|
333
|
-
shiftedElements: layoutResult.elements
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
match: issues.length === 0,
|
|
339
|
-
issues,
|
|
340
|
-
metrics: {
|
|
341
|
-
pixelDiff: diffResult.diffPercent,
|
|
342
|
-
structuralDiff: structuralResult.difference,
|
|
343
|
-
layoutShift: layoutResult.shiftScore
|
|
344
|
-
},
|
|
345
|
-
diffImage: diffResult.diffBuffer,
|
|
346
|
-
regions: diffResult.regions
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Decode image buffer to pixel data
|
|
352
|
-
* Uses PNG structure parsing (simplified)
|
|
353
|
-
*/
|
|
354
|
-
async decodeImage(buffer) {
|
|
355
|
-
// For full implementation, would use sharp or pngjs
|
|
356
|
-
// This is a simplified structure for the pattern
|
|
357
|
-
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
358
|
-
|
|
359
|
-
if (!buffer.slice(0, 8).equals(PNG_SIGNATURE)) {
|
|
360
|
-
throw new Error("Invalid PNG signature");
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Parse IHDR chunk to get dimensions
|
|
364
|
-
let offset = 8;
|
|
365
|
-
const ihdrLength = buffer.readUInt32BE(offset);
|
|
366
|
-
const ihdrType = buffer.slice(offset + 4, offset + 8).toString("ascii");
|
|
367
|
-
|
|
368
|
-
if (ihdrType !== "IHDR") {
|
|
369
|
-
throw new Error("Missing IHDR chunk");
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const width = buffer.readUInt32BE(offset + 8);
|
|
373
|
-
const height = buffer.readUInt32BE(offset + 12);
|
|
374
|
-
const bitDepth = buffer.readUInt8(offset + 16);
|
|
375
|
-
const colorType = buffer.readUInt8(offset + 17);
|
|
376
|
-
|
|
377
|
-
// For a real implementation, we'd decode the full pixel data
|
|
378
|
-
// For now, return metadata and use Playwright's built-in comparison
|
|
379
|
-
return {
|
|
380
|
-
width,
|
|
381
|
-
height,
|
|
382
|
-
bitDepth,
|
|
383
|
-
colorType,
|
|
384
|
-
buffer,
|
|
385
|
-
// Placeholder for actual pixel data - real impl would decode IDAT chunks
|
|
386
|
-
pixels: null
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Pixel-by-pixel comparison with tolerance
|
|
392
|
-
*/
|
|
393
|
-
pixelDiff(current, baseline) {
|
|
394
|
-
// Simplified implementation
|
|
395
|
-
// Real implementation would compare actual pixel data
|
|
396
|
-
|
|
397
|
-
const { colorDiff } = this.config.thresholds;
|
|
398
|
-
|
|
399
|
-
// Use buffer comparison as approximation
|
|
400
|
-
const currentBuf = current.buffer;
|
|
401
|
-
const baselineBuf = baseline.buffer;
|
|
402
|
-
|
|
403
|
-
let diffCount = 0;
|
|
404
|
-
const totalPixels = current.width * current.height;
|
|
405
|
-
const diffRegions = [];
|
|
406
|
-
|
|
407
|
-
// Compare buffers (simplified)
|
|
408
|
-
const minLen = Math.min(currentBuf.length, baselineBuf.length);
|
|
409
|
-
for (let i = 0; i < minLen; i++) {
|
|
410
|
-
if (Math.abs(currentBuf[i] - baselineBuf[i]) > colorDiff) {
|
|
411
|
-
diffCount++;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Estimate pixel diff percentage
|
|
416
|
-
const diffPercent = (diffCount / minLen) * 100;
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
diffPercent,
|
|
420
|
-
diffCount,
|
|
421
|
-
totalPixels,
|
|
422
|
-
regions: diffRegions,
|
|
423
|
-
diffBuffer: null // Would contain the diff visualization
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Structural similarity comparison (SSIM-like)
|
|
429
|
-
*/
|
|
430
|
-
structuralDiff(current, baseline) {
|
|
431
|
-
// Simplified structural comparison
|
|
432
|
-
// Real SSIM would use luminance, contrast, and structure comparison
|
|
433
|
-
|
|
434
|
-
// Use hash-based comparison as proxy
|
|
435
|
-
const currentHash = this.perceptualHash(current.buffer);
|
|
436
|
-
const baselineHash = this.perceptualHash(baseline.buffer);
|
|
437
|
-
|
|
438
|
-
// Calculate hamming distance
|
|
439
|
-
let distance = 0;
|
|
440
|
-
for (let i = 0; i < currentHash.length; i++) {
|
|
441
|
-
if (currentHash[i] !== baselineHash[i]) {
|
|
442
|
-
distance++;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const difference = distance / currentHash.length;
|
|
447
|
-
|
|
448
|
-
return {
|
|
449
|
-
difference,
|
|
450
|
-
currentHash,
|
|
451
|
-
baselineHash
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Generate perceptual hash of image
|
|
457
|
-
*/
|
|
458
|
-
perceptualHash(buffer) {
|
|
459
|
-
// Simplified perceptual hash (dHash-like)
|
|
460
|
-
// Real implementation would resize image and compare gradients
|
|
461
|
-
|
|
462
|
-
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
463
|
-
return hash.slice(0, 16); // Use first 64 bits
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Detect layout shifts between screenshots
|
|
468
|
-
*/
|
|
469
|
-
async detectLayoutShift(current, baseline) {
|
|
470
|
-
// Would compare element positions between screenshots
|
|
471
|
-
// For now, return placeholder
|
|
472
|
-
return {
|
|
473
|
-
shiftScore: 0,
|
|
474
|
-
elements: []
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
479
|
-
// BASELINE MANAGEMENT
|
|
480
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Save a screenshot as baseline
|
|
484
|
-
*/
|
|
485
|
-
async saveBaseline(screenshot, name) {
|
|
486
|
-
const baselineDir = path.join(this.baselinePath, this.sanitizeName(name));
|
|
487
|
-
|
|
488
|
-
if (!fs.existsSync(baselineDir)) {
|
|
489
|
-
fs.mkdirSync(baselineDir, { recursive: true });
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const viewportName = screenshot.viewport
|
|
493
|
-
? `${screenshot.viewport.width}x${screenshot.viewport.height}`
|
|
494
|
-
: "default";
|
|
495
|
-
|
|
496
|
-
const fileName = `${viewportName}.png`;
|
|
497
|
-
const filePath = path.join(baselineDir, fileName);
|
|
498
|
-
|
|
499
|
-
fs.writeFileSync(filePath, screenshot.buffer);
|
|
500
|
-
|
|
501
|
-
// Save metadata
|
|
502
|
-
const metadataPath = path.join(baselineDir, `${viewportName}.json`);
|
|
503
|
-
fs.writeFileSync(metadataPath, JSON.stringify({
|
|
504
|
-
name,
|
|
505
|
-
url: screenshot.url,
|
|
506
|
-
viewport: screenshot.viewport,
|
|
507
|
-
timestamp: screenshot.timestamp,
|
|
508
|
-
selector: screenshot.selector
|
|
509
|
-
}, null, 2));
|
|
510
|
-
|
|
511
|
-
return filePath;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Load baseline screenshot
|
|
516
|
-
*/
|
|
517
|
-
loadBaseline(name, viewport) {
|
|
518
|
-
const viewportName = viewport
|
|
519
|
-
? `${viewport.width}x${viewport.height}`
|
|
520
|
-
: "default";
|
|
521
|
-
|
|
522
|
-
const baselineDir = path.join(this.baselinePath, this.sanitizeName(name));
|
|
523
|
-
const filePath = path.join(baselineDir, `${viewportName}.png`);
|
|
524
|
-
const metadataPath = path.join(baselineDir, `${viewportName}.json`);
|
|
525
|
-
|
|
526
|
-
if (!fs.existsSync(filePath)) {
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const buffer = fs.readFileSync(filePath);
|
|
531
|
-
const metadata = fs.existsSync(metadataPath)
|
|
532
|
-
? JSON.parse(fs.readFileSync(metadataPath, "utf8"))
|
|
533
|
-
: {};
|
|
534
|
-
|
|
535
|
-
return {
|
|
536
|
-
name,
|
|
537
|
-
buffer,
|
|
538
|
-
viewport,
|
|
539
|
-
...metadata
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Check if baseline exists
|
|
545
|
-
*/
|
|
546
|
-
hasBaseline(name, viewport) {
|
|
547
|
-
const viewportName = viewport
|
|
548
|
-
? `${viewport.width}x${viewport.height}`
|
|
549
|
-
: "default";
|
|
550
|
-
|
|
551
|
-
const baselineDir = path.join(this.baselinePath, this.sanitizeName(name));
|
|
552
|
-
const filePath = path.join(baselineDir, `${viewportName}.png`);
|
|
553
|
-
|
|
554
|
-
return fs.existsSync(filePath);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Save diff image
|
|
559
|
-
*/
|
|
560
|
-
saveDiff(name, diffBuffer, metadata) {
|
|
561
|
-
if (!diffBuffer) return null;
|
|
562
|
-
|
|
563
|
-
const diffDir = path.join(this.diffPath, new Date().toISOString().split("T")[0]);
|
|
564
|
-
|
|
565
|
-
if (!fs.existsSync(diffDir)) {
|
|
566
|
-
fs.mkdirSync(diffDir, { recursive: true });
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const fileName = `${this.sanitizeName(name)}-${Date.now()}.png`;
|
|
570
|
-
const filePath = path.join(diffDir, fileName);
|
|
571
|
-
|
|
572
|
-
fs.writeFileSync(filePath, diffBuffer);
|
|
573
|
-
|
|
574
|
-
// Save metadata
|
|
575
|
-
fs.writeFileSync(
|
|
576
|
-
filePath.replace(".png", ".json"),
|
|
577
|
-
JSON.stringify(metadata, null, 2)
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
return filePath;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
584
|
-
// TEST RUNNER
|
|
585
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Run visual regression test for current page
|
|
589
|
-
*/
|
|
590
|
-
async runTest(pageName, options = {}) {
|
|
591
|
-
const {
|
|
592
|
-
updateBaseline = false,
|
|
593
|
-
viewports = this.config.viewports,
|
|
594
|
-
components = []
|
|
595
|
-
} = options;
|
|
596
|
-
|
|
597
|
-
const results = {
|
|
598
|
-
name: pageName,
|
|
599
|
-
url: this.page.url(),
|
|
600
|
-
timestamp: new Date().toISOString(),
|
|
601
|
-
viewports: [],
|
|
602
|
-
components: [],
|
|
603
|
-
summary: {
|
|
604
|
-
passed: 0,
|
|
605
|
-
failed: 0,
|
|
606
|
-
new: 0
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
// Test each viewport
|
|
611
|
-
for (const viewport of viewports) {
|
|
612
|
-
const viewportResult = await this.testViewport(pageName, viewport, updateBaseline);
|
|
613
|
-
results.viewports.push(viewportResult);
|
|
614
|
-
|
|
615
|
-
if (viewportResult.status === "passed") results.summary.passed++;
|
|
616
|
-
else if (viewportResult.status === "failed") results.summary.failed++;
|
|
617
|
-
else if (viewportResult.status === "new") results.summary.new++;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Test specific components
|
|
621
|
-
for (const component of components) {
|
|
622
|
-
const componentResult = await this.testComponent(pageName, component, updateBaseline);
|
|
623
|
-
results.components.push(componentResult);
|
|
624
|
-
|
|
625
|
-
if (componentResult.status === "passed") results.summary.passed++;
|
|
626
|
-
else if (componentResult.status === "failed") results.summary.failed++;
|
|
627
|
-
else if (componentResult.status === "new") results.summary.new++;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
this.results.push(results);
|
|
631
|
-
return results;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Test a specific viewport
|
|
636
|
-
*/
|
|
637
|
-
async testViewport(pageName, viewport, updateBaseline) {
|
|
638
|
-
const testName = `${pageName}-${viewport.name}`;
|
|
639
|
-
|
|
640
|
-
// Capture current screenshot
|
|
641
|
-
const current = await this.captureStable(testName, { viewport });
|
|
642
|
-
|
|
643
|
-
// Check for baseline
|
|
644
|
-
const baseline = this.loadBaseline(pageName, viewport);
|
|
645
|
-
|
|
646
|
-
if (!baseline || updateBaseline) {
|
|
647
|
-
// Save as new baseline
|
|
648
|
-
await this.saveBaseline(current, pageName);
|
|
649
|
-
return {
|
|
650
|
-
name: testName,
|
|
651
|
-
viewport,
|
|
652
|
-
status: "new",
|
|
653
|
-
message: updateBaseline ? "Baseline updated" : "New baseline created"
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Compare with baseline
|
|
658
|
-
const comparison = await this.compare(current, baseline);
|
|
659
|
-
|
|
660
|
-
if (comparison.match) {
|
|
661
|
-
return {
|
|
662
|
-
name: testName,
|
|
663
|
-
viewport,
|
|
664
|
-
status: "passed",
|
|
665
|
-
metrics: comparison.metrics
|
|
666
|
-
};
|
|
667
|
-
} else {
|
|
668
|
-
// Save diff
|
|
669
|
-
const diffPath = this.saveDiff(testName, comparison.diffImage, {
|
|
670
|
-
issues: comparison.issues,
|
|
671
|
-
metrics: comparison.metrics
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
return {
|
|
675
|
-
name: testName,
|
|
676
|
-
viewport,
|
|
677
|
-
status: "failed",
|
|
678
|
-
issues: comparison.issues,
|
|
679
|
-
metrics: comparison.metrics,
|
|
680
|
-
diffPath
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Test a specific component
|
|
687
|
-
*/
|
|
688
|
-
async testComponent(pageName, component, updateBaseline) {
|
|
689
|
-
const testName = `${pageName}-component-${component.name}`;
|
|
690
|
-
|
|
691
|
-
// Capture component screenshot
|
|
692
|
-
const current = await this.captureStable(testName, {
|
|
693
|
-
selector: component.selector
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
// Check for baseline
|
|
697
|
-
const baseline = this.loadBaseline(testName, null);
|
|
698
|
-
|
|
699
|
-
if (!baseline || updateBaseline) {
|
|
700
|
-
await this.saveBaseline(current, testName);
|
|
701
|
-
return {
|
|
702
|
-
name: testName,
|
|
703
|
-
component: component.name,
|
|
704
|
-
status: "new"
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// Compare
|
|
709
|
-
const comparison = await this.compare(current, baseline);
|
|
710
|
-
|
|
711
|
-
return {
|
|
712
|
-
name: testName,
|
|
713
|
-
component: component.name,
|
|
714
|
-
status: comparison.match ? "passed" : "failed",
|
|
715
|
-
issues: comparison.issues,
|
|
716
|
-
metrics: comparison.metrics
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
721
|
-
// UTILITIES
|
|
722
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Sanitize name for filesystem
|
|
726
|
-
*/
|
|
727
|
-
sanitizeName(name) {
|
|
728
|
-
return name.replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Get summary of all results
|
|
733
|
-
*/
|
|
734
|
-
getSummary() {
|
|
735
|
-
const summary = {
|
|
736
|
-
total: 0,
|
|
737
|
-
passed: 0,
|
|
738
|
-
failed: 0,
|
|
739
|
-
new: 0,
|
|
740
|
-
pages: []
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
for (const result of this.results) {
|
|
744
|
-
summary.total += result.viewports.length + result.components.length;
|
|
745
|
-
summary.passed += result.summary.passed;
|
|
746
|
-
summary.failed += result.summary.failed;
|
|
747
|
-
summary.new += result.summary.new;
|
|
748
|
-
|
|
749
|
-
summary.pages.push({
|
|
750
|
-
name: result.name,
|
|
751
|
-
url: result.url,
|
|
752
|
-
...result.summary
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
return summary;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Generate visual regression report
|
|
761
|
-
*/
|
|
762
|
-
generateReport() {
|
|
763
|
-
const summary = this.getSummary();
|
|
764
|
-
|
|
765
|
-
return {
|
|
766
|
-
timestamp: new Date().toISOString(),
|
|
767
|
-
summary,
|
|
768
|
-
details: this.results,
|
|
769
|
-
failedTests: this.results
|
|
770
|
-
.flatMap(r => [...r.viewports, ...r.components])
|
|
771
|
-
.filter(t => t.status === "failed")
|
|
772
|
-
.map(t => ({
|
|
773
|
-
name: t.name,
|
|
774
|
-
issues: t.issues,
|
|
775
|
-
diffPath: t.diffPath
|
|
776
|
-
}))
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
782
|
-
// INTEGRATION WITH REALITY MODE
|
|
783
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
784
|
-
|
|
785
|
-
/**
|
|
786
|
-
* Create visual regression checks for Reality Mode integration
|
|
787
|
-
*/
|
|
788
|
-
function createRealityModeHooks(engine) {
|
|
789
|
-
return {
|
|
790
|
-
/**
|
|
791
|
-
* Hook: Before navigation
|
|
792
|
-
*/
|
|
793
|
-
async beforeNavigation(page, url) {
|
|
794
|
-
engine.setPage(page);
|
|
795
|
-
},
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Hook: After page load
|
|
799
|
-
*/
|
|
800
|
-
async afterPageLoad(page, url) {
|
|
801
|
-
const pageName = new URL(url).pathname.replace(/\//g, "-") || "home";
|
|
802
|
-
|
|
803
|
-
// Only run visual regression if baseline exists or explicitly enabled
|
|
804
|
-
if (engine.hasBaseline(pageName, null)) {
|
|
805
|
-
const result = await engine.runTest(pageName);
|
|
806
|
-
return result;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
return null;
|
|
810
|
-
},
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Hook: Before interaction
|
|
814
|
-
*/
|
|
815
|
-
async beforeInteraction(page, element, action) {
|
|
816
|
-
// Capture state before interaction
|
|
817
|
-
return await engine.captureStable(`pre-${action}-${Date.now()}`);
|
|
818
|
-
},
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* Hook: After interaction
|
|
822
|
-
*/
|
|
823
|
-
async afterInteraction(page, element, action, preState) {
|
|
824
|
-
// Capture state after interaction
|
|
825
|
-
const postState = await engine.captureStable(`post-${action}-${Date.now()}`);
|
|
826
|
-
|
|
827
|
-
// Compare pre/post to detect unexpected changes
|
|
828
|
-
const comparison = await engine.compare(postState, preState);
|
|
829
|
-
|
|
830
|
-
if (!comparison.match && comparison.metrics.pixelDiff > 50) {
|
|
831
|
-
return {
|
|
832
|
-
type: "unexpected_visual_change",
|
|
833
|
-
severity: "WARN",
|
|
834
|
-
message: `Large visual change detected after ${action}`,
|
|
835
|
-
metrics: comparison.metrics
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return null;
|
|
840
|
-
}
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
845
|
-
// EXPORTS
|
|
846
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
847
|
-
|
|
848
|
-
module.exports = {
|
|
849
|
-
VisualRegressionEngine,
|
|
850
|
-
createRealityModeHooks,
|
|
851
|
-
DEFAULT_CONFIG
|
|
852
|
-
};
|