@bughunters/vision 1.0.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/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # 🐞 BugHunters Vision
2
+
3
+ **InfoSec-friendly AI Visual Testing for Playwright.**
4
+
5
+ Stop fighting 1-pixel false positives. BugHunters Vision uses Multimodal AI (Claude) to evaluate visual regression tests like a human QA — while keeping all your screenshots 100% local.
6
+
7
+ > Images are **never stored on our servers**. They are Base64-encoded, evaluated in-memory for a single request, and immediately discarded.
8
+
9
+ ---
10
+
11
+ ## How it works
12
+
13
+ 1. A test takes a screenshot of the current page.
14
+ 2. BugHunters Vision looks for a baseline in `./bhv-snapshots/`.
15
+ 3. **No baseline** → saves it as the new baseline, test passes ✅
16
+ 4. **Baseline found → Fast Pixel Match** → pixel-perfect = instant PASS, no AI call needed ⚡
17
+ 5. **Pixels differ** → both images are sent (Base64, HTTPS) to the BugHunters Vision API.
18
+ 6. The API calls **Anthropic Claude** (multimodal vision) to evaluate the diff like a human QA.
19
+ 7. Returns `{ status: "PASS" | "FAIL", reason: "..." }` — test passes or fails accordingly.
20
+ 8. After all tests finish, a **standalone HTML report** is generated locally.
21
+
22
+ ---
23
+
24
+ ## Quick start
25
+
26
+ ### 1. Install
27
+
28
+ ```bash
29
+ npm install @bughunters/vision --save-dev
30
+ ```
31
+
32
+ ### 2. Set your API token
33
+
34
+ Get a token at [bughunters.vision](https://bughunters.vision).
35
+
36
+ ```bash
37
+ # .env or your CI secrets manager
38
+ BUGHUNTERS_VISION_TOKEN=bhv_your_token_here
39
+ ```
40
+
41
+ ### 3. Configure `playwright.config.ts`
42
+
43
+ ```typescript
44
+ import { defineConfig, devices } from '@playwright/test';
45
+ import type { VisionFixtureOptions } from '@bughunters/vision';
46
+
47
+ export default defineConfig<VisionFixtureOptions>({
48
+ testDir: './tests',
49
+ reporter: [
50
+ ['list'],
51
+ ['@bughunters/vision/reporter', { snapshotsDir: './bhv-snapshots', reportDir: './bhv-report' }],
52
+ ],
53
+ use: {
54
+ bvMode: 'ai', // 'ai' | 'strict' | 'off'
55
+ },
56
+ projects: [
57
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
58
+ ],
59
+ });
60
+ ```
61
+
62
+ ### 4. Write a visual test
63
+
64
+ ```typescript
65
+ import { test } from '@bughunters/vision';
66
+
67
+ test('homepage looks correct', async ({ page, visionCheck }) => {
68
+ await page.goto('https://example.com');
69
+ await page.waitForLoadState('networkidle');
70
+ await visionCheck('homepage');
71
+ });
72
+ ```
73
+
74
+ You can pass an optional AI hint as the second argument:
75
+
76
+ ```typescript
77
+ await visionCheck('dashboard', 'Ignore the live timestamp in the top-right corner.');
78
+ ```
79
+
80
+ ### 5. Run your tests
81
+
82
+ ```bash
83
+ # First run — baselines are created automatically, all tests pass
84
+ BUGHUNTERS_VISION_TOKEN=bhv_... npx playwright test
85
+
86
+ # Second run — Fast Pixel Match first; AI only called if pixels differ
87
+ npx playwright test
88
+
89
+ # Force-update all baselines after an intentional UI change
90
+ BHV_UPDATE_BASELINE=true npx playwright test
91
+ ```
92
+
93
+ ### 6. View the HTML report
94
+
95
+ ```bash
96
+ open bhv-report/index.html # macOS
97
+ start bhv-report/index.html # Windows
98
+ xdg-open bhv-report/index.html # Linux
99
+ ```
100
+
101
+ The report includes side-by-side Baseline / Current / Diff views with full-screen zoom, Light/Dark theme toggle, and per-test AI verdicts.
102
+
103
+ ---
104
+
105
+ ## Configuration reference
106
+
107
+ All options can be set in `playwright.config.ts` under `use:`, or overridden via environment variables.
108
+
109
+ | Option in `use:` | Environment variable | Default | Description |
110
+ |---|---|---|---|
111
+ | `bvMode` | `BHV_MODE` | `'ai'` | Comparison mode: `ai` · `strict` · `off` |
112
+ | `bvSnapshotsDir` | `BHV_SNAPSHOTS_DIR` | `'./bhv-snapshots'` | Where to store PNG snapshots |
113
+ | `bvUpdateBaseline` | `BHV_UPDATE_BASELINE` | `false` | Force-overwrite all baselines on this run |
114
+ | `bvApiUrl` | `BUGHUNTERS_VISION_API_URL` | *(production)* | Override API endpoint (e.g. for self-hosted) |
115
+ | `bvToken` | `BUGHUNTERS_VISION_TOKEN` | — | Your API token — always use env var, never commit |
116
+
117
+ ### Comparison modes
118
+
119
+ | Mode | Behaviour |
120
+ |---|---|
121
+ | `ai` *(default)* | Fast Pixel Match first → AI evaluation only if pixels differ |
122
+ | `strict` | Fail immediately on any pixel difference — no AI call |
123
+ | `off` | Skip all visual checks — every test passes instantly |
124
+
125
+ ```bash
126
+ BHV_MODE=strict npx playwright test
127
+ BHV_MODE=off npx playwright test
128
+ ```
129
+
130
+ ---
131
+
132
+ ## HTML Report
133
+
134
+ After every run a report is generated at `bhv-report/index.html`. No upload, no cloud — everything is local.
135
+
136
+ - **Summary bar** — total / passed / failed / baseline counts
137
+ - **Baseline / Current / Diff** tab view per test — click any image for fullscreen zoom
138
+ - **AI verdict** with human-readable explanation for every evaluated test
139
+ - **Low Confidence FAIL** — amber tag when the AI detects a likely intentional change (A/B test, loading state) that a human should review
140
+ - **Method badge** — `AI` · `PIXEL` · `NEW` — shows how each test was evaluated
141
+ - **Light / Dark / System** theme toggle, persisted in `localStorage`
142
+
143
+ ---
144
+
145
+ ## Security model
146
+
147
+ - **Zero cloud storage** — images live only in RAM for the duration of one API request and are never written to disk on our side.
148
+ - **Local baselines** — all baseline PNGs stay on your machine or CI runner. We never see your UI.
149
+ - **Token isolation** — each customer token is scoped to their own credit bucket. Our Anthropic key never leaves our infrastructure.
150
+ - **InfoSec approved** — suitable for banking, healthcare, and enterprise environments where screenshots cannot leave the perimeter.
151
+
152
+ ---
153
+
154
+ ## Native Playwright integration
155
+
156
+ BugHunters Vision results appear directly in the **native Playwright HTML report** as annotations and attachments:
157
+
158
+ - `BugHunters Vision: PASS/FAIL/ERROR — <reason>` annotation on each test
159
+ - `bhv-baseline` and `bhv-current` screenshot attachments in the Playwright report
160
+
161
+ ---
162
+
163
+ ## Links
164
+
165
+ - 🌐 [bughunters.vision](https://bughunters.vision) — get your token
166
+ - 📄 [Changelog](https://github.com/bughunters/vision/releases)
167
+ - 🐛 [Report an issue](https://github.com/bughunters/vision/issues)
@@ -0,0 +1,16 @@
1
+ import type { Page, TestInfo } from '@playwright/test';
2
+ import type { VisionMode } from './types';
3
+ export interface RunVisionCheckOptions {
4
+ page: Page;
5
+ name: string;
6
+ prompt?: string;
7
+ apiUrl: string;
8
+ mode: VisionMode;
9
+ snapshotsDir: string;
10
+ updateBaseline: boolean;
11
+ token?: string;
12
+ testInfo?: TestInfo;
13
+ }
14
+ export declare function getLastRemainingBalance(): string | null;
15
+ export declare function runVisionCheck(opts: RunVisionCheckOptions): Promise<void>;
16
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAKD,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;AA+GD,wBAAsB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4I/E"}
package/dist/check.js ADDED
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getLastRemainingBalance = getLastRemainingBalance;
37
+ exports.runVisionCheck = runVisionCheck;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const test_1 = require("@playwright/test");
41
+ const pixel_match_1 = require("./pixel-match");
42
+ const results_1 = require("./results");
43
+ // Module-level balance tracker — updated after each AI call, read by reporter.onEnd()
44
+ let _lastRemainingBalance = null;
45
+ function getLastRemainingBalance() {
46
+ return _lastRemainingBalance;
47
+ }
48
+ async function callVisionApi(imageBase, imageCurrent, prompt, apiUrl, token) {
49
+ const headers = { 'Content-Type': 'application/json' };
50
+ if (token) {
51
+ headers['Authorization'] = `Bearer ${token}`;
52
+ }
53
+ // 75-second fetch timeout — gives Claude Vision enough time to respond while
54
+ // still failing before the 90-second Playwright test timeout.
55
+ const controller = new AbortController();
56
+ const timeoutId = setTimeout(() => controller.abort(), 75_000);
57
+ let response;
58
+ try {
59
+ response = await fetch(`${apiUrl}/api/compare`, {
60
+ method: 'POST',
61
+ headers,
62
+ body: JSON.stringify({ imageBase, imageCurrent, prompt }),
63
+ signal: controller.signal,
64
+ });
65
+ }
66
+ catch (err) {
67
+ if (err?.name === 'AbortError') {
68
+ throw new Error('[BugHunters Vision] API request timed out after 75 s — the server did not respond in time.');
69
+ }
70
+ throw err;
71
+ }
72
+ finally {
73
+ clearTimeout(timeoutId);
74
+ }
75
+ // Graceful 402: credits exhausted — return FAIL, never throw
76
+ if (response.status === 402) {
77
+ const body = await response.json().catch(() => ({
78
+ reason: 'BugHunters Vision API Limit Reached. Please top up your account.',
79
+ }));
80
+ return {
81
+ status: 'FAIL',
82
+ reason: body.reason ?? 'BugHunters Vision API Limit Reached. Please top up your account.',
83
+ };
84
+ }
85
+ // Graceful 503: AI temporarily overloaded — return ERROR, never throw or fail the test
86
+ if (response.status === 503) {
87
+ const body = await response.json().catch(() => ({
88
+ error: 'Claude AI is temporarily overloaded. Please retry in a moment.',
89
+ }));
90
+ return {
91
+ status: 'ERROR',
92
+ reason: body.error ?? 'Claude AI is temporarily overloaded. Please retry in a moment.',
93
+ };
94
+ }
95
+ if (!response.ok) {
96
+ const text = await response.text();
97
+ throw new Error(`[BugHunters Vision] API error (${response.status}): ${text}`);
98
+ }
99
+ // Track remaining balance from response header
100
+ const remaining = response.headers.get('X-RateLimit-Remaining');
101
+ if (remaining !== null) {
102
+ _lastRemainingBalance = remaining;
103
+ }
104
+ return response.json();
105
+ }
106
+ function humanReadableApiError(raw) {
107
+ if (raw.includes('timed out')) {
108
+ return 'Request timed out — the BugHunters Vision API did not respond in time. Try again or check the service status.';
109
+ }
110
+ const match = raw.match(/API error \((\d+)\)/);
111
+ if (match) {
112
+ switch (parseInt(match[1], 10)) {
113
+ case 401: return 'Unauthorized — missing or invalid API token. Set the BUGHUNTERS_VISION_TOKEN environment variable.';
114
+ case 403: return 'Access forbidden — your token does not have permission for this operation.';
115
+ case 429: return 'Rate limited — too many requests. Please wait a moment before retrying.';
116
+ case 500: return 'Internal server error — the BugHunters Vision API encountered an unexpected error.';
117
+ case 502: return 'AI evaluation failed — the server could not process the request. Try again.';
118
+ case 503: return 'Claude AI is temporarily overloaded. Please wait a moment and try again.';
119
+ default: return `API returned HTTP ${match[1]} — check your configuration and token.`;
120
+ }
121
+ }
122
+ return raw;
123
+ }
124
+ async function pushToPlaywright(testInfo, status, reason, resolvedDir, baselineFile, currentFile) {
125
+ if (!testInfo)
126
+ return;
127
+ testInfo.annotations.push({ type: 'BugHunters Vision', description: `${status}: ${reason}` });
128
+ if (baselineFile) {
129
+ const p = path.join(resolvedDir, baselineFile);
130
+ if (fs.existsSync(p))
131
+ await testInfo.attach('bhv-baseline', { path: p, contentType: 'image/png' });
132
+ }
133
+ if (currentFile) {
134
+ const p = path.join(resolvedDir, currentFile);
135
+ if (fs.existsSync(p))
136
+ await testInfo.attach('bhv-current', { path: p, contentType: 'image/png' });
137
+ }
138
+ }
139
+ async function runVisionCheck(opts) {
140
+ const { page, name, prompt = 'Check for any visual regressions.', apiUrl, mode, snapshotsDir, updateBaseline, token, testInfo, } = opts;
141
+ // Mode: off → skip all visual testing immediately
142
+ if (mode === 'off') {
143
+ console.log(`\n⏭ [BugHunters Vision] "${name}" — skipped (BHV_MODE=off)\n`);
144
+ return;
145
+ }
146
+ const resolvedDir = path.resolve(process.cwd(), snapshotsDir);
147
+ fs.mkdirSync(resolvedDir, { recursive: true });
148
+ const baselineFile = path.join(resolvedDir, `${name}.baseline.png`);
149
+ const currentBuffer = await page.screenshot({ fullPage: false, type: 'png' });
150
+ if (!fs.existsSync(baselineFile) || updateBaseline) {
151
+ // First run — save as baseline
152
+ fs.writeFileSync(baselineFile, currentBuffer);
153
+ console.log(`\n📸 [BugHunters Vision] Baseline saved: ${baselineFile}`);
154
+ console.log(' ✅ Test PASSED (baseline created — no previous reference exists)\n');
155
+ (0, results_1.appendResult)(resolvedDir, {
156
+ testName: name,
157
+ status: 'BASELINE_CREATED',
158
+ reason: 'First run detected — screenshot saved as the new baseline.',
159
+ method: null,
160
+ baselineFile: `${name}.baseline.png`,
161
+ currentFile: null,
162
+ timestamp: new Date().toISOString(),
163
+ });
164
+ await pushToPlaywright(testInfo, 'BASELINE_CREATED', 'First run — screenshot saved as the new baseline.', resolvedDir, `${name}.baseline.png`, null);
165
+ return;
166
+ }
167
+ // Baseline exists — run Fast Pixel Match first to save AI credits
168
+ const baselineBuffer = fs.readFileSync(baselineFile);
169
+ const diffPixels = (0, pixel_match_1.fastPixelMatch)(baselineBuffer, currentBuffer);
170
+ const currentFileName = `${name}.current.png`;
171
+ fs.writeFileSync(path.join(resolvedDir, currentFileName), currentBuffer);
172
+ if (diffPixels === 0) {
173
+ // ✅ Pixel-perfect match — skip AI call entirely
174
+ console.log(`\n⚡ [BugHunters Vision] "${name}" — pixel-perfect match. Skipping AI call.`);
175
+ console.log(' ✅ Test PASSED (Fast Pixel Match)\n');
176
+ (0, results_1.appendResult)(resolvedDir, {
177
+ testName: name,
178
+ status: 'PASS',
179
+ reason: 'Identical (Fast Pixel Match) — pixel-perfect match, no AI evaluation needed.',
180
+ method: 'FAST_PIXEL_MATCH',
181
+ baselineFile: `${name}.baseline.png`,
182
+ currentFile: currentFileName,
183
+ timestamp: new Date().toISOString(),
184
+ });
185
+ await pushToPlaywright(testInfo, 'PASS', 'Pixel-perfect match — no AI evaluation needed.', resolvedDir, `${name}.baseline.png`, currentFileName);
186
+ return;
187
+ }
188
+ // Pixels differ — behaviour depends on mode
189
+ const diffLabel = diffPixels === -1 ? 'dimension mismatch' : `${diffPixels} pixel(s) differ`;
190
+ if (mode === 'strict') {
191
+ // strict: fail immediately without calling AI
192
+ console.log(`\n❌ [BugHunters Vision] "${name}" — ${diffLabel}. FAILED (strict mode — no AI call).\n`);
193
+ (0, results_1.appendResult)(resolvedDir, {
194
+ testName: name,
195
+ status: 'FAIL',
196
+ reason: `Pixel mismatch detected (${diffLabel}). Strict mode — AI evaluation disabled.`,
197
+ method: 'FAST_PIXEL_MATCH',
198
+ baselineFile: `${name}.baseline.png`,
199
+ currentFile: currentFileName,
200
+ timestamp: new Date().toISOString(),
201
+ });
202
+ await pushToPlaywright(testInfo, 'FAIL', `Pixel mismatch detected (${diffLabel}). Strict mode.`, resolvedDir, `${name}.baseline.png`, currentFileName);
203
+ (0, test_1.expect)(false, `[BugHunters Vision strict] Pixel mismatch: ${diffLabel}`).toBe(true);
204
+ return;
205
+ }
206
+ // ai mode — escalate to AI for intelligent evaluation
207
+ console.log(`\n🔍 [BugHunters Vision] "${name}" — ${diffLabel}. Sending to AI…`);
208
+ let result;
209
+ try {
210
+ result = await callVisionApi(baselineBuffer.toString('base64'), currentBuffer.toString('base64'), prompt, apiUrl, token);
211
+ }
212
+ catch (err) {
213
+ const reason = humanReadableApiError(err instanceof Error ? err.message : String(err));
214
+ console.log(`\n❌ [BugHunters Vision] ${reason}\n`);
215
+ (0, results_1.appendResult)(resolvedDir, {
216
+ testName: name,
217
+ status: 'FAIL',
218
+ reason,
219
+ method: 'AI',
220
+ baselineFile: `${name}.baseline.png`,
221
+ currentFile: currentFileName,
222
+ timestamp: new Date().toISOString(),
223
+ });
224
+ await pushToPlaywright(testInfo, 'FAIL', reason, resolvedDir, `${name}.baseline.png`, currentFileName);
225
+ throw err;
226
+ }
227
+ // ERROR: AI temporarily unavailable — record but do NOT fail the Playwright test
228
+ if (result.status === 'ERROR') {
229
+ console.log(`\n⚠️ [BugHunters Vision] "${name}" — AI unavailable: ${result.reason}\n`);
230
+ (0, results_1.appendResult)(resolvedDir, {
231
+ testName: name,
232
+ status: 'ERROR',
233
+ reason: result.reason,
234
+ method: 'AI',
235
+ baselineFile: `${name}.baseline.png`,
236
+ currentFile: currentFileName,
237
+ timestamp: new Date().toISOString(),
238
+ });
239
+ await pushToPlaywright(testInfo, 'ERROR', result.reason, resolvedDir, `${name}.baseline.png`, currentFileName);
240
+ return; // test stays green
241
+ }
242
+ const icon = result.status === 'PASS' ? '✅' : '❌';
243
+ console.log(`${icon} [BugHunters Vision] ${result.status}: ${result.reason}\n`);
244
+ (0, results_1.appendResult)(resolvedDir, {
245
+ testName: name,
246
+ status: result.status,
247
+ reason: result.reason,
248
+ method: 'AI',
249
+ baselineFile: `${name}.baseline.png`,
250
+ currentFile: currentFileName,
251
+ timestamp: new Date().toISOString(),
252
+ });
253
+ await pushToPlaywright(testInfo, result.status, result.reason, resolvedDir, `${name}.baseline.png`, currentFileName);
254
+ (0, test_1.expect)(result.status, `Visual regression detected: ${result.reason}`).toBe('PASS');
255
+ }
@@ -0,0 +1,21 @@
1
+ import { expect } from '@playwright/test';
2
+ import type { VisionMode } from './types';
3
+ export interface VisionFixtureOptions {
4
+ /** BugHunters Vision API URL (default: https://bugvision-backend.vercel.app) */
5
+ bvApiUrl: string;
6
+ /** Comparison mode: ai | strict | off (default: ai) */
7
+ bvMode: VisionMode;
8
+ /** Where to store baseline + current screenshots (default: ./bhv-snapshots) */
9
+ bvSnapshotsDir: string;
10
+ /** Force-overwrite all baselines on this run (default: false) */
11
+ bvUpdateBaseline: boolean;
12
+ /** BugHunters Vision API token (env: BUGHUNTERS_VISION_TOKEN) */
13
+ bvToken: string;
14
+ }
15
+ type VisionFixtures = {
16
+ /** Run a visual snapshot check for the current page state */
17
+ visionCheck: (name: string, prompt?: string) => Promise<void>;
18
+ };
19
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & VisionFixtures & VisionFixtureOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
20
+ export { expect };
21
+ //# sourceMappingURL=fixture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../src/fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAK1C,MAAM,WAAW,oBAAoB;IACnC,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,MAAM,EAAE,UAAU,CAAC;IACnB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,KAAK,cAAc,GAAG;IACpB,6DAA6D;IAC7D,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D,CAAC;AAEF,eAAO,MAAM,IAAI,qRAoBf,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.expect = exports.test = void 0;
4
+ const test_1 = require("@playwright/test");
5
+ Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_1.expect; } });
6
+ const check_1 = require("./check");
7
+ const PRODUCTION_API_URL = 'https://bugvision-backend.vercel.app';
8
+ exports.test = test_1.test.extend({
9
+ bvApiUrl: [process.env.BUGHUNTERS_VISION_API_URL ?? process.env.BHV_API_URL ?? PRODUCTION_API_URL, { option: true }],
10
+ bvMode: [(process.env.BHV_MODE ?? 'ai'), { option: true }],
11
+ bvSnapshotsDir: [process.env.BHV_SNAPSHOTS_DIR ?? './bhv-snapshots', { option: true }],
12
+ bvUpdateBaseline: [process.env.BHV_UPDATE_BASELINE === 'true', { option: true }],
13
+ bvToken: [process.env.BUGHUNTERS_VISION_TOKEN ?? '', { option: true }],
14
+ visionCheck: async ({ page, bvApiUrl, bvMode, bvSnapshotsDir, bvUpdateBaseline, bvToken }, use, testInfo) => {
15
+ await use((name, prompt) => (0, check_1.runVisionCheck)({
16
+ page, name, prompt,
17
+ apiUrl: bvApiUrl,
18
+ mode: bvMode,
19
+ snapshotsDir: bvSnapshotsDir,
20
+ updateBaseline: bvUpdateBaseline,
21
+ token: bvToken || undefined,
22
+ testInfo,
23
+ }));
24
+ },
25
+ });
@@ -0,0 +1,6 @@
1
+ export { test, expect } from './fixture';
2
+ export type { VisionFixtureOptions } from './fixture';
3
+ export type { VisionMode, TestResult, Meta } from './types';
4
+ export { BugHuntersVisionReporter } from './reporter';
5
+ export type { BugHuntersVisionReporterOptions } from './reporter';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtD,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACtD,YAAY,EAAE,+BAA+B,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BugHuntersVisionReporter = exports.expect = exports.test = void 0;
4
+ var fixture_1 = require("./fixture");
5
+ Object.defineProperty(exports, "test", { enumerable: true, get: function () { return fixture_1.test; } });
6
+ Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return fixture_1.expect; } });
7
+ var reporter_1 = require("./reporter");
8
+ Object.defineProperty(exports, "BugHuntersVisionReporter", { enumerable: true, get: function () { return reporter_1.BugHuntersVisionReporter; } });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Fast local pixel comparison — avoids AI API call when images are identical.
3
+ * Returns the number of differing pixels, or -1 if image dimensions differ.
4
+ */
5
+ export declare function fastPixelMatch(baselineBuffer: Buffer, currentBuffer: Buffer): number;
6
+ //# sourceMappingURL=pixel-match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pixel-match.d.ts","sourceRoot":"","sources":["../src/pixel-match.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CASpF"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fastPixelMatch = fastPixelMatch;
7
+ const pngjs_1 = require("pngjs");
8
+ const pixelmatch_1 = __importDefault(require("pixelmatch"));
9
+ /**
10
+ * Fast local pixel comparison — avoids AI API call when images are identical.
11
+ * Returns the number of differing pixels, or -1 if image dimensions differ.
12
+ */
13
+ function fastPixelMatch(baselineBuffer, currentBuffer) {
14
+ const imgA = pngjs_1.PNG.sync.read(baselineBuffer);
15
+ const imgB = pngjs_1.PNG.sync.read(currentBuffer);
16
+ if (imgA.width !== imgB.width || imgA.height !== imgB.height) {
17
+ return -1; // dimension mismatch — let AI handle it
18
+ }
19
+ return (0, pixelmatch_1.default)(imgA.data, imgB.data, null, imgA.width, imgA.height);
20
+ }
@@ -0,0 +1,15 @@
1
+ import type { Reporter } from '@playwright/test/reporter';
2
+ export interface BugHuntersVisionReporterOptions {
3
+ snapshotsDir?: string;
4
+ reportDir?: string;
5
+ }
6
+ declare class BugHuntersVisionReporter implements Reporter {
7
+ private snapshotsDir;
8
+ private reportDir;
9
+ constructor(options?: BugHuntersVisionReporterOptions);
10
+ onBegin(): void;
11
+ onEnd(): void;
12
+ }
13
+ export default BugHuntersVisionReporter;
14
+ export { BugHuntersVisionReporter };
15
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAI1D,MAAM,WAAW,+BAA+B;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA87BD,cAAM,wBAAyB,YAAW,QAAQ;IAChD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,CAAC,EAAE,+BAA+B;IAKrD,OAAO,IAAI,IAAI;IAKf,KAAK,IAAI,IAAI;CA8Cd;AAED,eAAe,wBAAwB,CAAC;AACxC,OAAO,EAAE,wBAAwB,EAAE,CAAC"}