@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 +53 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +50 -41
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +180 -87
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- 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,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
|
|
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,CAyJ/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) {
|
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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: `${
|
|
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, `${
|
|
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(
|
|
190
|
+
const baselineBuffer = fs.readFileSync(baselinePath);
|
|
182
191
|
const diffPixels = (0, pixel_match_1.fastPixelMatch)(baselineBuffer, currentBuffer);
|
|
183
|
-
const currentFileName = `${
|
|
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(
|
|
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
|
|
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: `${
|
|
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, `${
|
|
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
|
|
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: `${
|
|
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, `${
|
|
216
|
-
(0, test_1.expect)(false, `[BugHunters Vision
|
|
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(
|
|
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
|
|
235
|
+
testName,
|
|
230
236
|
status: 'FAIL',
|
|
231
237
|
reason,
|
|
232
238
|
method: 'AI',
|
|
233
|
-
baselineFile: `${
|
|
239
|
+
baselineFile: `${filePrefix}.baseline.png`,
|
|
234
240
|
currentFile: currentFileName,
|
|
235
241
|
timestamp: new Date().toISOString(),
|
|
236
242
|
});
|
|
237
|
-
await pushToPlaywright(testInfo, 'FAIL', reason, resolvedDir, `${
|
|
238
|
-
throw
|
|
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(
|
|
248
|
+
console.log(`⚠️ [BugHunters Vision] AI error: ${name} — ${result.reason}`);
|
|
243
249
|
(0, results_1.appendResult)(resolvedDir, {
|
|
244
|
-
testName
|
|
250
|
+
testName,
|
|
245
251
|
status: 'ERROR',
|
|
246
252
|
reason: result.reason,
|
|
247
253
|
method: 'AI',
|
|
248
|
-
baselineFile: `${
|
|
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, `${
|
|
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
|
|
262
|
+
testName,
|
|
259
263
|
status: result.status,
|
|
260
264
|
reason: result.reason,
|
|
261
265
|
method: 'AI',
|
|
262
|
-
baselineFile: `${
|
|
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, `${
|
|
267
|
-
|
|
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
|
}
|
package/dist/reporter.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
/* ──
|
|
244
|
-
.
|
|
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
|
|
384
|
-
var META
|
|
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;
|
|
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
|
-
|
|
753
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
763
|
-
+ '</div>';
|
|
830
|
+
+ '</div>';
|
|
764
831
|
}
|
|
765
832
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
var
|
|
769
|
-
var
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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(
|
|
776
|
-
+ '<div class="test-snip">'
|
|
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 ' +
|
|
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="
|
|
860
|
+
+ '<span class="chev" id="gc' + gIdx + '">\u203a</span>'
|
|
783
861
|
+ '</div>'
|
|
784
|
-
+
|
|
785
|
-
|
|
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
|
|
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">
|
|
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
|
|
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-
|
|
949
|
+
var row = e.target.closest('[data-s]');
|
|
859
950
|
if (!row) return;
|
|
860
|
-
var idx
|
|
861
|
-
var detail
|
|
862
|
-
var
|
|
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 (
|
|
957
|
+
if (chev) chev.classList.toggle('open', !open);
|
|
867
958
|
});
|
|
868
959
|
|
|
869
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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-
|
|
1018
|
+
document.querySelectorAll('.test-group').forEach(function (el) {
|
|
1019
|
+
var gst = el.getAttribute('data-gst');
|
|
931
1020
|
var show = f === 'all'
|
|
932
|
-
|| (f === 'pass' &&
|
|
933
|
-
|| (f === 'fail' &&
|
|
934
|
-
|| (f === '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(
|
|
956
|
-
const
|
|
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-
|
|
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
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
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:
|
|
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(
|
|
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;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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"}
|