@bughunters/vision 1.0.4 ā 1.0.6
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 +21 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +28 -17
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +195 -87
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -130,6 +130,27 @@ The report includes side-by-side Baseline / Current / Diff views with full-scree
|
|
|
130
130
|
|
|
131
131
|
---
|
|
132
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
|
+
|
|
133
154
|
## IDE & VS Code integration
|
|
134
155
|
|
|
135
156
|
BugHunters Vision is fully compatible with the **VS Code Playwright extension** and **Playwright UI mode**.
|
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;AAoHD,wBAAsB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
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,CAqK/E"}
|
package/dist/check.js
CHANGED
|
@@ -156,16 +156,22 @@ async function runVisionCheck(opts) {
|
|
|
156
156
|
const token = projectUse['bvToken'] ?? process.env.BUGHUNTERS_VISION_TOKEN ?? undefined;
|
|
157
157
|
// Mode: off ā skip all visual testing immediately
|
|
158
158
|
if (mode === 'off') {
|
|
159
|
-
console.log(
|
|
159
|
+
console.log(`ā [BugHunters Vision] off: ${name}`);
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
162
|
const resolvedDir = path.resolve(process.cwd(), snapshotsDir);
|
|
163
163
|
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
164
|
-
// Build unique, collision-free filenames scoped to
|
|
165
|
-
// Format: <safe-test-title>--<safe-step-name>.baseline.png
|
|
164
|
+
// Build unique, collision-free filenames scoped to project + test title + step name.
|
|
165
|
+
// Format: [<safe-project>--]<safe-test-title>--<safe-step-name>.baseline.png
|
|
166
|
+
// Project prefix is required when multiple Playwright projects run the same test
|
|
167
|
+
// (e.g. widget-chromium, widget-firefox) ā without it all projects share one file.
|
|
168
|
+
const projectName = testInfo?.project?.name || undefined;
|
|
169
|
+
const safeProject = projectName ? sanitize(projectName) : '';
|
|
166
170
|
const safeTitle = testInfo?.title ? sanitize(testInfo.title) : 'unknown';
|
|
167
171
|
const safeName = sanitize(name);
|
|
168
|
-
const filePrefix =
|
|
172
|
+
const filePrefix = safeProject
|
|
173
|
+
? `${safeProject}--${safeTitle}--${safeName}`
|
|
174
|
+
: `${safeTitle}--${safeName}`;
|
|
169
175
|
// Human-readable label used in the HTML report: "Test title āŗ step name"
|
|
170
176
|
const testName = testInfo?.title ? `${testInfo.title} āŗ ${name}` : name;
|
|
171
177
|
const baselinePath = path.join(resolvedDir, `${filePrefix}.baseline.png`);
|
|
@@ -173,10 +179,10 @@ async function runVisionCheck(opts) {
|
|
|
173
179
|
if (!fs.existsSync(baselinePath) || updateBaseline) {
|
|
174
180
|
// First run ā save as baseline
|
|
175
181
|
fs.writeFileSync(baselinePath, currentBuffer);
|
|
176
|
-
console.log(
|
|
177
|
-
console.log(' ā
Test PASSED (baseline created ā no previous reference exists)\n');
|
|
182
|
+
console.log(`šø [BugHunters Vision] Baseline created: ${name}`);
|
|
178
183
|
(0, results_1.appendResult)(resolvedDir, {
|
|
179
184
|
testName,
|
|
185
|
+
projectName,
|
|
180
186
|
status: 'BASELINE_CREATED',
|
|
181
187
|
reason: 'First run detected ā screenshot saved as the new baseline.',
|
|
182
188
|
method: null,
|
|
@@ -194,10 +200,10 @@ async function runVisionCheck(opts) {
|
|
|
194
200
|
fs.writeFileSync(path.join(resolvedDir, currentFileName), currentBuffer);
|
|
195
201
|
if (diffPixels === 0) {
|
|
196
202
|
// ā
Pixel-perfect match ā skip AI call entirely
|
|
197
|
-
console.log(
|
|
198
|
-
console.log(' ā
Test PASSED (Fast Pixel Match)\n');
|
|
203
|
+
console.log(`ā
[BugHunters Vision] FAST PIXEL MATCH: ${name}`);
|
|
199
204
|
(0, results_1.appendResult)(resolvedDir, {
|
|
200
205
|
testName,
|
|
206
|
+
projectName,
|
|
201
207
|
status: 'PASS',
|
|
202
208
|
reason: 'Identical (Fast Pixel Match) ā pixel-perfect match, no AI evaluation needed.',
|
|
203
209
|
method: 'FAST_PIXEL_MATCH',
|
|
@@ -212,9 +218,9 @@ async function runVisionCheck(opts) {
|
|
|
212
218
|
const diffLabel = diffPixels === -1 ? 'dimension mismatch' : `${diffPixels} pixel(s) differ`;
|
|
213
219
|
if (mode === 'strict') {
|
|
214
220
|
// strict: fail immediately without calling AI
|
|
215
|
-
console.log(`\nā [BugHunters Vision] "${name}" ā ${diffLabel}. FAILED (strict mode ā no AI call).\n`);
|
|
216
221
|
(0, results_1.appendResult)(resolvedDir, {
|
|
217
222
|
testName,
|
|
223
|
+
projectName,
|
|
218
224
|
status: 'FAIL',
|
|
219
225
|
reason: `Pixel mismatch detected (${diffLabel}). Strict mode ā AI evaluation disabled.`,
|
|
220
226
|
method: 'FAST_PIXEL_MATCH',
|
|
@@ -223,20 +229,20 @@ async function runVisionCheck(opts) {
|
|
|
223
229
|
timestamp: new Date().toISOString(),
|
|
224
230
|
});
|
|
225
231
|
await pushToPlaywright(testInfo, name, 'FAIL', `Pixel mismatch detected (${diffLabel}). Strict mode.`, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
226
|
-
(0, test_1.expect)(false, `[BugHunters Vision
|
|
232
|
+
(0, test_1.expect)(false, `[BugHunters Vision] Visual regression detected in step "${name}": ${diffLabel} (strict mode)`).toBe(true);
|
|
227
233
|
return;
|
|
228
234
|
}
|
|
229
235
|
// ai mode ā escalate to AI for intelligent evaluation
|
|
230
|
-
console.log(
|
|
236
|
+
console.log(`š [BugHunters Vision] AI evaluating: ${name} (${diffLabel})ā¦`);
|
|
231
237
|
let result;
|
|
232
238
|
try {
|
|
233
239
|
result = await callVisionApi(baselineBuffer.toString('base64'), currentBuffer.toString('base64'), prompt, apiUrl, token);
|
|
234
240
|
}
|
|
235
241
|
catch (err) {
|
|
236
242
|
const reason = humanReadableApiError(err instanceof Error ? err.message : String(err));
|
|
237
|
-
console.log(`\nā [BugHunters Vision] ${reason}\n`);
|
|
238
243
|
(0, results_1.appendResult)(resolvedDir, {
|
|
239
244
|
testName,
|
|
245
|
+
projectName,
|
|
240
246
|
status: 'FAIL',
|
|
241
247
|
reason,
|
|
242
248
|
method: 'AI',
|
|
@@ -245,13 +251,14 @@ async function runVisionCheck(opts) {
|
|
|
245
251
|
timestamp: new Date().toISOString(),
|
|
246
252
|
});
|
|
247
253
|
await pushToPlaywright(testInfo, name, 'FAIL', reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
248
|
-
throw
|
|
254
|
+
throw new Error(`[BugHunters Vision] Visual regression detected in step "${name}": ${reason}`);
|
|
249
255
|
}
|
|
250
256
|
// ERROR: AI temporarily unavailable ā record but do NOT fail the Playwright test
|
|
251
257
|
if (result.status === 'ERROR') {
|
|
252
|
-
console.log(
|
|
258
|
+
console.log(`ā ļø [BugHunters Vision] AI error: ${name} ā ${result.reason}`);
|
|
253
259
|
(0, results_1.appendResult)(resolvedDir, {
|
|
254
260
|
testName,
|
|
261
|
+
projectName,
|
|
255
262
|
status: 'ERROR',
|
|
256
263
|
reason: result.reason,
|
|
257
264
|
method: 'AI',
|
|
@@ -262,10 +269,9 @@ async function runVisionCheck(opts) {
|
|
|
262
269
|
await pushToPlaywright(testInfo, name, 'ERROR', result.reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
263
270
|
return; // test stays green
|
|
264
271
|
}
|
|
265
|
-
const icon = result.status === 'PASS' ? 'ā
' : 'ā';
|
|
266
|
-
console.log(`${icon} [BugHunters Vision] ${result.status}: ${result.reason}\n`);
|
|
267
272
|
(0, results_1.appendResult)(resolvedDir, {
|
|
268
273
|
testName,
|
|
274
|
+
projectName,
|
|
269
275
|
status: result.status,
|
|
270
276
|
reason: result.reason,
|
|
271
277
|
method: 'AI',
|
|
@@ -274,5 +280,10 @@ async function runVisionCheck(opts) {
|
|
|
274
280
|
timestamp: new Date().toISOString(),
|
|
275
281
|
});
|
|
276
282
|
await pushToPlaywright(testInfo, name, result.status, result.reason, resolvedDir, `${filePrefix}.baseline.png`, currentFileName);
|
|
277
|
-
|
|
283
|
+
if (result.status === 'PASS') {
|
|
284
|
+
console.log(`š¤ [BugHunters Vision] AI PASS: ${name}`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
throw new Error(`[BugHunters Vision] Visual regression detected in step "${name}": ${result.reason}`);
|
|
288
|
+
}
|
|
278
289
|
}
|
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;AA4iCD,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,46 @@ 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 project + test title āāāāāāāāāāāāāāā
|
|
41
|
+
// The grouping key is "projectName||testTitle" so that the same test running
|
|
42
|
+
// on different browser projects (chromium, firefox, androidā¦) produces separate
|
|
43
|
+
// groups ā each with its own baseline files and its own row in the report.
|
|
44
|
+
function groupResults(results) {
|
|
45
|
+
const order = [];
|
|
46
|
+
const map = new Map();
|
|
47
|
+
for (const r of results) {
|
|
48
|
+
const sep = r.testName.indexOf(' āŗ ');
|
|
49
|
+
const testTitle = sep >= 0 ? r.testName.slice(0, sep) : r.testName;
|
|
50
|
+
// Composite key keeps project-variants separate while remaining stable
|
|
51
|
+
const key = r.projectName ? `${r.projectName}||${testTitle}` : testTitle;
|
|
52
|
+
if (!map.has(key)) {
|
|
53
|
+
map.set(key, []);
|
|
54
|
+
order.push(key);
|
|
55
|
+
}
|
|
56
|
+
map.get(key).push(r);
|
|
57
|
+
}
|
|
58
|
+
return order.map(key => {
|
|
59
|
+
const steps = map.get(key);
|
|
60
|
+
const firstResult = steps[0];
|
|
61
|
+
const sep = firstResult.testName.indexOf(' āŗ ');
|
|
62
|
+
const testTitle = sep >= 0 ? firstResult.testName.slice(0, sep) : firstResult.testName;
|
|
63
|
+
const projectName = firstResult.projectName;
|
|
64
|
+
const hasFail = steps.some(s => s.status === 'FAIL');
|
|
65
|
+
const hasError = steps.some(s => s.status === 'ERROR');
|
|
66
|
+
const allBase = steps.every(s => s.status === 'BASELINE_CREATED');
|
|
67
|
+
const status = hasFail ? 'FAIL' : hasError ? 'ERROR' : allBase ? 'BASELINE_CREATED' : 'PASS';
|
|
68
|
+
return {
|
|
69
|
+
testTitle,
|
|
70
|
+
projectName,
|
|
71
|
+
status,
|
|
72
|
+
passedSteps: steps.filter(s => s.status === 'PASS').length,
|
|
73
|
+
failedSteps: steps.filter(s => s.status === 'FAIL').length,
|
|
74
|
+
erroredSteps: steps.filter(s => s.status === 'ERROR').length,
|
|
75
|
+
baselineSteps: steps.filter(s => s.status === 'BASELINE_CREATED').length,
|
|
76
|
+
steps,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
40
80
|
// āāā CSS āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
41
81
|
function getCss() {
|
|
42
82
|
return `
|
|
@@ -192,27 +232,36 @@ body {
|
|
|
192
232
|
|
|
193
233
|
/* āā Test list āā */
|
|
194
234
|
.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
235
|
|
|
200
|
-
|
|
236
|
+
/* āā Test group ā one Playwright test āā */
|
|
237
|
+
.test-group {
|
|
238
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
239
|
+
border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-sm);
|
|
240
|
+
}
|
|
241
|
+
.test-group.s-fail { border-left: 3px solid var(--fail); }
|
|
242
|
+
.test-group.s-base { border-left: 3px solid var(--base); }
|
|
243
|
+
.test-group.s-warn { border-left: 3px solid var(--pixel); }
|
|
244
|
+
|
|
245
|
+
/* āā Group header row (clickable, expands step list) āā */
|
|
246
|
+
.group-row {
|
|
201
247
|
display: flex; align-items: center; gap: 11px;
|
|
202
248
|
padding: 12px 14px; cursor: pointer;
|
|
203
249
|
transition: background .1s; user-select: none;
|
|
204
250
|
}
|
|
205
|
-
.
|
|
251
|
+
.group-row:hover { background: var(--surface-2); }
|
|
206
252
|
|
|
253
|
+
/* āā Status dot āā */
|
|
207
254
|
.s-dot {
|
|
208
255
|
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
209
256
|
margin-top: 1px;
|
|
210
257
|
}
|
|
258
|
+
.s-dot-sm { width: 6px; height: 6px; margin-top: 0; }
|
|
211
259
|
.s-pass .s-dot { background: var(--pass); }
|
|
212
260
|
.s-fail .s-dot { background: var(--fail); }
|
|
213
261
|
.s-base .s-dot { background: var(--base); }
|
|
214
262
|
.s-warn .s-dot { background: var(--pixel); }
|
|
215
263
|
|
|
264
|
+
/* āā Group text āā */
|
|
216
265
|
.test-meta { flex: 1; min-width: 0; }
|
|
217
266
|
.test-name {
|
|
218
267
|
font-family: ui-monospace, 'SFMono-Regular', 'Fira Code', monospace;
|
|
@@ -224,12 +273,14 @@ body {
|
|
|
224
273
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
225
274
|
}
|
|
226
275
|
|
|
276
|
+
/* āā Badges āā */
|
|
227
277
|
.badges { display: flex; gap: 5px; align-items: center; flex-shrink: 0; }
|
|
228
278
|
.badge {
|
|
229
279
|
font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 4px;
|
|
230
280
|
letter-spacing: .4px; font-family: ui-monospace, monospace; white-space: nowrap;
|
|
231
281
|
}
|
|
232
282
|
.badge.m-method { background: var(--surface-2); color: var(--text-3); border: 1px solid var(--border); }
|
|
283
|
+
.badge.m-proj { background: var(--surface-2); color: var(--text-2); border: 1px solid var(--border); font-family: inherit; letter-spacing: 0; }
|
|
233
284
|
.badge.m-ai { background: var(--accent-soft); color: var(--accent); border: 1px solid var(--accent-border); }
|
|
234
285
|
.badge.m-warn { background: var(--pixel-soft); color: var(--pixel); border: 1px solid var(--pixel-border); }
|
|
235
286
|
.badge.m-pixel { background: var(--pixel-soft); color: var(--pixel); border: 1px solid var(--pixel-border); }
|
|
@@ -240,9 +291,35 @@ body {
|
|
|
240
291
|
.chev { font-size: 16px; color: var(--text-3); flex-shrink: 0; line-height: 1; transition: transform .2s; display: inline-block; }
|
|
241
292
|
.chev.open { transform: rotate(90deg); }
|
|
242
293
|
|
|
243
|
-
/* āā
|
|
244
|
-
.
|
|
294
|
+
/* āā Step list (expands when group is open) āā */
|
|
295
|
+
.group-steps { display: none; }
|
|
296
|
+
|
|
297
|
+
/* āā Individual step āā */
|
|
298
|
+
.step-item { border-top: 1px solid var(--border); }
|
|
299
|
+
|
|
300
|
+
/* āā Step header row (clickable, expands detail) āā */
|
|
301
|
+
.step-row {
|
|
302
|
+
display: flex; align-items: center; gap: 9px;
|
|
303
|
+
padding: 9px 14px 9px 34px; cursor: pointer;
|
|
304
|
+
transition: background .1s; user-select: none;
|
|
305
|
+
}
|
|
306
|
+
.step-row:hover { background: var(--surface-2); }
|
|
307
|
+
|
|
308
|
+
/* āā Step text āā */
|
|
309
|
+
.step-meta { flex: 1; min-width: 0; }
|
|
310
|
+
.step-name {
|
|
311
|
+
font-family: ui-monospace, 'SFMono-Regular', 'Fira Code', monospace;
|
|
312
|
+
font-size: 12px; font-weight: 500; color: var(--text-2);
|
|
313
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* āā Step detail (verdict + images) āā */
|
|
317
|
+
.step-detail {
|
|
318
|
+
display: none; border-top: 1px solid var(--border);
|
|
319
|
+
padding: 16px 14px 16px 34px;
|
|
320
|
+
}
|
|
245
321
|
|
|
322
|
+
/* āā Verdict box āā */
|
|
246
323
|
.verdict {
|
|
247
324
|
background: var(--accent-soft); border-left: 3px solid var(--accent);
|
|
248
325
|
border-radius: 0 6px 6px 0; padding: 12px 14px; margin-bottom: 18px;
|
|
@@ -267,9 +344,7 @@ body {
|
|
|
267
344
|
.view-tab.diff-tab.on { color: var(--fail); }
|
|
268
345
|
|
|
269
346
|
/* āā Image views āā */
|
|
270
|
-
.img-section { }
|
|
271
347
|
.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
348
|
.view-img {
|
|
274
349
|
width: 100%; display: block; border-radius: 6px; border: 1px solid var(--border);
|
|
275
350
|
max-height: 360px; object-fit: contain; object-position: top center;
|
|
@@ -380,8 +455,8 @@ function getJs() {
|
|
|
380
455
|
(function () {
|
|
381
456
|
'use strict';
|
|
382
457
|
|
|
383
|
-
var
|
|
384
|
-
var META
|
|
458
|
+
var GROUPS = JSON.parse(document.getElementById('bv-g').textContent);
|
|
459
|
+
var META = JSON.parse(document.getElementById('bv-m').textContent);
|
|
385
460
|
|
|
386
461
|
/* āā State āā */
|
|
387
462
|
var state = {
|
|
@@ -423,23 +498,19 @@ function getJs() {
|
|
|
423
498
|
|
|
424
499
|
/* āā Pixel diff engine (100% client-side, zero server calls) āā */
|
|
425
500
|
|
|
426
|
-
/* Weighted squared RGB distance ā perceptually accurate, fast (no sqrt needed) */
|
|
427
501
|
function colorDist(r1, g1, b1, r2, g2, b2) {
|
|
428
502
|
var rd = r1 - r2, gd = g1 - g2, bd = b1 - b2;
|
|
429
503
|
return rd * rd * 0.299 + gd * gd * 0.587 + bd * bd * 0.114;
|
|
430
504
|
}
|
|
431
505
|
|
|
432
|
-
/* Render diff image: changed pixels = neon red, unchanged = desaturated baseline */
|
|
433
506
|
function renderDiff(d1, d2, w, h) {
|
|
434
507
|
var n = w * h * 4;
|
|
435
508
|
var out = new Uint8ClampedArray(n);
|
|
436
|
-
var thr = 900;
|
|
509
|
+
var thr = 900;
|
|
437
510
|
for (var i = 0; i < n; i += 4) {
|
|
438
511
|
if (colorDist(d1[i], d1[i+1], d1[i+2], d2[i], d2[i+1], d2[i+2]) > thr) {
|
|
439
|
-
/* neon red highlight */
|
|
440
512
|
out[i] = 220; out[i+1] = 38; out[i+2] = 38; out[i+3] = 255;
|
|
441
513
|
} else {
|
|
442
|
-
/* desaturated baseline ā guides the eye to red pixels */
|
|
443
514
|
var lum = (d1[i] * 77 + d1[i+1] * 150 + d1[i+2] * 29) >> 8;
|
|
444
515
|
out[i] = lum; out[i+1] = lum; out[i+2] = lum; out[i+3] = 210;
|
|
445
516
|
}
|
|
@@ -447,7 +518,6 @@ function getJs() {
|
|
|
447
518
|
return new ImageData(out, w, h);
|
|
448
519
|
}
|
|
449
520
|
|
|
450
|
-
/* Load image and extract raw pixel data via canvas */
|
|
451
521
|
function loadImgPixels(url, cb) {
|
|
452
522
|
var img = new Image();
|
|
453
523
|
img.crossOrigin = 'anonymous';
|
|
@@ -469,7 +539,6 @@ function getJs() {
|
|
|
469
539
|
var diffCache = {};
|
|
470
540
|
|
|
471
541
|
function genDiff(idx, baseUrl, curUrl, container) {
|
|
472
|
-
/* Return cached result if already computed */
|
|
473
542
|
if (diffCache[idx]) {
|
|
474
543
|
var cached = diffCache[idx];
|
|
475
544
|
var clone = document.createElement('canvas');
|
|
@@ -525,7 +594,6 @@ function getJs() {
|
|
|
525
594
|
|
|
526
595
|
function lbReset() { lb.scale = 1; lb.tx = 0; lb.ty = 0; lbApply(); }
|
|
527
596
|
|
|
528
|
-
/* Zoom towards a viewport-center-relative point (mx, my) */
|
|
529
597
|
function lbZoom(factor, mx, my) {
|
|
530
598
|
var ns = Math.max(lb.minScale, Math.min(lb.maxScale, lb.scale * factor));
|
|
531
599
|
var r = ns / lb.scale;
|
|
@@ -535,7 +603,6 @@ function getJs() {
|
|
|
535
603
|
lbApply();
|
|
536
604
|
}
|
|
537
605
|
|
|
538
|
-
/* Auto-fit: scale image to fill viewport on first open */
|
|
539
606
|
function lbAutoFit(w, h) {
|
|
540
607
|
var vw = lb.inner.clientWidth - 48;
|
|
541
608
|
var vh = lb.inner.clientHeight - 48;
|
|
@@ -556,7 +623,6 @@ function getJs() {
|
|
|
556
623
|
el.onload = function () { lbAutoFit(el.naturalWidth, el.naturalHeight); };
|
|
557
624
|
el.src = payload;
|
|
558
625
|
} else {
|
|
559
|
-
/* diff canvas */
|
|
560
626
|
var src = diffCache[payload];
|
|
561
627
|
if (!src) return;
|
|
562
628
|
el = document.createElement('canvas');
|
|
@@ -603,28 +669,19 @@ function getJs() {
|
|
|
603
669
|
lb.wrap = ov.querySelector('.lb-img-wrap');
|
|
604
670
|
lb.zoomLbl = ov.querySelector('.lb-zoom-label');
|
|
605
671
|
|
|
606
|
-
/* close button */
|
|
607
672
|
ov.querySelector('.lb-close').addEventListener('click', lbClose);
|
|
608
|
-
|
|
609
|
-
/* click on backdrop (not on image) */
|
|
610
673
|
lb.inner.addEventListener('click', function (e) {
|
|
611
674
|
if (e.target === lb.inner) lbClose();
|
|
612
675
|
});
|
|
613
|
-
|
|
614
|
-
/* double-click: reset zoom */
|
|
615
676
|
lb.inner.addEventListener('dblclick', function (e) {
|
|
616
677
|
e.preventDefault();
|
|
617
678
|
lbReset();
|
|
618
679
|
});
|
|
619
|
-
|
|
620
|
-
/* scroll-to-zoom */
|
|
621
680
|
lb.inner.addEventListener('wheel', function (e) {
|
|
622
681
|
e.preventDefault();
|
|
623
682
|
var p = lbCenterPos(e);
|
|
624
683
|
lbZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, p.x, p.y);
|
|
625
684
|
}, { passive: false });
|
|
626
|
-
|
|
627
|
-
/* mouse drag: pan */
|
|
628
685
|
lb.inner.addEventListener('mousedown', function (e) {
|
|
629
686
|
if (e.button !== 0) return;
|
|
630
687
|
lb.dragging = true;
|
|
@@ -644,8 +701,6 @@ function getJs() {
|
|
|
644
701
|
lb.dragging = false;
|
|
645
702
|
lb.wrap.classList.remove('dragging');
|
|
646
703
|
});
|
|
647
|
-
|
|
648
|
-
/* touch: pan + pinch-to-zoom */
|
|
649
704
|
lb.inner.addEventListener('touchstart', function (e) {
|
|
650
705
|
if (e.touches.length === 2) {
|
|
651
706
|
var dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
@@ -674,8 +729,6 @@ function getJs() {
|
|
|
674
729
|
}
|
|
675
730
|
}, { passive: false });
|
|
676
731
|
lb.inner.addEventListener('touchend', function () { lb.dragging = false; }, { passive: true });
|
|
677
|
-
|
|
678
|
-
/* keyboard shortcuts */
|
|
679
732
|
document.addEventListener('keydown', function (e) {
|
|
680
733
|
if (!lb.overlay.classList.contains('open')) return;
|
|
681
734
|
if (e.key === 'Escape') lbClose();
|
|
@@ -684,6 +737,7 @@ function getJs() {
|
|
|
684
737
|
}
|
|
685
738
|
|
|
686
739
|
/* āā HTML builders āā */
|
|
740
|
+
|
|
687
741
|
function statusInfo(r) {
|
|
688
742
|
if (r.status === 'PASS') return { cls:'s-pass', badge:'m-pass', label:'PASS' };
|
|
689
743
|
if (r.status === 'FAIL') return { cls:'s-fail', badge:'m-fail', label:'FAIL' };
|
|
@@ -749,40 +803,79 @@ function getJs() {
|
|
|
749
803
|
+ tabs + basePanel + curPanel + diffPanel;
|
|
750
804
|
}
|
|
751
805
|
|
|
752
|
-
|
|
753
|
-
|
|
806
|
+
/* Build a single visual-check step row + expandable detail */
|
|
807
|
+
function buildStep(r) {
|
|
808
|
+
var s = statusInfo(r);
|
|
809
|
+
var method = methodLabel(r);
|
|
810
|
+
var methodCls = methodBadgeCls(r);
|
|
811
|
+
|
|
812
|
+
/* Extract step name: "Test title āŗ step name" ā "step name" */
|
|
813
|
+
var raw = r.testName || '';
|
|
814
|
+
var sepIdx = raw.indexOf(' \u203a ');
|
|
815
|
+
var stepName = sepIdx >= 0 ? raw.slice(sepIdx + 3) : raw;
|
|
816
|
+
|
|
754
817
|
var verdictLbl = r.method === 'FAST_PIXEL_MATCH'
|
|
755
818
|
? '\u26A1 Fast Pixel Match'
|
|
756
819
|
: '\uD83E\uDD16 AI Verdict';
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
820
|
+
var imgWrap = r.status === 'PASS' ? 'img-section pass-imgs' : 'img-section';
|
|
821
|
+
|
|
822
|
+
return '<div class="step-item ' + s.cls + '">'
|
|
823
|
+
+ '<div class="step-row" data-s="' + r.index + '">'
|
|
824
|
+
+ '<div class="s-dot s-dot-sm"></div>'
|
|
825
|
+
+ '<div class="step-meta">'
|
|
826
|
+
+ '<div class="step-name">' + esc(stepName) + '</div>'
|
|
827
|
+
+ '</div>'
|
|
828
|
+
+ '<div class="badges">'
|
|
829
|
+
+ '<span class="badge ' + methodCls + '">' + method + '</span>'
|
|
830
|
+
+ '<span class="badge ' + s.badge + '">' + s.label + '</span>'
|
|
831
|
+
+ '</div>'
|
|
832
|
+
+ '<span class="chev" id="sc' + r.index + '">\u203a</span>'
|
|
761
833
|
+ '</div>'
|
|
762
|
-
+ '<div class="
|
|
763
|
-
|
|
834
|
+
+ '<div class="step-detail" id="sd' + r.index + '">'
|
|
835
|
+
+ '<div class="verdict">'
|
|
836
|
+
+ '<div class="verdict-lbl">' + verdictLbl + '</div>'
|
|
837
|
+
+ '<p class="verdict-txt">\u201C' + esc(r.reason) + '\u201D</p>'
|
|
838
|
+
+ '</div>'
|
|
839
|
+
+ '<div class="' + imgWrap + '">' + buildImages(r) + '</div>'
|
|
840
|
+
+ '</div>'
|
|
841
|
+
+ '</div>';
|
|
764
842
|
}
|
|
765
843
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
var
|
|
769
|
-
var
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
844
|
+
/* Build a test group row (Playwright test + its visual-check steps) */
|
|
845
|
+
function buildGroup(g, gIdx) {
|
|
846
|
+
var s = statusInfo(g);
|
|
847
|
+
var n = g.steps.length;
|
|
848
|
+
|
|
849
|
+
/* Snip: "5 visual checks Ā· 2 failed" or "3 visual checks Ā· all passed" */
|
|
850
|
+
var parts = [];
|
|
851
|
+
if (g.failedSteps > 0) parts.push(g.failedSteps + ' failed');
|
|
852
|
+
if (g.erroredSteps > 0) parts.push(g.erroredSteps + (g.erroredSteps !== 1 ? ' errors' : ' error'));
|
|
853
|
+
if (g.baselineSteps > 0 && g.failedSteps === 0 && g.erroredSteps === 0)
|
|
854
|
+
parts.push('baselines created');
|
|
855
|
+
var checkWord = n === 1 ? 'visual check' : 'visual checks';
|
|
856
|
+
var snip = n + ' ' + checkWord
|
|
857
|
+
+ (parts.length > 0 ? ' \u00b7 ' + parts.join(' \u00b7 ') : ' \u00b7 all passed');
|
|
858
|
+
|
|
859
|
+
var stepsHtml = g.steps.map(buildStep).join('');
|
|
860
|
+
var projBadge = g.projectName
|
|
861
|
+
? '<span class="badge m-proj">' + esc(g.projectName) + '</span>'
|
|
862
|
+
: '';
|
|
863
|
+
|
|
864
|
+
return '<div class="test-group ' + s.cls + '" data-gst="' + g.status + '">'
|
|
865
|
+
+ '<div class="group-row" data-g="' + gIdx + '">'
|
|
773
866
|
+ '<div class="s-dot"></div>'
|
|
774
867
|
+ '<div class="test-meta">'
|
|
775
|
-
+ '<div class="test-name">' + esc(
|
|
776
|
-
+ '<div class="test-snip">'
|
|
868
|
+
+ '<div class="test-name">' + esc(g.testTitle) + '</div>'
|
|
869
|
+
+ '<div class="test-snip">' + esc(snip) + '</div>'
|
|
777
870
|
+ '</div>'
|
|
778
871
|
+ '<div class="badges">'
|
|
779
|
-
+
|
|
780
|
-
+ '<span class="badge ' + s.badge
|
|
872
|
+
+ projBadge
|
|
873
|
+
+ '<span class="badge ' + s.badge + '">' + s.label + '</span>'
|
|
781
874
|
+ '</div>'
|
|
782
|
-
+ '<span class="chev" id="
|
|
875
|
+
+ '<span class="chev" id="gc' + gIdx + '">\u203a</span>'
|
|
783
876
|
+ '</div>'
|
|
784
|
-
+
|
|
785
|
-
|
|
877
|
+
+ '<div class="group-steps" id="gs' + gIdx + '">' + stepsHtml + '</div>'
|
|
878
|
+
+ '</div>';
|
|
786
879
|
}
|
|
787
880
|
|
|
788
881
|
function buildThemeBtns() {
|
|
@@ -812,7 +905,7 @@ function getJs() {
|
|
|
812
905
|
var warnFilter = hasErrors
|
|
813
906
|
? '<button class="f-btn" data-f="warn">Errors (' + META.errored + ')</button>'
|
|
814
907
|
: '';
|
|
815
|
-
var items
|
|
908
|
+
var items = GROUPS.map(function (g, i) { return buildGroup(g, i); }).join('');
|
|
816
909
|
|
|
817
910
|
return '<header class="hdr">'
|
|
818
911
|
+ '<div class="wrap"><div class="hdr-inner">'
|
|
@@ -826,7 +919,7 @@ function getJs() {
|
|
|
826
919
|
+ '</header>'
|
|
827
920
|
+ '<div class="wrap">'
|
|
828
921
|
+ '<div class="summary">'
|
|
829
|
-
+ '<div class="stat"><div class="stat-lbl">
|
|
922
|
+
+ '<div class="stat"><div class="stat-lbl">Tests</div><div class="stat-val">' + META.total + '</div></div>'
|
|
830
923
|
+ '<div class="stat s-pass"><div class="stat-lbl">Passed</div><div class="stat-val">' + META.passed + '</div></div>'
|
|
831
924
|
+ '<div class="stat' + failCls + '"><div class="stat-lbl">Failed</div><div class="stat-val">' + META.failed + '</div></div>'
|
|
832
925
|
+ warnStat
|
|
@@ -853,35 +946,45 @@ function getJs() {
|
|
|
853
946
|
function bind() {
|
|
854
947
|
var tlist = document.getElementById('tlist');
|
|
855
948
|
|
|
856
|
-
/* accordion expand/collapse
|
|
949
|
+
/* Level 1: test group accordion ā expand/collapse step list */
|
|
950
|
+
tlist.addEventListener('click', function (e) {
|
|
951
|
+
var row = e.target.closest('[data-g]');
|
|
952
|
+
if (!row) return;
|
|
953
|
+
var idx = row.getAttribute('data-g');
|
|
954
|
+
var steps = document.getElementById('gs' + idx);
|
|
955
|
+
var chev = document.getElementById('gc' + idx);
|
|
956
|
+
if (!steps) return;
|
|
957
|
+
var open = steps.style.display !== 'none';
|
|
958
|
+
steps.style.display = open ? 'none' : 'block';
|
|
959
|
+
if (chev) chev.classList.toggle('open', !open);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
/* Level 2: step accordion ā expand/collapse verdict + images */
|
|
857
963
|
tlist.addEventListener('click', function (e) {
|
|
858
|
-
var row = e.target.closest('[data-
|
|
964
|
+
var row = e.target.closest('[data-s]');
|
|
859
965
|
if (!row) return;
|
|
860
|
-
var idx
|
|
861
|
-
var detail
|
|
862
|
-
var
|
|
966
|
+
var idx = row.getAttribute('data-s');
|
|
967
|
+
var detail = document.getElementById('sd' + idx);
|
|
968
|
+
var chev = document.getElementById('sc' + idx);
|
|
863
969
|
if (!detail) return;
|
|
864
970
|
var open = detail.style.display !== 'none';
|
|
865
971
|
detail.style.display = open ? 'none' : 'block';
|
|
866
|
-
if (
|
|
972
|
+
if (chev) chev.classList.toggle('open', !open);
|
|
867
973
|
});
|
|
868
974
|
|
|
869
|
-
/*
|
|
975
|
+
/* View-tab switching: Baseline / Current / Diff */
|
|
870
976
|
tlist.addEventListener('click', function (e) {
|
|
871
977
|
var tab = e.target.closest('[data-tab]');
|
|
872
978
|
if (!tab) return;
|
|
873
979
|
var tidx = tab.getAttribute('data-tidx');
|
|
874
980
|
var tabName = tab.getAttribute('data-tab');
|
|
875
|
-
/* update active tab */
|
|
876
981
|
tab.closest('.view-tabs').querySelectorAll('.view-tab').forEach(function (t) {
|
|
877
982
|
t.classList.toggle('on', t === tab);
|
|
878
983
|
});
|
|
879
|
-
/* show correct panel, hide others */
|
|
880
984
|
['base', 'cur', 'diff'].forEach(function (key) {
|
|
881
985
|
var panel = document.getElementById('v' + key + '-' + tidx);
|
|
882
986
|
if (panel) panel.style.display = key === tabName ? '' : 'none';
|
|
883
987
|
});
|
|
884
|
-
/* lazy diff generation ā only on first click */
|
|
885
988
|
if (tabName === 'diff') {
|
|
886
989
|
var dp = document.getElementById('vdiff-' + tidx);
|
|
887
990
|
if (dp && dp.getAttribute('data-ready') === '0') {
|
|
@@ -891,7 +994,7 @@ function getJs() {
|
|
|
891
994
|
}
|
|
892
995
|
});
|
|
893
996
|
|
|
894
|
-
/*
|
|
997
|
+
/* Image / diff-canvas click ā open lightbox */
|
|
895
998
|
tlist.addEventListener('click', function (e) {
|
|
896
999
|
var el = e.target.closest('[data-lb]');
|
|
897
1000
|
if (!el) return;
|
|
@@ -900,7 +1003,7 @@ function getJs() {
|
|
|
900
1003
|
if (type === 'diff') lbOpen('diff', el.getAttribute('data-lb-idx'));
|
|
901
1004
|
});
|
|
902
1005
|
|
|
903
|
-
/*
|
|
1006
|
+
/* Theme */
|
|
904
1007
|
document.getElementById('t-toggle').addEventListener('click', function (e) {
|
|
905
1008
|
var btn = e.target.closest('[data-th]');
|
|
906
1009
|
if (!btn) return;
|
|
@@ -912,7 +1015,7 @@ function getJs() {
|
|
|
912
1015
|
});
|
|
913
1016
|
});
|
|
914
1017
|
|
|
915
|
-
/*
|
|
1018
|
+
/* Screenshots on pass toggle */
|
|
916
1019
|
document.getElementById('img-btn').addEventListener('click', function () {
|
|
917
1020
|
state.showPassImg = !state.showPassImg;
|
|
918
1021
|
localStorage.setItem('bv-pass-img', String(state.showPassImg));
|
|
@@ -920,18 +1023,20 @@ function getJs() {
|
|
|
920
1023
|
this.classList.toggle('on', state.showPassImg);
|
|
921
1024
|
});
|
|
922
1025
|
|
|
923
|
-
/*
|
|
1026
|
+
/* Filter ā operates at test-group level */
|
|
924
1027
|
document.querySelectorAll('[data-f]').forEach(function (btn) {
|
|
925
1028
|
btn.addEventListener('click', function () {
|
|
926
1029
|
var f = btn.getAttribute('data-f');
|
|
927
1030
|
document.querySelectorAll('[data-f]').forEach(function (b) {
|
|
928
1031
|
b.classList.toggle('on', b.getAttribute('data-f') === f);
|
|
929
1032
|
});
|
|
930
|
-
document.querySelectorAll('.test-
|
|
1033
|
+
document.querySelectorAll('.test-group').forEach(function (el) {
|
|
1034
|
+
var gst = el.getAttribute('data-gst');
|
|
931
1035
|
var show = f === 'all'
|
|
932
|
-
|| (f === 'pass' &&
|
|
933
|
-
|| (f === 'fail' &&
|
|
934
|
-
|| (f === 'warn' &&
|
|
1036
|
+
|| (f === 'pass' && gst === 'PASS')
|
|
1037
|
+
|| (f === 'fail' && gst === 'FAIL')
|
|
1038
|
+
|| (f === 'warn' && gst === 'ERROR')
|
|
1039
|
+
|| (f === 'base' && gst === 'BASELINE_CREATED');
|
|
935
1040
|
el.style.display = show ? '' : 'none';
|
|
936
1041
|
});
|
|
937
1042
|
});
|
|
@@ -952,8 +1057,8 @@ function getJs() {
|
|
|
952
1057
|
`.trim();
|
|
953
1058
|
}
|
|
954
1059
|
// āāā HTML Assembly āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
955
|
-
function generateHTML(
|
|
956
|
-
const
|
|
1060
|
+
function generateHTML(groups, meta) {
|
|
1061
|
+
const groupsJson = JSON.stringify(groups);
|
|
957
1062
|
const metaJson = JSON.stringify(meta);
|
|
958
1063
|
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
1064
|
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 +1082,7 @@ function generateHTML(results, meta) {
|
|
|
977
1082
|
</head>
|
|
978
1083
|
<body>
|
|
979
1084
|
<div id="root"></div>
|
|
980
|
-
<script type="application/json" id="bv-
|
|
1085
|
+
<script type="application/json" id="bv-g">${groupsJson}</script>
|
|
981
1086
|
<script type="application/json" id="bv-m">${metaJson}</script>
|
|
982
1087
|
<script>${getJs()}</script>
|
|
983
1088
|
</body>
|
|
@@ -1010,13 +1115,14 @@ class BugHuntersVisionReporter {
|
|
|
1010
1115
|
return;
|
|
1011
1116
|
}
|
|
1012
1117
|
const results = JSON.parse(raw);
|
|
1013
|
-
const
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1118
|
+
const groups = groupResults(results);
|
|
1119
|
+
const passed = groups.filter(g => g.status === 'PASS').length;
|
|
1120
|
+
const failed = groups.filter(g => g.status === 'FAIL').length;
|
|
1121
|
+
const baselines = groups.filter(g => g.status === 'BASELINE_CREATED').length;
|
|
1122
|
+
const errored = groups.filter(g => g.status === 'ERROR').length;
|
|
1017
1123
|
const meta = {
|
|
1018
1124
|
generatedAt: new Date().toISOString(),
|
|
1019
|
-
total:
|
|
1125
|
+
total: groups.length,
|
|
1020
1126
|
passed,
|
|
1021
1127
|
failed,
|
|
1022
1128
|
baselines,
|
|
@@ -1024,11 +1130,13 @@ class BugHuntersVisionReporter {
|
|
|
1024
1130
|
};
|
|
1025
1131
|
const reportPath = path.join(this.reportDir, 'index.html');
|
|
1026
1132
|
fs.mkdirSync(this.reportDir, { recursive: true });
|
|
1027
|
-
fs.writeFileSync(reportPath, generateHTML(
|
|
1133
|
+
fs.writeFileSync(reportPath, generateHTML(groups, meta), 'utf-8');
|
|
1134
|
+
const testWord = groups.length === 1 ? 'test' : 'tests';
|
|
1135
|
+
const stepWord = results.length === 1 ? 'check' : 'checks';
|
|
1028
1136
|
console.log('\n\uD83D\uDC1E BugHunters Vision Report generated:');
|
|
1029
1137
|
console.log(' \uD83D\uDCC4 ' + reportPath);
|
|
1030
1138
|
const erroredStr = errored > 0 ? ' \u26A0\uFE0F ' + errored + ' errored' : '';
|
|
1031
|
-
console.log(' \u2705 ' + passed + ' passed \u274C ' + failed + ' failed' + erroredStr + ' (' + results.length + ' total)');
|
|
1139
|
+
console.log(' \u2705 ' + passed + ' passed \u274C ' + failed + ' failed' + erroredStr + ' (' + groups.length + ' ' + testWord + ' \u00B7 ' + results.length + ' ' + stepWord + ' total)');
|
|
1032
1140
|
// Show remaining API balance if an AI call was made this run
|
|
1033
1141
|
const balance = (0, check_1.getLastRemainingBalance)();
|
|
1034
1142
|
if (balance !== null) {
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type VisionMode = 'ai' | 'strict' | 'off';
|
|
|
2
2
|
export interface TestResult {
|
|
3
3
|
index: number;
|
|
4
4
|
testName: string;
|
|
5
|
+
projectName?: string;
|
|
5
6
|
status: 'PASS' | 'FAIL' | 'BASELINE_CREATED' | 'ERROR';
|
|
6
7
|
reason: string;
|
|
7
8
|
method: 'AI' | 'FAST_PIXEL_MATCH' | null;
|
|
@@ -9,6 +10,16 @@ export interface TestResult {
|
|
|
9
10
|
currentFile: string | null;
|
|
10
11
|
timestamp: string;
|
|
11
12
|
}
|
|
13
|
+
export interface TestGroup {
|
|
14
|
+
testTitle: string;
|
|
15
|
+
projectName?: string;
|
|
16
|
+
status: 'PASS' | 'FAIL' | 'BASELINE_CREATED' | 'ERROR';
|
|
17
|
+
passedSteps: number;
|
|
18
|
+
failedSteps: number;
|
|
19
|
+
erroredSteps: number;
|
|
20
|
+
baselineSteps: number;
|
|
21
|
+
steps: TestResult[];
|
|
22
|
+
}
|
|
12
23
|
export interface Meta {
|
|
13
24
|
generatedAt: string;
|
|
14
25
|
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,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,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,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,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
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "InfoSec-friendly AI Visual Testing plugin for Playwright.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"reporter.d.ts",
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
|
-
"homepage": "https://bughunters.
|
|
27
|
+
"homepage": "https://bughunters.dev",
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
30
|
"url": "git+https://github.com/bughunters/vision.git"
|