@bughunters/vision 1.0.3 → 1.0.5

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,39 @@ The report includes side-by-side Baseline / Current / Diff views with full-scree
111
130
 
112
131
  ---
113
132
 
133
+ ## Terminal output
134
+
135
+ BugHunters Vision keeps terminal output compact and linear — one line per step, no duplicates:
136
+
137
+ ```
138
+ 📸 [BugHunters Vision] Baseline created: cart-page
139
+ ✅ [BugHunters Vision] FAST PIXEL MATCH: shipping-form
140
+ 🔍 [BugHunters Vision] AI evaluating: payment-page (12 pixel(s) differ)…
141
+ 🤖 [BugHunters Vision] AI PASS: payment-page
142
+ ⚠️ [BugHunters Vision] AI error: header — Claude AI is temporarily overloaded.
143
+ ⏭ [BugHunters Vision] off: footer
144
+ ```
145
+
146
+ When a check fails, a single clear error is thrown (no repeated messages):
147
+
148
+ ```
149
+ Error: [BugHunters Vision] Visual regression detected in step "payment-page": Button label changed from "Pay now" to "Subscribe".
150
+ ```
151
+
152
+ ---
153
+
154
+ ## IDE & VS Code integration
155
+
156
+ BugHunters Vision is fully compatible with the **VS Code Playwright extension** and **Playwright UI mode**.
157
+
158
+ - Tests appear normally in the VS Code Test Explorer sidebar
159
+ - `npx playwright test --list` works as expected
160
+ - `npx playwright test --ui` works as expected
161
+
162
+ This works because `vision` is a standalone object — not a custom fixture — so the native `import { test } from '@playwright/test'` is preserved.
163
+
164
+ ---
165
+
114
166
  ## Configuration reference
115
167
 
116
168
  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 +196,9 @@ BHV_MODE=off npx playwright test
144
196
 
145
197
  After every run a report is generated at `bhv-report/index.html`. No upload, no cloud — everything is local.
146
198
 
147
- - **Summary bar** — total / passed / failed / baseline counts
199
+ - **Summary bar** — total / passed / failed / baseline / error counts
148
200
  - **Baseline / Current / Diff** tab view per test — click any image for fullscreen zoom
149
201
  - **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
202
  - **Method badge** — `AI` · `PIXEL` · `NEW` — shows how each test was evaluated
152
203
  - **Light / Dark / System** theme toggle, persisted in `localStorage`
153
204
 
