@bughunters/vision 1.0.2 → 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 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
 
@@ -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;AA+GD,wBAAsB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmJ/E"}
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
- async function pushToPlaywright(testInfo, status, reason, resolvedDir, baselineFile, currentFile) {
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: 'BugHunters Vision', description: `${status}: ${reason}` });
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('bhv-baseline', { path: p, contentType: 'image/png' });
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('bhv-current', { path: p, contentType: 'image/png' });
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
- const baselineFile = path.join(resolvedDir, `${name}.baseline.png`);
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(baselineFile) || updateBaseline) {
173
+ if (!fs.existsSync(baselinePath) || updateBaseline) {
164
174
  // First run — save as baseline
165
- fs.writeFileSync(baselineFile, currentBuffer);
166
- console.log(`\n📸 [BugHunters Vision] Baseline saved: ${baselineFile}`);
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: name,
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: `${name}.baseline.png`,
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, `${name}.baseline.png`, null);
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(baselineFile);
191
+ const baselineBuffer = fs.readFileSync(baselinePath);
182
192
  const diffPixels = (0, pixel_match_1.fastPixelMatch)(baselineBuffer, currentBuffer);
183
- const currentFileName = `${name}.current.png`;
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: name,
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: `${name}.baseline.png`,
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, `${name}.baseline.png`, currentFileName);
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: name,
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: `${name}.baseline.png`,
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, `${name}.baseline.png`, currentFileName);
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: name,
239
+ testName,
230
240
  status: 'FAIL',
231
241
  reason,
232
242
  method: 'AI',
233
- baselineFile: `${name}.baseline.png`,
243
+ baselineFile: `${filePrefix}.baseline.png`,
234
244
  currentFile: currentFileName,
235
245
  timestamp: new Date().toISOString(),
236
246
  });
237
- await pushToPlaywright(testInfo, 'FAIL', reason, resolvedDir, `${name}.baseline.png`, currentFileName);
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: name,
254
+ testName,
245
255
  status: 'ERROR',
246
256
  reason: result.reason,
247
257
  method: 'AI',
248
- baselineFile: `${name}.baseline.png`,
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, `${name}.baseline.png`, currentFileName);
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: name,
268
+ testName,
259
269
  status: result.status,
260
270
  reason: result.reason,
261
271
  method: 'AI',
262
- baselineFile: `${name}.baseline.png`,
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, `${name}.baseline.png`, currentFileName);
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
  }
@@ -7,6 +7,7 @@ declare class BugHuntersVisionReporter implements Reporter {
7
7
  private snapshotsDir;
8
8
  private reportDir;
9
9
  constructor(options?: BugHuntersVisionReporterOptions);
10
+ printsToStdio(): boolean;
10
11
  onBegin(): void;
11
12
  onEnd(): void;
12
13
  }
@@ -1 +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"}
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,aAAa,IAAI,OAAO;IAIxB,OAAO,IAAI,IAAI;IAKf,KAAK,IAAI,IAAI;CA8Cd;AAED,eAAe,wBAAwB,CAAC;AACxC,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
package/dist/reporter.js CHANGED
@@ -991,6 +991,9 @@ class BugHuntersVisionReporter {
991
991
  this.snapshotsDir = path.resolve(process.cwd(), options?.snapshotsDir ?? './bhv-snapshots');
992
992
  this.reportDir = path.resolve(process.cwd(), options?.reportDir ?? './bhv-report');
993
993
  }
994
+ printsToStdio() {
995
+ return false;
996
+ }
994
997
  onBegin() {
995
998
  fs.mkdirSync(this.snapshotsDir, { recursive: true });
996
999
  fs.writeFileSync(path.join(this.snapshotsDir, 'results.json'), '[]', 'utf-8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bughunters/vision",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "InfoSec-friendly AI Visual Testing plugin for Playwright.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",