@bughunters/vision 1.0.3 → 1.0.4
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 +32 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +38 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,6 +86,25 @@ await vision.check(page, 'dashboard', 'Ignore the live timestamp in the top-righ
|
|
|
86
86
|
await vision.check(page.locator('#sidebar'), 'sidebar');
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Multi-step visual checks
|
|
90
|
+
|
|
91
|
+
Call `vision.check()` multiple times within one test — each step gets its own baseline file, its own row in the HTML report, and its own annotation in the native Playwright report.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
test('checkout flow looks correct', async ({ page }) => {
|
|
95
|
+
await page.goto('https://shop.example.com/cart');
|
|
96
|
+
await vision.check(page, 'cart-page', 'Cart summary should be visible.');
|
|
97
|
+
|
|
98
|
+
await page.click('#checkout-btn');
|
|
99
|
+
await vision.check(page, 'shipping-form', 'Shipping form should be shown.');
|
|
100
|
+
|
|
101
|
+
await page.click('#next-btn');
|
|
102
|
+
await vision.check(page, 'payment-page', 'Payment options should be visible.');
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Each step produces isolated files (`<test-title>--<step-name>.baseline.png`) so steps from different tests never collide, even if you reuse step names like `"header"` across multiple tests.
|
|
107
|
+
|
|
89
108
|
### 5. Run your tests
|
|
90
109
|
|
|
91
110
|
```bash
|
|
@@ -111,6 +130,18 @@ The report includes side-by-side Baseline / Current / Diff views with full-scree
|
|
|
111
130
|
|
|
112
131
|
---
|
|
113
132
|
|
|
133
|
+
## IDE & VS Code integration
|
|
134
|
+
|
|
135
|
+
BugHunters Vision is fully compatible with the **VS Code Playwright extension** and **Playwright UI mode**.
|
|
136
|
+
|
|
137
|
+
- Tests appear normally in the VS Code Test Explorer sidebar
|
|
138
|
+
- `npx playwright test --list` works as expected
|
|
139
|
+
- `npx playwright test --ui` works as expected
|
|
140
|
+
|
|
141
|
+
This works because `vision` is a standalone object — not a custom fixture — so the native `import { test } from '@playwright/test'` is preserved.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
114
145
|
## Configuration reference
|
|
115
146
|
|
|
116
147
|
Options can be set in `playwright.config.ts` under `use:` **or** via environment variables. The `use:` block takes precedence; env vars are the fallback.
|
|
@@ -144,10 +175,9 @@ BHV_MODE=off npx playwright test
|
|
|
144
175
|
|
|
145
176
|
After every run a report is generated at `bhv-report/index.html`. No upload, no cloud — everything is local.
|
|
146
177
|
|
|
147
|
-
- **Summary bar** — total / passed / failed / baseline counts
|
|
178
|
+
- **Summary bar** — total / passed / failed / baseline / error counts
|
|
148
179
|
- **Baseline / Current / Diff** tab view per test — click any image for fullscreen zoom
|
|
149
180
|
- **AI verdict** with human-readable explanation for every evaluated test
|
|
150
|
-
- **Low Confidence FAIL** — amber tag when the AI detects a likely intentional change (A/B test, loading state) that a human should review
|
|
151
181
|
- **Method badge** — `AI` · `PIXEL` · `NEW` — shows how each test was evaluated
|
|
152
182
|
- **Light / Dark / System** theme toggle, persisted in `localStorage`
|
|
153
183
|
|
package/dist/check.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAOhE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAOhE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;AAoHD,wBAAsB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4J/E"}
|
package/dist/check.js
CHANGED
|
@@ -122,19 +122,22 @@ function humanReadableApiError(raw) {
|
|
|
122
122
|
}
|
|
123
123
|
return raw;
|
|
124
124
|
}
|
|
125
|
-
|
|
125
|
+
function sanitize(s) {
|
|
126
|
+
return s.replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
127
|
+
}
|
|
128
|
+
async function pushToPlaywright(testInfo, stepName, status, reason, resolvedDir, baselineFile, currentFile) {
|
|
126
129
|
if (!testInfo)
|
|
127
130
|
return;
|
|
128
|
-
testInfo.annotations.push({ type:
|
|
131
|
+
testInfo.annotations.push({ type: `BugHunters Vision - ${stepName}`, description: `${status}: ${reason}` });
|
|
129
132
|
if (baselineFile) {
|
|
130
133
|
const p = path.join(resolvedDir, baselineFile);
|
|
131
134
|
if (fs.existsSync(p))
|
|
132
|
-
await testInfo.attach(
|
|
135
|
+
await testInfo.attach(`bhv-baseline [${stepName}]`, { path: p, contentType: 'image/png' });
|
|
133
136
|
}
|
|
134
137
|
if (currentFile) {
|
|
135
138
|
const p = path.join(resolvedDir, currentFile);
|
|
136
139
|
if (fs.existsSync(p))
|
|
137
|
-
await testInfo.attach(
|
|
140
|
+
await testInfo.attach(`bhv-current [${stepName}]`, { path: p, contentType: 'image/png' });
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
async function runVisionCheck(opts) {
|
|
@@ -158,44 +161,51 @@ async function runVisionCheck(opts) {
|
|
|
158
161
|
}
|
|
159
162
|
const resolvedDir = path.resolve(process.cwd(), snapshotsDir);
|
|
160
163
|
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
161
|
-
|
|
164
|
+
// Build unique, collision-free filenames scoped to both the test title and the step name.
|
|
165
|
+
// Format: <safe-test-title>--<safe-step-name>.baseline.png
|
|
166
|
+
const safeTitle = testInfo?.title ? sanitize(testInfo.title) : 'unknown';
|
|
167
|
+
const safeName = sanitize(name);
|
|
168
|
+
const filePrefix = `${safeTitle}--${safeName}`;
|
|
169
|
+
// Human-readable label used in the HTML report: "Test title › step name"
|
|
170
|
+
const testName = testInfo?.title ? `${testInfo.title} › ${name}` : name;
|
|
171
|
+
const baselinePath = path.join(resolvedDir, `${filePrefix}.baseline.png`);
|
|
162
172
|
const currentBuffer = await target.screenshot();
|
|
163
|
-
if (!fs.existsSync(
|
|
173
|
+
if (!fs.existsSync(baselinePath) || updateBaseline) {
|
|
164
174
|
// First run — save as baseline
|
|
165
|
-
fs.writeFileSync(
|
|
166
|
-
console.log(`\n📸 [BugHunters Vision] Baseline saved: ${
|
|
175
|
+
fs.writeFileSync(baselinePath, currentBuffer);
|
|
176
|
+
console.log(`\n📸 [BugHunters Vision] Baseline saved: ${baselinePath}`);
|
|
167
177
|
console.log(' ✅ Test PASSED (baseline created — no previous reference exists)\n');
|
|
168
178
|
(0, results_1.appendResult)(resolvedDir, {
|
|
169
|
-
testName
|
|
179
|
+
testName,
|
|
170
180
|
status: 'BASELINE_CREATED',
|
|
171
181
|
reason: 'First run detected — screenshot saved as the new baseline.',
|
|
172
182
|
method: null,
|
|
173
|
-
baselineFile: `${
|
|
183
|
+
baselineFile: `${filePrefix}.baseline.png`,
|
|
174
184
|
currentFile: null,
|
|
175
185
|
timestamp: new Date().toISOString(),
|
|
176
186
|
});
|
|
177
|
-
await pushToPlaywright(testInfo, 'BASELINE_CREATED', 'First run — screenshot saved as the new baseline.', resolvedDir, `${
|
|
187
|
+
await pushToPlaywright(testInfo, name, 'BASELINE_CREATED', 'First run — screenshot saved as the new baseline.', resolvedDir, `${filePrefix}.baseline.png`, null);
|
|
178
188
|
return;
|
|
179
189
|
}
|
|
180
190
|
// Baseline exists — run Fast Pixel Match first to save AI credits
|
|
181
|
-
const baselineBuffer = fs.readFileSync(
|
|
191
|
+
const baselineBuffer = fs.readFileSync(baselinePath);
|
|
182
192
|
const diffPixels = (0, pixel_match_1.fastPixelMatch)(baselineBuffer, currentBuffer);
|
|
183
|
-
const currentFileName = `${
|
|
193
|
+
const currentFileName = `${filePrefix}.current.png`;
|
|
184
194
|
fs.writeFileSync(path.join(resolvedDir, currentFileName), currentBuffer);
|
|
185
195
|
if (diffPixels === 0) {
|
|
186
196
|
// ✅ Pixel-perfect match — skip AI call entirely
|
|
187
197
|
console.log(`\n⚡ [BugHunters Vision] "${name}" — pixel-perfect match. Skipping AI call.`);
|
|
188
198
|
console.log(' ✅ Test PASSED (Fast Pixel Match)\n');
|
|
189
199
|
(0, results_1.appendResult)(resolvedDir, {
|
|
190
|
-
testName
|
|
200
|
+
testName,
|
|
191
201
|
status: 'PASS',
|
|
192
202
|
reason: 'Identical (Fast Pixel Match) — pixel-perfect match, no AI evaluation needed.',
|
|
193
203
|
method: 'FAST_PIXEL_MATCH',
|
|
194
|
-
baselineFile: `${
|
|
204
|
+
baselineFile: `${filePrefix}.baseline.png`,
|
|
195
205
|
currentFile: currentFileName,
|
|
196
206
|
timestamp: new Date().toISOString(),
|
|
197
207
|
});
|
|
198
|
-
await pushToPlaywright(testInfo, 'PASS', 'Pixel-perfect match — no AI evaluation needed.', resolvedDir, `${
|
|
208
|
+
await pushToPlaywright(testInfo, name, 'PASS', 'Pixel-perfect match — no AI evaluation needed.', resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
199
209
|
return;
|
|
200
210
|
}
|
|
201
211
|
// Pixels differ — behaviour depends on mode
|
|
@@ -204,15 +214,15 @@ async function runVisionCheck(opts) {
|
|
|
204
214
|
// strict: fail immediately without calling AI
|
|
205
215
|
console.log(`\n❌ [BugHunters Vision] "${name}" — ${diffLabel}. FAILED (strict mode — no AI call).\n`);
|
|
206
216
|
(0, results_1.appendResult)(resolvedDir, {
|
|
207
|
-
testName
|
|
217
|
+
testName,
|
|
208
218
|
status: 'FAIL',
|
|
209
219
|
reason: `Pixel mismatch detected (${diffLabel}). Strict mode — AI evaluation disabled.`,
|
|
210
220
|
method: 'FAST_PIXEL_MATCH',
|
|
211
|
-
baselineFile: `${
|
|
221
|
+
baselineFile: `${filePrefix}.baseline.png`,
|
|
212
222
|
currentFile: currentFileName,
|
|
213
223
|
timestamp: new Date().toISOString(),
|
|
214
224
|
});
|
|
215
|
-
await pushToPlaywright(testInfo, 'FAIL', `Pixel mismatch detected (${diffLabel}). Strict mode.`, resolvedDir, `${
|
|
225
|
+
await pushToPlaywright(testInfo, name, 'FAIL', `Pixel mismatch detected (${diffLabel}). Strict mode.`, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
216
226
|
(0, test_1.expect)(false, `[BugHunters Vision strict] Pixel mismatch: ${diffLabel}`).toBe(true);
|
|
217
227
|
return;
|
|
218
228
|
}
|
|
@@ -226,43 +236,43 @@ async function runVisionCheck(opts) {
|
|
|
226
236
|
const reason = humanReadableApiError(err instanceof Error ? err.message : String(err));
|
|
227
237
|
console.log(`\n❌ [BugHunters Vision] ${reason}\n`);
|
|
228
238
|
(0, results_1.appendResult)(resolvedDir, {
|
|
229
|
-
testName
|
|
239
|
+
testName,
|
|
230
240
|
status: 'FAIL',
|
|
231
241
|
reason,
|
|
232
242
|
method: 'AI',
|
|
233
|
-
baselineFile: `${
|
|
243
|
+
baselineFile: `${filePrefix}.baseline.png`,
|
|
234
244
|
currentFile: currentFileName,
|
|
235
245
|
timestamp: new Date().toISOString(),
|
|
236
246
|
});
|
|
237
|
-
await pushToPlaywright(testInfo, 'FAIL', reason, resolvedDir, `${
|
|
247
|
+
await pushToPlaywright(testInfo, name, 'FAIL', reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
238
248
|
throw err;
|
|
239
249
|
}
|
|
240
250
|
// ERROR: AI temporarily unavailable — record but do NOT fail the Playwright test
|
|
241
251
|
if (result.status === 'ERROR') {
|
|
242
252
|
console.log(`\n⚠️ [BugHunters Vision] "${name}" — AI unavailable: ${result.reason}\n`);
|
|
243
253
|
(0, results_1.appendResult)(resolvedDir, {
|
|
244
|
-
testName
|
|
254
|
+
testName,
|
|
245
255
|
status: 'ERROR',
|
|
246
256
|
reason: result.reason,
|
|
247
257
|
method: 'AI',
|
|
248
|
-
baselineFile: `${
|
|
258
|
+
baselineFile: `${filePrefix}.baseline.png`,
|
|
249
259
|
currentFile: currentFileName,
|
|
250
260
|
timestamp: new Date().toISOString(),
|
|
251
261
|
});
|
|
252
|
-
await pushToPlaywright(testInfo, 'ERROR', result.reason, resolvedDir, `${
|
|
262
|
+
await pushToPlaywright(testInfo, name, 'ERROR', result.reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
253
263
|
return; // test stays green
|
|
254
264
|
}
|
|
255
265
|
const icon = result.status === 'PASS' ? '✅' : '❌';
|
|
256
266
|
console.log(`${icon} [BugHunters Vision] ${result.status}: ${result.reason}\n`);
|
|
257
267
|
(0, results_1.appendResult)(resolvedDir, {
|
|
258
|
-
testName
|
|
268
|
+
testName,
|
|
259
269
|
status: result.status,
|
|
260
270
|
reason: result.reason,
|
|
261
271
|
method: 'AI',
|
|
262
|
-
baselineFile: `${
|
|
272
|
+
baselineFile: `${filePrefix}.baseline.png`,
|
|
263
273
|
currentFile: currentFileName,
|
|
264
274
|
timestamp: new Date().toISOString(),
|
|
265
275
|
});
|
|
266
|
-
await pushToPlaywright(testInfo, result.status, result.reason, resolvedDir, `${
|
|
276
|
+
await pushToPlaywright(testInfo, name, result.status, result.reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
267
277
|
(0, test_1.expect)(result.status, `Visual regression detected: ${result.reason}`).toBe('PASS');
|
|
268
278
|
}
|