@@ -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,CAyJ/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) {
@@ -153,116 +156,122 @@ async function runVisionCheck(opts) {
153
156
  const token = projectUse['bvToken'] ?? process.env.BUGHUNTERS_VISION_TOKEN ?? undefined;
154
157
  // Mode: off → skip all visual testing immediately
155
158
  if (mode === 'off') {
156
- console.log(`\n⏭ [BugHunters Vision] "${name}" — skipped (BHV_MODE=off)\n`);
159
+ console.log(`⏭ [BugHunters Vision] off: ${name}`);
157
160
  return;
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}`);
167
- console.log(' ✅ Test PASSED (baseline created — no previous reference exists)\n');
175
+ fs.writeFileSync(baselinePath, currentBuffer);
176
+ console.log(`📸 [BugHunters Vision] Baseline created: ${name}`);
168
177
  (0, results_1.appendResult)(resolvedDir, {
169
- testName: name,
178
+ testName,
170
179
  status: 'BASELINE_CREATED',
171
180
  reason: 'First run detected — screenshot saved as the new baseline.',
172
181
  method: null,
173
- baselineFile: `${name}.baseline.png`,
182
+ baselineFile: `${filePrefix}.baseline.png`,
174
183
  currentFile: null,
175
184
  timestamp: new Date().toISOString(),
176
185
  });
177
- await pushToPlaywright(testInfo, 'BASELINE_CREATED', 'First run — screenshot saved as the new baseline.', resolvedDir, `${name}.baseline.png`, null);
186
+ await pushToPlaywright(testInfo, name, 'BASELINE_CREATED', 'First run — screenshot saved as the new baseline.', resolvedDir, `${filePrefix}.baseline.png`, null);
178
187
  return;
179
188
  }
180
189
  // Baseline exists — run Fast Pixel Match first to save AI credits
181
- const baselineBuffer = fs.readFileSync(baselineFile);
190
+ const baselineBuffer = fs.readFileSync(baselinePath);
182
191
  const diffPixels = (0, pixel_match_1.fastPixelMatch)(baselineBuffer, currentBuffer);
183
- const currentFileName = `${name}.current.png`;
192
+ const currentFileName = `${filePrefix}.current.png`;
184
193
  fs.writeFileSync(path.join(resolvedDir, currentFileName), currentBuffer);
185
194
  if (diffPixels === 0) {
186
195
  // ✅ Pixel-perfect match — skip AI call entirely
187
- console.log(`\n⚡ [BugHunters Vision] "${name}" — pixel-perfect match. Skipping AI call.`);
188
- console.log(' ✅ Test PASSED (Fast Pixel Match)\n');
196
+ console.log(`✅ [BugHunters Vision] FAST PIXEL MATCH: ${name}`);
189
197
  (0, results_1.appendResult)(resolvedDir, {
190
- testName: name,
198
+ testName,
191
199
  status: 'PASS',
192
200
  reason: 'Identical (Fast Pixel Match) — pixel-perfect match, no AI evaluation needed.',
193
201
  method: 'FAST_PIXEL_MATCH',
194
- baselineFile: `${name}.baseline.png`,
202
+ baselineFile: `${filePrefix}.baseline.png`,
195
203
  currentFile: currentFileName,
196
204
  timestamp: new Date().toISOString(),
197
205
  });
198
- await pushToPlaywright(testInfo, 'PASS', 'Pixel-perfect match — no AI evaluation needed.', resolvedDir, `${name}.baseline.png`, currentFileName);
206
+ await pushToPlaywright(testInfo, name, 'PASS', 'Pixel-perfect match — no AI evaluation needed.', resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
199
207
  return;
200
208
  }
201
209
  // Pixels differ — behaviour depends on mode
202
210
  const diffLabel = diffPixels === -1 ? 'dimension mismatch' : `${diffPixels} pixel(s) differ`;
203
211
  if (mode === 'strict') {
204
212
  // strict: fail immediately without calling AI
205
- console.log(`\n❌ [BugHunters Vision] "${name}" — ${diffLabel}. FAILED (strict mode — no AI call).\n`);
206
213
  (0, results_1.appendResult)(resolvedDir, {
207
- testName: name,
214
+ testName,
208
215
  status: 'FAIL',
209
216
  reason: `Pixel mismatch detected (${diffLabel}). Strict mode — AI evaluation disabled.`,
210
217
  method: 'FAST_PIXEL_MATCH',
211
- baselineFile: `${name}.baseline.png`,
218
+ baselineFile: `${filePrefix}.baseline.png`,
212
219
  currentFile: currentFileName,
213
220
  timestamp: new Date().toISOString(),
214
221
  });
215
- await pushToPlaywright(testInfo, 'FAIL', `Pixel mismatch detected (${diffLabel}). Strict mode.`, resolvedDir, `${name}.baseline.png`, currentFileName);
216
- (0, test_1.expect)(false, `[BugHunters Vision strict] Pixel mismatch: ${diffLabel}`).toBe(true);
222
+ await pushToPlaywright(testInfo, name, 'FAIL', `Pixel mismatch detected (${diffLabel}). Strict mode.`, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
223
+ (0, test_1.expect)(false, `[BugHunters Vision] Visual regression detected in step "${name}": ${diffLabel} (strict mode)`).toBe(true);
217
224
  return;
218
225
  }
219
226
  // ai mode — escalate to AI for intelligent evaluation
220
- console.log(`\n🔍 [BugHunters Vision] "${name}" ${diffLabel}. Sending to AI…`);
227
+ console.log(`🔍 [BugHunters Vision] AI evaluating: ${name} (${diffLabel})…`);
221
228
  let result;
222
229
  try {
223
230
  result = await callVisionApi(baselineBuffer.toString('base64'), currentBuffer.toString('base64'), prompt, apiUrl, token);
224
231
  }
225
232
  catch (err) {
226
233
  const reason = humanReadableApiError(err instanceof Error ? err.message : String(err));
227
- console.log(`\n❌ [BugHunters Vision] ${reason}\n`);
228
234
  (0, results_1.appendResult)(resolvedDir, {
229
- testName: name,
235
+ testName,
230
236
  status: 'FAIL',
231
237
  reason,
232
238
  method: 'AI',
233
- baselineFile: `${name}.baseline.png`,
239
+ baselineFile: `${filePrefix}.baseline.png`,
234
240
  currentFile: currentFileName,
235
241
  timestamp: new Date().toISOString(),
236
242
  });
237
- await pushToPlaywright(testInfo, 'FAIL', reason, resolvedDir, `${name}.baseline.png`, currentFileName);
238
- throw err;
243
+ await pushToPlaywright(testInfo, name, 'FAIL', reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
244
+ throw new Error(`[BugHunters Vision] Visual regression detected in step "${name}": ${reason}`);
239
245
  }
240
246
  // ERROR: AI temporarily unavailable — record but do NOT fail the Playwright test
241
247
  if (result.status === 'ERROR') {
242
- console.log(`\n⚠️ [BugHunters Vision] "${name}"AI unavailable: ${result.reason}\n`);
248
+ console.log(`⚠️ [BugHunters Vision] AI error: ${name} — ${result.reason}`);
243
249
  (0, results_1.appendResult)(resolvedDir, {
244
- testName: name,
250
+ testName,
245
251
  status: 'ERROR',
246
252
  reason: result.reason,
247
253
  method: 'AI',
248
- baselineFile: `${name}.baseline.png`,
254
+ baselineFile: `${filePrefix}.baseline.png`,
249
255
  currentFile: currentFileName,
250
256
  timestamp: new Date().toISOString(),
251
257
  });
252
- await pushToPlaywright(testInfo, 'ERROR', result.reason, resolvedDir, `${name}.baseline.png`, currentFileName);
258
+ await pushToPlaywright(testInfo, name, 'ERROR', result.reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
253
259
  return; // test stays green
254
260
  }
255
- const icon = result.status === 'PASS' ? '✅' : '❌';
256
- console.log(`${icon} [BugHunters Vision] ${result.status}: ${result.reason}\n`);
257
261
  (0, results_1.appendResult)(resolvedDir, {
258
- testName: name,
262
+ testName,
259
263
  status: result.status,
260
264
  reason: result.reason,
261
265
  method: 'AI',
262
- baselineFile: `${name}.baseline.png`,
266
+ baselineFile: `${filePrefix}.baseline.png`,
263
267
  currentFile: currentFileName,
264
268
  timestamp: new Date().toISOString(),
265
269
  });
266
- await pushToPlaywright(testInfo, result.status, result.reason, resolvedDir, `${name}.baseline.png`, currentFileName);
267
- (0, test_1.expect)(result.status, `Visual regression detected: ${result.reason}`).toBe('PASS');
270
+ await pushToPlaywright(testInfo, name, result.status, result.reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
271
+ if (result.status === 'PASS') {
272
+ console.log(`🤖 [BugHunters Vision] AI PASS: ${name}`);
273
+ }
274
+ else {
275
+ throw new Error(`[BugHunters Vision] Visual regression detected in step "${name}": ${result.reason}`);
276
+ }
268
277
  }
@@ -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,aAAa,IAAI,OAAO;IAIxB,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;AA4hCD,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;CAkDd;AAED,eAAe,wBAAwB,CAAC;AACxC,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
package/dist/reporter.js CHANGED
@@ -37,6 +37,36 @@ exports.BugHuntersVisionReporter = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const check_1 = require("./check");
40
+ // ─── Group flat step results by Playwright test title ─────────────────────────
41
+ function groupResults(results) {
42
+ const order = [];
43
+ const map = new Map();
44
+ for (const r of results) {
45
+ const sep = r.testName.indexOf(' › ');
46
+ const title = sep >= 0 ? r.testName.slice(0, sep) : r.testName;
47
+ if (!map.has(title)) {
48
+ map.set(title, []);
49
+ order.push(title);
50
+ }
51
+ map.get(title).push(r);
52
+ }
53
+ return order.map(testTitle => {
54
+ const steps = map.get(testTitle);
55
+ const hasFail = steps.some(s => s.status === 'FAIL');
56
+ const hasError = steps.some(s => s.status === 'ERROR');
57
+ const allBase = steps.every(s => s.status === 'BASELINE_CREATED');
58
+ const status = hasFail ? 'FAIL' : hasError ? 'ERROR' : allBase ? 'BASELINE_CREATED' : 'PASS';
59
+ return {
60
+ testTitle,
61
+ status,
62
+ passedSteps: steps.filter(s => s.status === 'PASS').length,
63
+ failedSteps: steps.filter(s => s.status === 'FAIL').length,
64
+ erroredSteps: steps.filter(s => s.status === 'ERROR').length,
65
+ baselineSteps: steps.filter(s => s.status === 'BASELINE_CREATED').length,
66
+ steps,
67
+ };
68
+ });
69
+ }
40
70
  // ─── CSS ─────────────────────────────────────────────────────────────────────
41
71
  function getCss() {
42
72
  return `
@@ -192,27 +222,36 @@ body {
192
222
 
193
223
  /* ── Test list ── */
194
224
  .test-list { display: flex; flex-direction: column; gap: 6px; padding-bottom: 48px; }
195
- .test-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-sm); }
196
- .test-item.s-fail { border-left: 3px solid var(--fail); }
197
- .test-item.s-base { border-left: 3px solid var(--base); }
198
- .test-item.s-warn { border-left: 3px solid var(--pixel); }
199
225
 
200
- .test-row {
226
+ /* ── Test group — one Playwright test ── */
227
+ .test-group {
228
+ background: var(--surface); border: 1px solid var(--border);
229
+ border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-sm);
230
+ }
231
+ .test-group.s-fail { border-left: 3px solid var(--fail); }
232
+ .test-group.s-base { border-left: 3px solid var(--base); }
233
+ .test-group.s-warn { border-left: 3px solid var(--pixel); }
234
+
235
+ /* ── Group header row (clickable, expands step list) ── */
236
+ .group-row {
201
237
  display: flex; align-items: center; gap: 11px;
202
238
  padding: 12px 14px; cursor: pointer;
203
239
  transition: background .1s; user-select: none;
204
240
  }
205
- .test-row:hover { background: var(--surface-2); }
241
+ .group-row:hover { background: var(--surface-2); }
206
242
 
243
+ /* ── Status dot ── */
207
244
  .s-dot {
208
245
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
209
246
  margin-top: 1px;
210
247
  }
248
+ .s-dot-sm { width: 6px; height: 6px; margin-top: 0; }
211
249
  .s-pass .s-dot { background: var(--pass); }
212
250
  .s-fail .s-dot { background: var(--fail); }
213
251
  .s-base .s-dot { background: var(--base); }
214
252
  .s-warn .s-dot { background: var(--pixel); }
215
253
 
254
+ /* ── Group text ── */
216
255
  .test-meta { flex: 1; min-width: 0; }
217
256
  .test-name {
218
257
  font-family: ui-monospace, 'SFMono-Regular', 'Fira Code', monospace;
@@ -224,6 +263,7 @@ body {
224
263
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
225
264
  }
226
265
 
266
+ /* ── Badges ── */
227
267
  .badges { display: flex; gap: 5px; align-items: center; flex-shrink: 0; }
228
268
  .badge {
229
269
  font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 4px;
@@ -240,9 +280,35 @@ body {
240
280
  .chev { font-size: 16px; color: var(--text-3); flex-shrink: 0; line-height: 1; transition: transform .2s; display: inline-block; }
241
281
  .chev.open { transform: rotate(90deg); }
242
282
 
243
- /* ── Detail panel ── */
244
- .test-detail { border-top: 1px solid var(--border); padding: 18px 14px 18px 33px; display: none; }
283
+ /* ── Step list (expands when group is open) ── */
284
+ .group-steps { display: none; }
285
+
286
+ /* ── Individual step ── */
287
+ .step-item { border-top: 1px solid var(--border); }
288
+
289
+ /* ── Step header row (clickable, expands detail) ── */
290
+ .step-row {
291
+ display: flex; align-items: center; gap: 9px;
292
+ padding: 9px 14px 9px 34px; cursor: pointer;
293
+ transition: background .1s; user-select: none;
294
+ }
295
+ .step-row:hover { background: var(--surface-2); }
245
296
 
297
+ /* ── Step text ── */
298
+ .step-meta { flex: 1; min-width: 0; }
299
+ .step-name {
300
+ font-family: ui-monospace, 'SFMono-Regular', 'Fira Code', monospace;
301
+ font-size: 12px; font-weight: 500; color: var(--text-2);
302
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
303
+ }
304
+
305
+ /* ── Step detail (verdict + images) ── */
306
+ .step-detail {
307
+ display: none; border-top: 1px solid var(--border);
308
+ padding: 16px 14px 16px 34px;
309
+ }
310
+
311
+ /* ── Verdict box ── */
246
312
  .verdict {
247
313
  background: var(--accent-soft); border-left: 3px solid var(--accent);
248
314
  border-radius: 0 6px 6px 0; padding: 12px 14px; margin-bottom: 18px;
@@ -267,9 +333,7 @@ body {
267
333
  .view-tab.diff-tab.on { color: var(--fail); }
268
334
 
269
335
  /* ── Image views ── */
270
- .img-section { }
271
336
  .img-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 1.2px; color: var(--text-3); font-weight: 500; margin-bottom: 12px; }
272
- .img-view { }
273
337
  .view-img {
274
338
  width: 100%; display: block; border-radius: 6px; border: 1px solid var(--border);
275
339
  max-height: 360px; object-fit: contain; object-position: top center;
@@ -380,8 +444,8 @@ function getJs() {
380
444
  (function () {
381
445
  'use strict';
382
446
 
383
- var RESULTS = JSON.parse(document.getElementById('bv-r').textContent);
384
- var META = JSON.parse(document.getElementById('bv-m').textContent);
447
+ var GROUPS = JSON.parse(document.getElementById('bv-g').textContent);
448
+ var META = JSON.parse(document.getElementById('bv-m').textContent);
385
449
 
386
450
  /* ── State ── */
387
451
  var state = {
@@ -423,23 +487,19 @@ function getJs() {
423
487
 
424
488
  /* ── Pixel diff engine (100% client-side, zero server calls) ── */
425
489
 
426
- /* Weighted squared RGB distance — perceptually accurate, fast (no sqrt needed) */
427
490
  function colorDist(r1, g1, b1, r2, g2, b2) {
428
491
  var rd = r1 - r2, gd = g1 - g2, bd = b1 - b2;
429
492
  return rd * rd * 0.299 + gd * gd * 0.587 + bd * bd * 0.114;
430
493
  }
431
494
 
432
- /* Render diff image: changed pixels = neon red, unchanged = desaturated baseline */
433
495
  function renderDiff(d1, d2, w, h) {
434
496
  var n = w * h * 4;
435
497
  var out = new Uint8ClampedArray(n);
436
- var thr = 900; /* squared perceptual threshold */
498
+ var thr = 900;
437
499
  for (var i = 0; i < n; i += 4) {
438
500
  if (colorDist(d1[i], d1[i+1], d1[i+2], d2[i], d2[i+1], d2[i+2]) > thr) {
439
- /* neon red highlight */
440
501
  out[i] = 220; out[i+1] = 38; out[i+2] = 38; out[i+3] = 255;
441
502
  } else {
442
- /* desaturated baseline — guides the eye to red pixels */
443
503
  var lum = (d1[i] * 77 + d1[i+1] * 150 + d1[i+2] * 29) >> 8;
444
504
  out[i] = lum; out[i+1] = lum; out[i+2] = lum; out[i+3] = 210;
445
505
  }
@@ -447,7 +507,6 @@ function getJs() {
447
507
  return new ImageData(out, w, h);
448
508
  }
449
509
 
450
- /* Load image and extract raw pixel data via canvas */
451
510
  function loadImgPixels(url, cb) {
452
511
  var img = new Image();
453
512
  img.crossOrigin = 'anonymous';
@@ -469,7 +528,6 @@ function getJs() {
469
528
  var diffCache = {};
470
529
 
471
530
  function genDiff(idx, baseUrl, curUrl, container) {
472
- /* Return cached result if already computed */
473
531
  if (diffCache[idx]) {
474
532
  var cached = diffCache[idx];
475
533
  var clone = document.createElement('canvas');
@@ -525,7 +583,6 @@ function getJs() {
525
583
 
526
584
  function lbReset() { lb.scale = 1; lb.tx = 0; lb.ty = 0; lbApply(); }
527
585
 
528
- /* Zoom towards a viewport-center-relative point (mx, my) */
529
586
  function lbZoom(factor, mx, my) {
530
587
  var ns = Math.max(lb.minScale, Math.min(lb.maxScale, lb.scale * factor));
531
588
  var r = ns / lb.scale;
@@ -535,7 +592,6 @@ function getJs() {
535
592
  lbApply();
536
593
  }
537
594
 
538
- /* Auto-fit: scale image to fill viewport on first open */
539
595
  function lbAutoFit(w, h) {
540
596
  var vw = lb.inner.clientWidth - 48;
541
597
  var vh = lb.inner.clientHeight - 48;
@@ -556,7 +612,6 @@ function getJs() {
556
612
  el.onload = function () { lbAutoFit(el.naturalWidth, el.naturalHeight); };
557
613
  el.src = payload;
558
614
  } else {
559
- /* diff canvas */
560
615
  var src = diffCache[payload];
561
616
  if (!src) return;
562
617
  el = document.createElement('canvas');
@@ -603,28 +658,19 @@ function getJs() {
603
658
  lb.wrap = ov.querySelector('.lb-img-wrap');
604
659
  lb.zoomLbl = ov.querySelector('.lb-zoom-label');
605
660
 
606
- /* close button */
607
661
  ov.querySelector('.lb-close').addEventListener('click', lbClose);
608
-
609
- /* click on backdrop (not on image) */
610
662
  lb.inner.addEventListener('click', function (e) {
611
663
  if (e.target === lb.inner) lbClose();
612
664
  });
613
-
614
- /* double-click: reset zoom */
615
665
  lb.inner.addEventListener('dblclick', function (e) {
616
666
  e.preventDefault();
617
667
  lbReset();
618
668
  });
619
-
620
- /* scroll-to-zoom */
621
669
  lb.inner.addEventListener('wheel', function (e) {
622
670
  e.preventDefault();
623
671
  var p = lbCenterPos(e);
624
672
  lbZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, p.x, p.y);
625
673
  }, { passive: false });
626
-
627
- /* mouse drag: pan */
628
674
  lb.inner.addEventListener('mousedown', function (e) {
629
675
  if (e.button !== 0) return;
630
676
  lb.dragging = true;
@@ -644,8 +690,6 @@ function getJs() {
644
690
  lb.dragging = false;
645
691
  lb.wrap.classList.remove('dragging');
646
692
  });
647
-
648
- /* touch: pan + pinch-to-zoom */
649
693
  lb.inner.addEventListener('touchstart', function (e) {
650
694
  if (e.touches.length === 2) {
651
695
  var dx = e.touches[0].clientX - e.touches[1].clientX;
@@ -674,8 +718,6 @@ function getJs() {
674
718
  }
675
719
  }, { passive: false });
676
720
  lb.inner.addEventListener('touchend', function () { lb.dragging = false; }, { passive: true });
677
-
678
- /* keyboard shortcuts */
679
721
  document.addEventListener('keydown', function (e) {
680
722
  if (!lb.overlay.classList.contains('open')) return;
681
723
  if (e.key === 'Escape') lbClose();
@@ -684,6 +726,7 @@ function getJs() {
684
726
  }
685
727
 
686
728
  /* ── HTML builders ── */
729
+
687
730
  function statusInfo(r) {
688
731
  if (r.status === 'PASS') return { cls:'s-pass', badge:'m-pass', label:'PASS' };
689
732
  if (r.status === 'FAIL') return { cls:'s-fail', badge:'m-fail', label:'FAIL' };
@@ -749,40 +792,75 @@ function getJs() {
749
792
  + tabs + basePanel + curPanel + diffPanel;
750
793
  }
751
794
 
752
- function buildDetail(r) {
753
- var imgWrap = r.status === 'PASS' ? 'img-section pass-imgs' : 'img-section';
795
+ /* Build a single visual-check step row + expandable detail */
796
+ function buildStep(r) {
797
+ var s = statusInfo(r);
798
+ var method = methodLabel(r);
799
+ var methodCls = methodBadgeCls(r);
800
+
801
+ /* Extract step name: "Test title › step name" → "step name" */
802
+ var raw = r.testName || '';
803
+ var sepIdx = raw.indexOf(' \u203a ');
804
+ var stepName = sepIdx >= 0 ? raw.slice(sepIdx + 3) : raw;
805
+
754
806
  var verdictLbl = r.method === 'FAST_PIXEL_MATCH'
755
807
  ? '\u26A1 Fast Pixel Match'
756
808
  : '\uD83E\uDD16 AI Verdict';
757
- return '<div class="test-detail" id="d' + r.index + '">'
758
- + '<div class="verdict">'
759
- + '<div class="verdict-lbl">' + verdictLbl + '</div>'
760
- + '<p class="verdict-txt">\u201C' + esc(r.reason) + '\u201D</p>'
809
+ var imgWrap = r.status === 'PASS' ? 'img-section pass-imgs' : 'img-section';
810
+
811
+ return '<div class="step-item ' + s.cls + '">'
812
+ + '<div class="step-row" data-s="' + r.index + '">'
813
+ + '<div class="s-dot s-dot-sm"></div>'
814
+ + '<div class="step-meta">'
815
+ + '<div class="step-name">' + esc(stepName) + '</div>'
816
+ + '</div>'
817
+ + '<div class="badges">'
818
+ + '<span class="badge ' + methodCls + '">' + method + '</span>'
819
+ + '<span class="badge ' + s.badge + '">' + s.label + '</span>'
820
+ + '</div>'
821
+ + '<span class="chev" id="sc' + r.index + '">\u203a</span>'
822
+ + '</div>'
823
+ + '<div class="step-detail" id="sd' + r.index + '">'
824
+ + '<div class="verdict">'
825
+ + '<div class="verdict-lbl">' + verdictLbl + '</div>'
826
+ + '<p class="verdict-txt">\u201C' + esc(r.reason) + '\u201D</p>'
827
+ + '</div>'
828
+ + '<div class="' + imgWrap + '">' + buildImages(r) + '</div>'
761
829
  + '</div>'
762
- + '<div class="' + imgWrap + '">' + buildImages(r) + '</div>'
763
- + '</div>';
830
+ + '</div>';
764
831
  }
765
832
 
766
- function buildItem(r) {
767
- var s = statusInfo(r);
768
- var method = methodLabel(r);
769
- var methodCls = methodBadgeCls(r);
770
- var snip = trunc(r.reason, 88);
771
- return '<div class="test-item ' + s.cls + '">'
772
- + '<div class="test-row" data-t="' + r.index + '">'
833
+ /* Build a test group row (Playwright test + its visual-check steps) */
834
+ function buildGroup(g, gIdx) {
835
+ var s = statusInfo(g);
836
+ var n = g.steps.length;
837
+
838
+ /* Snip: "5 visual checks · 2 failed" or "3 visual checks · all passed" */
839
+ var parts = [];
840
+ if (g.failedSteps > 0) parts.push(g.failedSteps + ' failed');
841
+ if (g.erroredSteps > 0) parts.push(g.erroredSteps + (g.erroredSteps !== 1 ? ' errors' : ' error'));
842
+ if (g.baselineSteps > 0 && g.failedSteps === 0 && g.erroredSteps === 0)
843
+ parts.push('baselines created');
844
+ var checkWord = n === 1 ? 'visual check' : 'visual checks';
845
+ var snip = n + ' ' + checkWord
846
+ + (parts.length > 0 ? ' \u00b7 ' + parts.join(' \u00b7 ') : ' \u00b7 all passed');
847
+
848
+ var stepsHtml = g.steps.map(buildStep).join('');
849
+
850
+ return '<div class="test-group ' + s.cls + '" data-gst="' + g.status + '">'
851
+ + '<div class="group-row" data-g="' + gIdx + '">'
773
852
  + '<div class="s-dot"></div>'
774
853
  + '<div class="test-meta">'
775
- + '<div class="test-name">' + esc(r.testName) + '</div>'
776
- + '<div class="test-snip">' + esc(snip) + '</div>'
854
+ + '<div class="test-name">' + esc(g.testTitle) + '</div>'
855
+ + '<div class="test-snip">' + esc(snip) + '</div>'
777
856
  + '</div>'
778
857
  + '<div class="badges">'
779
- + '<span class="badge ' + methodCls + '">' + method + '</span>'
780
- + '<span class="badge ' + s.badge + '">' + s.label + '</span>'
858
+ + '<span class="badge ' + s.badge + '">' + s.label + '</span>'
781
859
  + '</div>'
782
- + '<span class="chev" id="c' + r.index + '">\u203A</span>'
860
+ + '<span class="chev" id="gc' + gIdx + '">\u203a</span>'
783
861
  + '</div>'
784
- + buildDetail(r)
785
- + '</div>';
862
+ + '<div class="group-steps" id="gs' + gIdx + '">' + stepsHtml + '</div>'
863
+ + '</div>';
786
864
  }
787
865
 
788
866
  function buildThemeBtns() {
@@ -812,7 +890,7 @@ function getJs() {
812
890
  var warnFilter = hasErrors
813
891
  ? '<button class="f-btn" data-f="warn">Errors (' + META.errored + ')</button>'
814
892
  : '';
815
- var items = RESULTS.map(buildItem).join('');
893
+ var items = GROUPS.map(function (g, i) { return buildGroup(g, i); }).join('');
816
894
 
817
895
  return '<header class="hdr">'
818
896
  + '<div class="wrap"><div class="hdr-inner">'
@@ -826,7 +904,7 @@ function getJs() {
826
904
  + '</header>'
827
905
  + '<div class="wrap">'
828
906
  + '<div class="summary">'
829
- + '<div class="stat"><div class="stat-lbl">Total</div><div class="stat-val">' + META.total + '</div></div>'
907
+ + '<div class="stat"><div class="stat-lbl">Tests</div><div class="stat-val">' + META.total + '</div></div>'
830
908
  + '<div class="stat s-pass"><div class="stat-lbl">Passed</div><div class="stat-val">' + META.passed + '</div></div>'
831
909
  + '<div class="stat' + failCls + '"><div class="stat-lbl">Failed</div><div class="stat-val">' + META.failed + '</div></div>'
832
910
  + warnStat
@@ -853,35 +931,45 @@ function getJs() {
853
931
  function bind() {
854
932
  var tlist = document.getElementById('tlist');
855
933
 
856
- /* accordion expand/collapse only fires from .test-row[data-t] */
934
+ /* Level 1: test group accordion expand/collapse step list */
935
+ tlist.addEventListener('click', function (e) {
936
+ var row = e.target.closest('[data-g]');
937
+ if (!row) return;
938
+ var idx = row.getAttribute('data-g');
939
+ var steps = document.getElementById('gs' + idx);
940
+ var chev = document.getElementById('gc' + idx);
941
+ if (!steps) return;
942
+ var open = steps.style.display !== 'none';
943
+ steps.style.display = open ? 'none' : 'block';
944
+ if (chev) chev.classList.toggle('open', !open);
945
+ });
946
+
947
+ /* Level 2: step accordion — expand/collapse verdict + images */
857
948
  tlist.addEventListener('click', function (e) {
858
- var row = e.target.closest('[data-t]');
949
+ var row = e.target.closest('[data-s]');
859
950
  if (!row) return;
860
- var idx = row.getAttribute('data-t');
861
- var detail = document.getElementById('d' + idx);
862
- var chevron = document.getElementById('c' + idx);
951
+ var idx = row.getAttribute('data-s');
952
+ var detail = document.getElementById('sd' + idx);
953
+ var chev = document.getElementById('sc' + idx);
863
954
  if (!detail) return;
864
955
  var open = detail.style.display !== 'none';
865
956
  detail.style.display = open ? 'none' : 'block';
866
- if (chevron) chevron.classList.toggle('open', !open);
957
+ if (chev) chev.classList.toggle('open', !open);
867
958
  });
868
959
 
869
- /* view-tab switching: Baseline / Current / Diff */
960
+ /* View-tab switching: Baseline / Current / Diff */
870
961
  tlist.addEventListener('click', function (e) {
871
962
  var tab = e.target.closest('[data-tab]');
872
963
  if (!tab) return;
873
964
  var tidx = tab.getAttribute('data-tidx');
874
965
  var tabName = tab.getAttribute('data-tab');
875
- /* update active tab */
876
966
  tab.closest('.view-tabs').querySelectorAll('.view-tab').forEach(function (t) {
877
967
  t.classList.toggle('on', t === tab);
878
968
  });
879
- /* show correct panel, hide others */
880
969
  ['base', 'cur', 'diff'].forEach(function (key) {
881
970
  var panel = document.getElementById('v' + key + '-' + tidx);
882
971
  if (panel) panel.style.display = key === tabName ? '' : 'none';
883
972
  });
884
- /* lazy diff generation — only on first click */
885
973
  if (tabName === 'diff') {
886
974
  var dp = document.getElementById('vdiff-' + tidx);
887
975
  if (dp && dp.getAttribute('data-ready') === '0') {
@@ -891,7 +979,7 @@ function getJs() {
891
979
  }
892
980
  });
893
981
 
894
- /* image / diff-canvas click → open lightbox */
982
+ /* Image / diff-canvas click → open lightbox */
895
983
  tlist.addEventListener('click', function (e) {
896
984
  var el = e.target.closest('[data-lb]');
897
985
  if (!el) return;
@@ -900,7 +988,7 @@ function getJs() {
900
988
  if (type === 'diff') lbOpen('diff', el.getAttribute('data-lb-idx'));
901
989
  });
902
990
 
903
- /* theme */
991
+ /* Theme */
904
992
  document.getElementById('t-toggle').addEventListener('click', function (e) {
905
993
  var btn = e.target.closest('[data-th]');
906
994
  if (!btn) return;
@@ -912,7 +1000,7 @@ function getJs() {
912
1000
  });
913
1001
  });
914
1002
 
915
- /* screenshots on pass toggle */
1003
+ /* Screenshots on pass toggle */
916
1004
  document.getElementById('img-btn').addEventListener('click', function () {
917
1005
  state.showPassImg = !state.showPassImg;
918
1006
  localStorage.setItem('bv-pass-img', String(state.showPassImg));
@@ -920,18 +1008,20 @@ function getJs() {
920
1008
  this.classList.toggle('on', state.showPassImg);
921
1009
  });
922
1010
 
923
- /* filter */
1011
+ /* Filter — operates at test-group level */
924
1012
  document.querySelectorAll('[data-f]').forEach(function (btn) {
925
1013
  btn.addEventListener('click', function () {
926
1014
  var f = btn.getAttribute('data-f');
927
1015
  document.querySelectorAll('[data-f]').forEach(function (b) {
928
1016
  b.classList.toggle('on', b.getAttribute('data-f') === f);
929
1017
  });
930
- document.querySelectorAll('.test-item').forEach(function (el) {
1018
+ document.querySelectorAll('.test-group').forEach(function (el) {
1019
+ var gst = el.getAttribute('data-gst');
931
1020
  var show = f === 'all'
932
- || (f === 'pass' && el.classList.contains('s-pass'))
933
- || (f === 'fail' && el.classList.contains('s-fail'))
934
- || (f === 'warn' && el.classList.contains('s-warn'));
1021
+ || (f === 'pass' && gst === 'PASS')
1022
+ || (f === 'fail' && gst === 'FAIL')
1023
+ || (f === 'warn' && gst === 'ERROR')
1024
+ || (f === 'base' && gst === 'BASELINE_CREATED');
935
1025
  el.style.display = show ? '' : 'none';
936
1026
  });
937
1027
  });
@@ -952,8 +1042,8 @@ function getJs() {
952
1042
  `.trim();
953
1043
  }
954
1044
  // ─── HTML Assembly ───────────────────────────────────────────────────────────
955
- function generateHTML(results, meta) {
956
- const resultsJson = JSON.stringify(results);
1045
+ function generateHTML(groups, meta) {
1046
+ const groupsJson = JSON.stringify(groups);
957
1047
  const metaJson = JSON.stringify(meta);
958
1048
  const svgPass = encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#1a2e22"/><path d="M7 16.5L13 22L25 10" stroke="#73c991" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>');
959
1049
  const svgFail = encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#2a1515"/><path d="M10 10L22 22M22 10L10 22" stroke="#f14c4c" stroke-width="3" stroke-linecap="round"/></svg>');
@@ -977,7 +1067,7 @@ function generateHTML(results, meta) {
977
1067
  </head>
978
1068
  <body>
979
1069
  <div id="root"></div>
980
- <script type="application/json" id="bv-r">${resultsJson}</script>
1070
+ <script type="application/json" id="bv-g">${groupsJson}</script>
981
1071
  <script type="application/json" id="bv-m">${metaJson}</script>
982
1072
  <script>${getJs()}</script>
983
1073
  </body>
@@ -1010,13 +1100,14 @@ class BugHuntersVisionReporter {
1010
1100
  return;
1011
1101
  }
1012
1102
  const results = JSON.parse(raw);
1013
- const passed = results.filter(r => r.status === 'PASS').length;
1014
- const failed = results.filter(r => r.status === 'FAIL').length;
1015
- const baselines = results.filter(r => r.status === 'BASELINE_CREATED').length;
1016
- const errored = results.filter(r => r.status === 'ERROR').length;
1103
+ const groups = groupResults(results);
1104
+ const passed = groups.filter(g => g.status === 'PASS').length;
1105
+ const failed = groups.filter(g => g.status === 'FAIL').length;
1106
+ const baselines = groups.filter(g => g.status === 'BASELINE_CREATED').length;
1107
+ const errored = groups.filter(g => g.status === 'ERROR').length;
1017
1108
  const meta = {
1018
1109
  generatedAt: new Date().toISOString(),
1019
- total: results.length,
1110
+ total: groups.length,
1020
1111
  passed,
1021
1112
  failed,
1022
1113
  baselines,
@@ -1024,11 +1115,13 @@ class BugHuntersVisionReporter {
1024
1115
  };
1025
1116
  const reportPath = path.join(this.reportDir, 'index.html');
1026
1117
  fs.mkdirSync(this.reportDir, { recursive: true });
1027
- fs.writeFileSync(reportPath, generateHTML(results, meta), 'utf-8');
1118
+ fs.writeFileSync(reportPath, generateHTML(groups, meta), 'utf-8');
1119
+ const testWord = groups.length === 1 ? 'test' : 'tests';
1120
+ const stepWord = results.length === 1 ? 'check' : 'checks';
1028
1121
  console.log('\n\uD83D\uDC1E BugHunters Vision Report generated:');
1029
1122
  console.log(' \uD83D\uDCC4 ' + reportPath);
1030
1123
  const erroredStr = errored > 0 ? ' \u26A0\uFE0F ' + errored + ' errored' : '';
1031
- console.log(' \u2705 ' + passed + ' passed \u274C ' + failed + ' failed' + erroredStr + ' (' + results.length + ' total)');
1124
+ console.log(' \u2705 ' + passed + ' passed \u274C ' + failed + ' failed' + erroredStr + ' (' + groups.length + ' ' + testWord + ' \u00B7 ' + results.length + ' ' + stepWord + ' total)');
1032
1125
  // Show remaining API balance if an AI call was made this run
1033
1126
  const balance = (0, check_1.getLastRemainingBalance)();
1034
1127
  if (balance !== null) {
package/dist/types.d.ts CHANGED
@@ -9,6 +9,15 @@ export interface TestResult {
9
9
  currentFile: string | null;
10
10
  timestamp: string;
11
11
  }
12
+ export interface TestGroup {
13
+ testTitle: string;
14
+ status: 'PASS' | 'FAIL' | 'BASELINE_CREATED' | 'ERROR';
15
+ passedSteps: number;
16
+ failedSteps: number;
17
+ erroredSteps: number;
18
+ baselineSteps: number;
19
+ steps: TestResult[];
20
+ }
12
21
  export interface Meta {
13
22
  generatedAt: string;
14
23
  total: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,GAAG,OAAO,CAAC;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,IAAI,GAAG,kBAAkB,GAAG,IAAI,CAAC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,IAAI;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,GAAG,OAAO,CAAC;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,IAAI,GAAG,kBAAkB,GAAG,IAAI,CAAC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,GAAG,OAAO,CAAC;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,IAAI;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bughunters/vision",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "InfoSec-friendly AI Visual Testing plugin for Playwright.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",