cbrowser 7.1.1 → 7.3.0
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/dist/browser.d.ts +37 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +798 -1
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +296 -3
- package/dist/cli.js.map +1 -1
- package/dist/types.d.ts +253 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -54,6 +54,15 @@ exports.runCrossBrowserTest = runCrossBrowserTest;
|
|
|
54
54
|
exports.runCrossBrowserSuite = runCrossBrowserSuite;
|
|
55
55
|
exports.formatCrossBrowserReport = formatCrossBrowserReport;
|
|
56
56
|
exports.generateCrossBrowserHtmlReport = generateCrossBrowserHtmlReport;
|
|
57
|
+
exports.runResponsiveTest = runResponsiveTest;
|
|
58
|
+
exports.runResponsiveSuite = runResponsiveSuite;
|
|
59
|
+
exports.formatResponsiveReport = formatResponsiveReport;
|
|
60
|
+
exports.generateResponsiveHtmlReport = generateResponsiveHtmlReport;
|
|
61
|
+
exports.listViewportPresets = listViewportPresets;
|
|
62
|
+
exports.runABComparison = runABComparison;
|
|
63
|
+
exports.runABSuite = runABSuite;
|
|
64
|
+
exports.formatABReport = formatABReport;
|
|
65
|
+
exports.generateABHtmlReport = generateABHtmlReport;
|
|
57
66
|
const playwright_1 = require("playwright");
|
|
58
67
|
const fs_1 = require("fs");
|
|
59
68
|
const path_1 = require("path");
|
|
@@ -6208,7 +6217,795 @@ function generateCrossBrowserHtmlReport(suiteResult) {
|
|
|
6208
6217
|
</div>
|
|
6209
6218
|
|
|
6210
6219
|
<footer>
|
|
6211
|
-
Generated by CBrowser v7.
|
|
6220
|
+
Generated by CBrowser v7.2.0 | Test completed in ${(duration / 1000).toFixed(1)}s
|
|
6221
|
+
</footer>
|
|
6222
|
+
</div>
|
|
6223
|
+
</body>
|
|
6224
|
+
</html>`;
|
|
6225
|
+
}
|
|
6226
|
+
// ============================================================================
|
|
6227
|
+
// Responsive Visual Testing (v7.2.0)
|
|
6228
|
+
// ============================================================================
|
|
6229
|
+
/**
|
|
6230
|
+
* Get viewport presets by name or return custom preset
|
|
6231
|
+
*/
|
|
6232
|
+
function resolveViewports(viewports) {
|
|
6233
|
+
if (!viewports || viewports.length === 0) {
|
|
6234
|
+
// Default: mobile, tablet, desktop
|
|
6235
|
+
return types_js_1.VIEWPORT_PRESETS.filter(v => v.name === "mobile" || v.name === "tablet" || v.name === "desktop");
|
|
6236
|
+
}
|
|
6237
|
+
return viewports.map(v => {
|
|
6238
|
+
if (typeof v === "string") {
|
|
6239
|
+
const preset = types_js_1.VIEWPORT_PRESETS.find(p => p.name === v);
|
|
6240
|
+
if (!preset) {
|
|
6241
|
+
throw new Error(`Unknown viewport preset: ${v}. Available: ${types_js_1.VIEWPORT_PRESETS.map(p => p.name).join(", ")}`);
|
|
6242
|
+
}
|
|
6243
|
+
return preset;
|
|
6244
|
+
}
|
|
6245
|
+
return v;
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6248
|
+
/**
|
|
6249
|
+
* Get the path for responsive testing screenshots
|
|
6250
|
+
*/
|
|
6251
|
+
function getResponsiveScreenshotsPath() {
|
|
6252
|
+
const basePath = process.cwd();
|
|
6253
|
+
const screenshotsPath = (0, path_1.join)(basePath, ".cbrowser", "responsive");
|
|
6254
|
+
if (!(0, fs_1.existsSync)(screenshotsPath)) {
|
|
6255
|
+
(0, fs_1.mkdirSync)(screenshotsPath, { recursive: true });
|
|
6256
|
+
}
|
|
6257
|
+
return screenshotsPath;
|
|
6258
|
+
}
|
|
6259
|
+
/**
|
|
6260
|
+
* Capture screenshot at a specific viewport
|
|
6261
|
+
*/
|
|
6262
|
+
async function captureAtViewport(url, viewport, options = {}) {
|
|
6263
|
+
const startTime = Date.now();
|
|
6264
|
+
const browser = new CBrowser({
|
|
6265
|
+
viewportWidth: viewport.width,
|
|
6266
|
+
viewportHeight: viewport.height,
|
|
6267
|
+
});
|
|
6268
|
+
try {
|
|
6269
|
+
await browser.launch();
|
|
6270
|
+
const page = await browser.getPage();
|
|
6271
|
+
// Set mobile emulation if needed
|
|
6272
|
+
if (viewport.isMobile || viewport.hasTouch) {
|
|
6273
|
+
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
6274
|
+
}
|
|
6275
|
+
await browser.navigate(url);
|
|
6276
|
+
// Wait if specified
|
|
6277
|
+
if (options.waitForSelector) {
|
|
6278
|
+
await page.waitForSelector(options.waitForSelector, { timeout: 10000 }).catch(() => { });
|
|
6279
|
+
}
|
|
6280
|
+
if (options.waitBeforeCapture) {
|
|
6281
|
+
await new Promise(resolve => setTimeout(resolve, options.waitBeforeCapture));
|
|
6282
|
+
}
|
|
6283
|
+
// Take screenshot
|
|
6284
|
+
const screenshotsPath = getResponsiveScreenshotsPath();
|
|
6285
|
+
const filename = `${viewport.name}-${Date.now()}.png`;
|
|
6286
|
+
const screenshotPath = (0, path_1.join)(screenshotsPath, filename);
|
|
6287
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
6288
|
+
return {
|
|
6289
|
+
viewport,
|
|
6290
|
+
screenshotPath,
|
|
6291
|
+
timestamp: new Date().toISOString(),
|
|
6292
|
+
captureTime: Date.now() - startTime,
|
|
6293
|
+
};
|
|
6294
|
+
}
|
|
6295
|
+
finally {
|
|
6296
|
+
await browser.close();
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
6299
|
+
/**
|
|
6300
|
+
* Analyze responsive issues from comparisons
|
|
6301
|
+
*/
|
|
6302
|
+
function analyzeResponsiveIssues(comparisons, screenshots) {
|
|
6303
|
+
const issues = [];
|
|
6304
|
+
for (const comparison of comparisons) {
|
|
6305
|
+
if (comparison.analysis.overallStatus !== "pass") {
|
|
6306
|
+
const changes = comparison.analysis.changes || [];
|
|
6307
|
+
for (const change of changes) {
|
|
6308
|
+
let issueType = "other";
|
|
6309
|
+
const desc = change.description.toLowerCase();
|
|
6310
|
+
if (desc.includes("overflow") || desc.includes("scroll")) {
|
|
6311
|
+
issueType = "overflow";
|
|
6312
|
+
}
|
|
6313
|
+
else if (desc.includes("truncat") || desc.includes("cut off")) {
|
|
6314
|
+
issueType = "truncation";
|
|
6315
|
+
}
|
|
6316
|
+
else if (desc.includes("overlap")) {
|
|
6317
|
+
issueType = "overlap";
|
|
6318
|
+
}
|
|
6319
|
+
else if (desc.includes("hidden") || desc.includes("disappear")) {
|
|
6320
|
+
issueType = "hidden_content";
|
|
6321
|
+
}
|
|
6322
|
+
else if (desc.includes("text") && (desc.includes("small") || desc.includes("read"))) {
|
|
6323
|
+
issueType = "unreadable_text";
|
|
6324
|
+
}
|
|
6325
|
+
else if (desc.includes("layout") || desc.includes("break") || desc.includes("shift")) {
|
|
6326
|
+
issueType = "layout_break";
|
|
6327
|
+
}
|
|
6328
|
+
// Map VisualChange severity to ResponsiveIssue severity
|
|
6329
|
+
const severityMap = {
|
|
6330
|
+
breaking: "critical",
|
|
6331
|
+
warning: "major",
|
|
6332
|
+
info: "minor",
|
|
6333
|
+
acceptable: "minor",
|
|
6334
|
+
};
|
|
6335
|
+
issues.push({
|
|
6336
|
+
type: issueType,
|
|
6337
|
+
severity: severityMap[change.severity] || "minor",
|
|
6338
|
+
description: change.description,
|
|
6339
|
+
affectedViewports: [comparison.viewportA.name, comparison.viewportB.name],
|
|
6340
|
+
breakpointRange: {
|
|
6341
|
+
min: Math.min(comparison.viewportA.width, comparison.viewportB.width),
|
|
6342
|
+
max: Math.max(comparison.viewportA.width, comparison.viewportB.width),
|
|
6343
|
+
},
|
|
6344
|
+
});
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
return issues;
|
|
6349
|
+
}
|
|
6350
|
+
/**
|
|
6351
|
+
* Run responsive visual test for a single URL
|
|
6352
|
+
*/
|
|
6353
|
+
async function runResponsiveTest(url, options = {}) {
|
|
6354
|
+
const startTime = Date.now();
|
|
6355
|
+
const viewports = resolveViewports(options.viewports);
|
|
6356
|
+
console.log(`\n📱 Responsive Visual Test`);
|
|
6357
|
+
console.log(` URL: ${url}`);
|
|
6358
|
+
console.log(` Viewports: ${viewports.map(v => v.name).join(", ")}\n`);
|
|
6359
|
+
// Capture screenshots at each viewport
|
|
6360
|
+
const screenshots = [];
|
|
6361
|
+
for (const viewport of viewports) {
|
|
6362
|
+
console.log(` 📸 Capturing ${viewport.name} (${viewport.width}x${viewport.height})...`);
|
|
6363
|
+
try {
|
|
6364
|
+
const screenshot = await captureAtViewport(url, viewport, options);
|
|
6365
|
+
screenshots.push(screenshot);
|
|
6366
|
+
console.log(` ✅ Captured in ${screenshot.captureTime}ms`);
|
|
6367
|
+
}
|
|
6368
|
+
catch (error) {
|
|
6369
|
+
console.log(` ❌ Failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
if (screenshots.length < 2) {
|
|
6373
|
+
return {
|
|
6374
|
+
url,
|
|
6375
|
+
screenshots,
|
|
6376
|
+
comparisons: [],
|
|
6377
|
+
issues: [],
|
|
6378
|
+
overallStatus: "major_issues",
|
|
6379
|
+
summary: "Could not capture enough screenshots for comparison",
|
|
6380
|
+
problematicViewports: [],
|
|
6381
|
+
duration: Date.now() - startTime,
|
|
6382
|
+
timestamp: new Date().toISOString(),
|
|
6383
|
+
};
|
|
6384
|
+
}
|
|
6385
|
+
// Compare adjacent viewport sizes (small to large)
|
|
6386
|
+
const sortedScreenshots = [...screenshots].sort((a, b) => a.viewport.width - b.viewport.width);
|
|
6387
|
+
const comparisons = [];
|
|
6388
|
+
let hasMinorIssues = false;
|
|
6389
|
+
let hasMajorIssues = false;
|
|
6390
|
+
const problematicViewports = new Set();
|
|
6391
|
+
console.log(`\n 🔍 Comparing viewports...`);
|
|
6392
|
+
for (let i = 0; i < sortedScreenshots.length - 1; i++) {
|
|
6393
|
+
const a = sortedScreenshots[i];
|
|
6394
|
+
const b = sortedScreenshots[i + 1];
|
|
6395
|
+
console.log(` ${a.viewport.name} → ${b.viewport.name}...`);
|
|
6396
|
+
const analysis = await analyzeVisualDifferences(a.screenshotPath, b.screenshotPath, { sensitivity: options.sensitivity || "medium" });
|
|
6397
|
+
comparisons.push({
|
|
6398
|
+
viewportA: a.viewport,
|
|
6399
|
+
viewportB: b.viewport,
|
|
6400
|
+
analysis,
|
|
6401
|
+
screenshots: {
|
|
6402
|
+
a: a.screenshotPath,
|
|
6403
|
+
b: b.screenshotPath,
|
|
6404
|
+
},
|
|
6405
|
+
});
|
|
6406
|
+
if (analysis.overallStatus === "fail") {
|
|
6407
|
+
hasMajorIssues = true;
|
|
6408
|
+
problematicViewports.add(a.viewport.name);
|
|
6409
|
+
problematicViewports.add(b.viewport.name);
|
|
6410
|
+
console.log(` ❌ Major issues (${(analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
6411
|
+
}
|
|
6412
|
+
else if (analysis.overallStatus === "warning") {
|
|
6413
|
+
hasMinorIssues = true;
|
|
6414
|
+
console.log(` ⚠️ Minor issues (${(analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
6415
|
+
}
|
|
6416
|
+
else {
|
|
6417
|
+
console.log(` ✅ Responsive (${(analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
6418
|
+
}
|
|
6419
|
+
}
|
|
6420
|
+
// Analyze issues
|
|
6421
|
+
const issues = analyzeResponsiveIssues(comparisons, screenshots);
|
|
6422
|
+
const overallStatus = hasMajorIssues
|
|
6423
|
+
? "major_issues"
|
|
6424
|
+
: hasMinorIssues
|
|
6425
|
+
? "minor_issues"
|
|
6426
|
+
: "responsive";
|
|
6427
|
+
const summary = overallStatus === "responsive"
|
|
6428
|
+
? "Page is fully responsive across all tested viewports"
|
|
6429
|
+
: overallStatus === "minor_issues"
|
|
6430
|
+
? "Minor responsive issues detected"
|
|
6431
|
+
: "Significant responsive issues detected";
|
|
6432
|
+
return {
|
|
6433
|
+
url,
|
|
6434
|
+
screenshots,
|
|
6435
|
+
comparisons,
|
|
6436
|
+
issues,
|
|
6437
|
+
overallStatus,
|
|
6438
|
+
summary,
|
|
6439
|
+
problematicViewports: Array.from(problematicViewports),
|
|
6440
|
+
duration: Date.now() - startTime,
|
|
6441
|
+
timestamp: new Date().toISOString(),
|
|
6442
|
+
};
|
|
6443
|
+
}
|
|
6444
|
+
/**
|
|
6445
|
+
* Run responsive test suite for multiple URLs
|
|
6446
|
+
*/
|
|
6447
|
+
async function runResponsiveSuite(suite) {
|
|
6448
|
+
const startTime = Date.now();
|
|
6449
|
+
const results = [];
|
|
6450
|
+
console.log(`\n📱 Responsive Test Suite: ${suite.name}`);
|
|
6451
|
+
console.log(` Testing ${suite.urls.length} URLs\n`);
|
|
6452
|
+
for (const url of suite.urls) {
|
|
6453
|
+
const result = await runResponsiveTest(url, suite.options);
|
|
6454
|
+
results.push(result);
|
|
6455
|
+
}
|
|
6456
|
+
// Aggregate common issues
|
|
6457
|
+
const issueMap = new Map();
|
|
6458
|
+
for (const result of results) {
|
|
6459
|
+
for (const issue of result.issues) {
|
|
6460
|
+
const key = `${issue.type}-${issue.description}`;
|
|
6461
|
+
if (issueMap.has(key)) {
|
|
6462
|
+
const existing = issueMap.get(key);
|
|
6463
|
+
existing.affectedViewports = [...new Set([...existing.affectedViewports, ...issue.affectedViewports])];
|
|
6464
|
+
}
|
|
6465
|
+
else {
|
|
6466
|
+
issueMap.set(key, { ...issue });
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
return {
|
|
6471
|
+
suite,
|
|
6472
|
+
results,
|
|
6473
|
+
summary: {
|
|
6474
|
+
total: results.length,
|
|
6475
|
+
responsive: results.filter(r => r.overallStatus === "responsive").length,
|
|
6476
|
+
minorIssues: results.filter(r => r.overallStatus === "minor_issues").length,
|
|
6477
|
+
majorIssues: results.filter(r => r.overallStatus === "major_issues").length,
|
|
6478
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
6479
|
+
},
|
|
6480
|
+
commonIssues: Array.from(issueMap.values()),
|
|
6481
|
+
duration: Date.now() - startTime,
|
|
6482
|
+
timestamp: new Date().toISOString(),
|
|
6483
|
+
};
|
|
6484
|
+
}
|
|
6485
|
+
/**
|
|
6486
|
+
* Format responsive test result as console report
|
|
6487
|
+
*/
|
|
6488
|
+
function formatResponsiveReport(result) {
|
|
6489
|
+
const lines = [];
|
|
6490
|
+
const duration = (result.duration / 1000).toFixed(2);
|
|
6491
|
+
lines.push(`╔${"═".repeat(78)}╗`);
|
|
6492
|
+
lines.push(`║${" ".repeat(20)}RESPONSIVE VISUAL TEST REPORT${" ".repeat(29)}║`);
|
|
6493
|
+
lines.push(`╚${"═".repeat(78)}╝`);
|
|
6494
|
+
lines.push("");
|
|
6495
|
+
const statusIcon = result.overallStatus === "responsive" ? "✅" : result.overallStatus === "minor_issues" ? "⚠️" : "❌";
|
|
6496
|
+
const statusText = result.overallStatus.toUpperCase().replace("_", " ");
|
|
6497
|
+
lines.push(`${statusIcon} Status: ${statusText}`);
|
|
6498
|
+
lines.push(`🔗 URL: ${result.url}`);
|
|
6499
|
+
lines.push(`⏱️ Duration: ${duration}s`);
|
|
6500
|
+
lines.push("");
|
|
6501
|
+
lines.push("─".repeat(79));
|
|
6502
|
+
lines.push("📸 VIEWPORT SCREENSHOTS");
|
|
6503
|
+
lines.push("─".repeat(79));
|
|
6504
|
+
for (const screenshot of result.screenshots) {
|
|
6505
|
+
const v = screenshot.viewport;
|
|
6506
|
+
lines.push(` ${v.name.toUpperCase()} (${v.deviceType})`);
|
|
6507
|
+
lines.push(` Dimensions: ${v.width}x${v.height}`);
|
|
6508
|
+
if (v.deviceName)
|
|
6509
|
+
lines.push(` Device: ${v.deviceName}`);
|
|
6510
|
+
lines.push(` Capture time: ${screenshot.captureTime}ms`);
|
|
6511
|
+
lines.push(` Path: ${screenshot.screenshotPath}`);
|
|
6512
|
+
lines.push("");
|
|
6513
|
+
}
|
|
6514
|
+
lines.push("─".repeat(79));
|
|
6515
|
+
lines.push("🔍 VIEWPORT COMPARISONS");
|
|
6516
|
+
lines.push("─".repeat(79));
|
|
6517
|
+
for (const comparison of result.comparisons) {
|
|
6518
|
+
const icon = comparison.analysis.overallStatus === "pass" ? "✅" : comparison.analysis.overallStatus === "warning" ? "⚠️" : "❌";
|
|
6519
|
+
lines.push(` ${comparison.viewportA.name} → ${comparison.viewportB.name}: ${icon}`);
|
|
6520
|
+
lines.push(` Similarity: ${(comparison.analysis.similarityScore * 100).toFixed(1)}%`);
|
|
6521
|
+
lines.push(` ${comparison.analysis.summary}`);
|
|
6522
|
+
lines.push("");
|
|
6523
|
+
}
|
|
6524
|
+
if (result.issues.length > 0) {
|
|
6525
|
+
lines.push("─".repeat(79));
|
|
6526
|
+
lines.push("⚠️ RESPONSIVE ISSUES DETECTED");
|
|
6527
|
+
lines.push("─".repeat(79));
|
|
6528
|
+
for (const issue of result.issues) {
|
|
6529
|
+
const severityIcon = issue.severity === "critical" ? "🔴" : issue.severity === "major" ? "🟠" : "🟡";
|
|
6530
|
+
lines.push(` ${severityIcon} [${issue.type.toUpperCase()}] ${issue.description}`);
|
|
6531
|
+
lines.push(` Affected: ${issue.affectedViewports.join(", ")}`);
|
|
6532
|
+
if (issue.breakpointRange) {
|
|
6533
|
+
lines.push(` Breakpoint range: ${issue.breakpointRange.min}px - ${issue.breakpointRange.max}px`);
|
|
6534
|
+
}
|
|
6535
|
+
lines.push("");
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
lines.push("─".repeat(79));
|
|
6539
|
+
lines.push(`📝 SUMMARY: ${result.summary}`);
|
|
6540
|
+
lines.push("─".repeat(79));
|
|
6541
|
+
return lines.join("\n");
|
|
6542
|
+
}
|
|
6543
|
+
/**
|
|
6544
|
+
* Generate HTML report for responsive test suite
|
|
6545
|
+
*/
|
|
6546
|
+
function generateResponsiveHtmlReport(suiteResult) {
|
|
6547
|
+
const { suite, results, summary, duration } = suiteResult;
|
|
6548
|
+
return `<!DOCTYPE html>
|
|
6549
|
+
<html>
|
|
6550
|
+
<head>
|
|
6551
|
+
<title>Responsive Test Report - ${suite.name}</title>
|
|
6552
|
+
<style>
|
|
6553
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
6554
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #e2e8f0; line-height: 1.6; }
|
|
6555
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
6556
|
+
header { text-align: center; margin-bottom: 2rem; }
|
|
6557
|
+
h1 { font-size: 2rem; margin-bottom: 0.5rem; background: linear-gradient(135deg, #8b5cf6, #06b6d4); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
6558
|
+
.subtitle { color: #94a3b8; }
|
|
6559
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
6560
|
+
.summary-card { background: #1e293b; border-radius: 0.5rem; padding: 1.5rem; text-align: center; }
|
|
6561
|
+
.summary-value { font-size: 2rem; font-weight: bold; }
|
|
6562
|
+
.summary-label { color: #94a3b8; font-size: 0.875rem; }
|
|
6563
|
+
.responsive { color: #22c55e; }
|
|
6564
|
+
.minor { color: #f59e0b; }
|
|
6565
|
+
.major { color: #ef4444; }
|
|
6566
|
+
.result-card { background: #1e293b; border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 1rem; }
|
|
6567
|
+
.result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
|
6568
|
+
.badge { padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
|
|
6569
|
+
.badge-responsive { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
|
|
6570
|
+
.badge-minor { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
|
6571
|
+
.badge-major { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
|
6572
|
+
.viewport-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem; margin-top: 1rem; }
|
|
6573
|
+
.viewport-item { background: #0f172a; padding: 0.75rem; border-radius: 0.375rem; font-size: 0.875rem; }
|
|
6574
|
+
.viewport-name { font-weight: 600; color: #8b5cf6; }
|
|
6575
|
+
.issue { background: #0f172a; padding: 0.75rem; border-radius: 0.375rem; margin-top: 0.5rem; border-left: 3px solid; }
|
|
6576
|
+
.issue-critical { border-color: #ef4444; }
|
|
6577
|
+
.issue-major { border-color: #f59e0b; }
|
|
6578
|
+
.issue-minor { border-color: #22c55e; }
|
|
6579
|
+
footer { text-align: center; color: #64748b; padding: 2rem 0; font-size: 0.875rem; }
|
|
6580
|
+
</style>
|
|
6581
|
+
</head>
|
|
6582
|
+
<body>
|
|
6583
|
+
<div class="container">
|
|
6584
|
+
<header>
|
|
6585
|
+
<h1>📱 Responsive Test Report</h1>
|
|
6586
|
+
<p class="subtitle">${suite.name}</p>
|
|
6587
|
+
</header>
|
|
6588
|
+
|
|
6589
|
+
<div class="summary-grid">
|
|
6590
|
+
<div class="summary-card">
|
|
6591
|
+
<div class="summary-value">${summary.total}</div>
|
|
6592
|
+
<div class="summary-label">Total URLs</div>
|
|
6593
|
+
</div>
|
|
6594
|
+
<div class="summary-card">
|
|
6595
|
+
<div class="summary-value responsive">${summary.responsive}</div>
|
|
6596
|
+
<div class="summary-label">Fully Responsive</div>
|
|
6597
|
+
</div>
|
|
6598
|
+
<div class="summary-card">
|
|
6599
|
+
<div class="summary-value minor">${summary.minorIssues}</div>
|
|
6600
|
+
<div class="summary-label">Minor Issues</div>
|
|
6601
|
+
</div>
|
|
6602
|
+
<div class="summary-card">
|
|
6603
|
+
<div class="summary-value major">${summary.majorIssues}</div>
|
|
6604
|
+
<div class="summary-label">Major Issues</div>
|
|
6605
|
+
</div>
|
|
6606
|
+
<div class="summary-card">
|
|
6607
|
+
<div class="summary-value">${summary.totalIssues}</div>
|
|
6608
|
+
<div class="summary-label">Total Issues</div>
|
|
6609
|
+
</div>
|
|
6610
|
+
</div>
|
|
6611
|
+
|
|
6612
|
+
<div class="results">
|
|
6613
|
+
${results.map(result => {
|
|
6614
|
+
const badgeClass = result.overallStatus === "responsive" ? "badge-responsive" : result.overallStatus === "minor_issues" ? "badge-minor" : "badge-major";
|
|
6615
|
+
return `
|
|
6616
|
+
<div class="result-card">
|
|
6617
|
+
<div class="result-header">
|
|
6618
|
+
<div>
|
|
6619
|
+
<strong>${result.url}</strong>
|
|
6620
|
+
<p style="color: #94a3b8; font-size: 0.875rem;">${result.summary}</p>
|
|
6621
|
+
</div>
|
|
6622
|
+
<span class="badge ${badgeClass}">${result.overallStatus.replace("_", " ").toUpperCase()}</span>
|
|
6623
|
+
</div>
|
|
6624
|
+
<div class="viewport-grid">
|
|
6625
|
+
${result.screenshots.map(s => `
|
|
6626
|
+
<div class="viewport-item">
|
|
6627
|
+
<span class="viewport-name">${s.viewport.name}</span>
|
|
6628
|
+
<span style="color: #94a3b8;"> ${s.viewport.width}×${s.viewport.height}</span>
|
|
6629
|
+
</div>
|
|
6630
|
+
`).join("")}
|
|
6631
|
+
</div>
|
|
6632
|
+
${result.issues.length > 0 ? `
|
|
6633
|
+
<div style="margin-top: 1rem;">
|
|
6634
|
+
<strong style="color: #f59e0b;">Issues:</strong>
|
|
6635
|
+
${result.issues.map(issue => `
|
|
6636
|
+
<div class="issue issue-${issue.severity}">
|
|
6637
|
+
<strong>[${issue.type.toUpperCase()}]</strong> ${issue.description}
|
|
6638
|
+
<br><span style="color: #94a3b8; font-size: 0.75rem;">Affected: ${issue.affectedViewports.join(", ")}</span>
|
|
6639
|
+
</div>
|
|
6640
|
+
`).join("")}
|
|
6641
|
+
</div>
|
|
6642
|
+
` : ""}
|
|
6643
|
+
</div>
|
|
6644
|
+
`;
|
|
6645
|
+
}).join("")}
|
|
6646
|
+
</div>
|
|
6647
|
+
|
|
6648
|
+
<footer>
|
|
6649
|
+
Generated by CBrowser v7.2.0 | Test completed in ${(duration / 1000).toFixed(1)}s
|
|
6650
|
+
</footer>
|
|
6651
|
+
</div>
|
|
6652
|
+
</body>
|
|
6653
|
+
</html>`;
|
|
6654
|
+
}
|
|
6655
|
+
/**
|
|
6656
|
+
* List available viewport presets
|
|
6657
|
+
*/
|
|
6658
|
+
function listViewportPresets() {
|
|
6659
|
+
return types_js_1.VIEWPORT_PRESETS;
|
|
6660
|
+
}
|
|
6661
|
+
// ============================================================================
|
|
6662
|
+
// A/B Visual Comparison (v7.3.0)
|
|
6663
|
+
// ============================================================================
|
|
6664
|
+
/**
|
|
6665
|
+
* Get the path for A/B comparison screenshots
|
|
6666
|
+
*/
|
|
6667
|
+
function getABScreenshotsPath() {
|
|
6668
|
+
const basePath = process.cwd();
|
|
6669
|
+
const screenshotsPath = (0, path_1.join)(basePath, ".cbrowser", "ab-comparison");
|
|
6670
|
+
if (!(0, fs_1.existsSync)(screenshotsPath)) {
|
|
6671
|
+
(0, fs_1.mkdirSync)(screenshotsPath, { recursive: true });
|
|
6672
|
+
}
|
|
6673
|
+
return screenshotsPath;
|
|
6674
|
+
}
|
|
6675
|
+
/**
|
|
6676
|
+
* Capture screenshot for A/B comparison
|
|
6677
|
+
*/
|
|
6678
|
+
async function captureForAB(url, label, options = {}) {
|
|
6679
|
+
const startTime = Date.now();
|
|
6680
|
+
const browser = new CBrowser({
|
|
6681
|
+
viewportWidth: options.viewport?.width || 1920,
|
|
6682
|
+
viewportHeight: options.viewport?.height || 1080,
|
|
6683
|
+
});
|
|
6684
|
+
try {
|
|
6685
|
+
await browser.launch();
|
|
6686
|
+
await browser.navigate(url);
|
|
6687
|
+
const page = await browser.getPage();
|
|
6688
|
+
// Wait if specified
|
|
6689
|
+
if (options.waitForSelector) {
|
|
6690
|
+
await page.waitForSelector(options.waitForSelector, { timeout: 10000 }).catch(() => { });
|
|
6691
|
+
}
|
|
6692
|
+
if (options.waitBeforeCapture) {
|
|
6693
|
+
await new Promise(resolve => setTimeout(resolve, options.waitBeforeCapture));
|
|
6694
|
+
}
|
|
6695
|
+
// Get page title
|
|
6696
|
+
const title = await page.title();
|
|
6697
|
+
// Take screenshot
|
|
6698
|
+
const screenshotsPath = getABScreenshotsPath();
|
|
6699
|
+
const filename = `${label.toLowerCase()}-${Date.now()}.png`;
|
|
6700
|
+
const screenshotPath = (0, path_1.join)(screenshotsPath, filename);
|
|
6701
|
+
const viewport = page.viewportSize() || { width: 1920, height: 1080 };
|
|
6702
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
6703
|
+
return {
|
|
6704
|
+
label,
|
|
6705
|
+
url,
|
|
6706
|
+
screenshotPath,
|
|
6707
|
+
title,
|
|
6708
|
+
viewport,
|
|
6709
|
+
timestamp: new Date().toISOString(),
|
|
6710
|
+
captureTime: Date.now() - startTime,
|
|
6711
|
+
};
|
|
6712
|
+
}
|
|
6713
|
+
finally {
|
|
6714
|
+
await browser.close();
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
/**
|
|
6718
|
+
* Analyze differences between A and B for detailed reporting
|
|
6719
|
+
*/
|
|
6720
|
+
function analyzeABDifferences(analysis) {
|
|
6721
|
+
const differences = [];
|
|
6722
|
+
for (const change of analysis.changes || []) {
|
|
6723
|
+
// Map VisualChange to ABDifference
|
|
6724
|
+
const severityMap = {
|
|
6725
|
+
breaking: "critical",
|
|
6726
|
+
warning: "major",
|
|
6727
|
+
info: "minor",
|
|
6728
|
+
acceptable: "info",
|
|
6729
|
+
};
|
|
6730
|
+
const typeMap = {
|
|
6731
|
+
layout: "layout",
|
|
6732
|
+
content: "content",
|
|
6733
|
+
style: "style",
|
|
6734
|
+
missing: "missing",
|
|
6735
|
+
added: "added",
|
|
6736
|
+
moved: "structure",
|
|
6737
|
+
};
|
|
6738
|
+
differences.push({
|
|
6739
|
+
type: typeMap[change.type] || "content",
|
|
6740
|
+
severity: severityMap[change.severity] || "minor",
|
|
6741
|
+
description: change.description,
|
|
6742
|
+
affectedSide: "both", // AI analysis doesn't specify which side
|
|
6743
|
+
region: change.region,
|
|
6744
|
+
});
|
|
6745
|
+
}
|
|
6746
|
+
return differences;
|
|
6747
|
+
}
|
|
6748
|
+
/**
|
|
6749
|
+
* Run A/B visual comparison between two URLs
|
|
6750
|
+
*/
|
|
6751
|
+
async function runABComparison(urlA, urlB, options = {}) {
|
|
6752
|
+
const startTime = Date.now();
|
|
6753
|
+
const labels = options.labels || { a: "Version A", b: "Version B" };
|
|
6754
|
+
console.log(`\n🔀 A/B Visual Comparison`);
|
|
6755
|
+
console.log(` A: ${urlA}`);
|
|
6756
|
+
console.log(` B: ${urlB}\n`);
|
|
6757
|
+
// Capture both screenshots
|
|
6758
|
+
console.log(` 📸 Capturing ${labels.a}...`);
|
|
6759
|
+
const screenshotA = await captureForAB(urlA, "A", options);
|
|
6760
|
+
console.log(` ✅ Captured in ${screenshotA.captureTime}ms`);
|
|
6761
|
+
console.log(` 📸 Capturing ${labels.b}...`);
|
|
6762
|
+
const screenshotB = await captureForAB(urlB, "B", options);
|
|
6763
|
+
console.log(` ✅ Captured in ${screenshotB.captureTime}ms`);
|
|
6764
|
+
// Compare using AI analysis
|
|
6765
|
+
console.log(`\n 🔍 Comparing...`);
|
|
6766
|
+
const analysis = await analyzeVisualDifferences(screenshotA.screenshotPath, screenshotB.screenshotPath, { sensitivity: options.sensitivity || "medium" });
|
|
6767
|
+
// Determine overall status based on similarity
|
|
6768
|
+
let overallStatus;
|
|
6769
|
+
if (analysis.similarityScore >= 0.95) {
|
|
6770
|
+
overallStatus = "identical";
|
|
6771
|
+
console.log(` ✅ Identical (${(analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
6772
|
+
}
|
|
6773
|
+
else if (analysis.similarityScore >= 0.80) {
|
|
6774
|
+
overallStatus = "similar";
|
|
6775
|
+
console.log(` ⚠️ Similar (${(analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
6776
|
+
}
|
|
6777
|
+
else if (analysis.similarityScore >= 0.50) {
|
|
6778
|
+
overallStatus = "different";
|
|
6779
|
+
console.log(` 🟠 Different (${(analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
6780
|
+
}
|
|
6781
|
+
else {
|
|
6782
|
+
overallStatus = "very_different";
|
|
6783
|
+
console.log(` ❌ Very Different (${(analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
6784
|
+
}
|
|
6785
|
+
// Analyze differences
|
|
6786
|
+
const differences = analyzeABDifferences(analysis);
|
|
6787
|
+
// Generate summary
|
|
6788
|
+
const summaryMap = {
|
|
6789
|
+
identical: "Pages are visually identical",
|
|
6790
|
+
similar: "Pages are similar with minor differences",
|
|
6791
|
+
different: "Pages have significant visual differences",
|
|
6792
|
+
very_different: "Pages are very different - likely different designs",
|
|
6793
|
+
};
|
|
6794
|
+
return {
|
|
6795
|
+
urlA,
|
|
6796
|
+
urlB,
|
|
6797
|
+
labels,
|
|
6798
|
+
screenshots: {
|
|
6799
|
+
a: screenshotA,
|
|
6800
|
+
b: screenshotB,
|
|
6801
|
+
},
|
|
6802
|
+
analysis,
|
|
6803
|
+
differences,
|
|
6804
|
+
overallStatus,
|
|
6805
|
+
summary: summaryMap[overallStatus],
|
|
6806
|
+
duration: Date.now() - startTime,
|
|
6807
|
+
timestamp: new Date().toISOString(),
|
|
6808
|
+
};
|
|
6809
|
+
}
|
|
6810
|
+
/**
|
|
6811
|
+
* Run A/B comparison suite for multiple page pairs
|
|
6812
|
+
*/
|
|
6813
|
+
async function runABSuite(suite) {
|
|
6814
|
+
const startTime = Date.now();
|
|
6815
|
+
const results = [];
|
|
6816
|
+
console.log(`\n🔀 A/B Comparison Suite: ${suite.name}`);
|
|
6817
|
+
console.log(` Testing ${suite.pairs.length} page pairs\n`);
|
|
6818
|
+
for (const pair of suite.pairs) {
|
|
6819
|
+
const pairOptions = {
|
|
6820
|
+
...suite.options,
|
|
6821
|
+
labels: pair.name ? { a: `${pair.name} (A)`, b: `${pair.name} (B)` } : suite.options?.labels,
|
|
6822
|
+
};
|
|
6823
|
+
const result = await runABComparison(pair.urlA, pair.urlB, pairOptions);
|
|
6824
|
+
results.push(result);
|
|
6825
|
+
}
|
|
6826
|
+
return {
|
|
6827
|
+
suite,
|
|
6828
|
+
results,
|
|
6829
|
+
summary: {
|
|
6830
|
+
total: results.length,
|
|
6831
|
+
identical: results.filter(r => r.overallStatus === "identical").length,
|
|
6832
|
+
similar: results.filter(r => r.overallStatus === "similar").length,
|
|
6833
|
+
different: results.filter(r => r.overallStatus === "different").length,
|
|
6834
|
+
veryDifferent: results.filter(r => r.overallStatus === "very_different").length,
|
|
6835
|
+
},
|
|
6836
|
+
duration: Date.now() - startTime,
|
|
6837
|
+
timestamp: new Date().toISOString(),
|
|
6838
|
+
};
|
|
6839
|
+
}
|
|
6840
|
+
/**
|
|
6841
|
+
* Format A/B comparison result as console report
|
|
6842
|
+
*/
|
|
6843
|
+
function formatABReport(result) {
|
|
6844
|
+
const lines = [];
|
|
6845
|
+
const duration = (result.duration / 1000).toFixed(2);
|
|
6846
|
+
lines.push(`╔${"═".repeat(78)}╗`);
|
|
6847
|
+
lines.push(`║${" ".repeat(22)}A/B VISUAL COMPARISON REPORT${" ".repeat(28)}║`);
|
|
6848
|
+
lines.push(`╚${"═".repeat(78)}╝`);
|
|
6849
|
+
lines.push("");
|
|
6850
|
+
const statusIcons = {
|
|
6851
|
+
identical: "✅",
|
|
6852
|
+
similar: "⚠️",
|
|
6853
|
+
different: "🟠",
|
|
6854
|
+
very_different: "❌",
|
|
6855
|
+
};
|
|
6856
|
+
const statusIcon = statusIcons[result.overallStatus];
|
|
6857
|
+
const statusText = result.overallStatus.toUpperCase().replace("_", " ");
|
|
6858
|
+
lines.push(`${statusIcon} Status: ${statusText}`);
|
|
6859
|
+
lines.push(`📊 Similarity: ${(result.analysis.similarityScore * 100).toFixed(1)}%`);
|
|
6860
|
+
lines.push(`⏱️ Duration: ${duration}s`);
|
|
6861
|
+
lines.push("");
|
|
6862
|
+
lines.push("─".repeat(79));
|
|
6863
|
+
lines.push("📸 SCREENSHOTS");
|
|
6864
|
+
lines.push("─".repeat(79));
|
|
6865
|
+
lines.push(` ${result.labels.a.toUpperCase()} (A)`);
|
|
6866
|
+
lines.push(` URL: ${result.screenshots.a.url}`);
|
|
6867
|
+
lines.push(` Title: ${result.screenshots.a.title}`);
|
|
6868
|
+
lines.push(` Capture time: ${result.screenshots.a.captureTime}ms`);
|
|
6869
|
+
lines.push(` Path: ${result.screenshots.a.screenshotPath}`);
|
|
6870
|
+
lines.push("");
|
|
6871
|
+
lines.push(` ${result.labels.b.toUpperCase()} (B)`);
|
|
6872
|
+
lines.push(` URL: ${result.screenshots.b.url}`);
|
|
6873
|
+
lines.push(` Title: ${result.screenshots.b.title}`);
|
|
6874
|
+
lines.push(` Capture time: ${result.screenshots.b.captureTime}ms`);
|
|
6875
|
+
lines.push(` Path: ${result.screenshots.b.screenshotPath}`);
|
|
6876
|
+
lines.push("");
|
|
6877
|
+
if (result.differences.length > 0) {
|
|
6878
|
+
lines.push("─".repeat(79));
|
|
6879
|
+
lines.push("🔍 DIFFERENCES DETECTED");
|
|
6880
|
+
lines.push("─".repeat(79));
|
|
6881
|
+
for (const diff of result.differences) {
|
|
6882
|
+
const severityIcons = { critical: "🔴", major: "🟠", minor: "🟡", info: "🔵" };
|
|
6883
|
+
const icon = severityIcons[diff.severity];
|
|
6884
|
+
lines.push(` ${icon} [${diff.type.toUpperCase()}] ${diff.description}`);
|
|
6885
|
+
}
|
|
6886
|
+
lines.push("");
|
|
6887
|
+
}
|
|
6888
|
+
lines.push("─".repeat(79));
|
|
6889
|
+
lines.push(`📝 SUMMARY: ${result.summary}`);
|
|
6890
|
+
lines.push("─".repeat(79));
|
|
6891
|
+
return lines.join("\n");
|
|
6892
|
+
}
|
|
6893
|
+
/**
|
|
6894
|
+
* Generate HTML report for A/B comparison suite
|
|
6895
|
+
*/
|
|
6896
|
+
function generateABHtmlReport(suiteResult) {
|
|
6897
|
+
const { suite, results, summary, duration } = suiteResult;
|
|
6898
|
+
return `<!DOCTYPE html>
|
|
6899
|
+
<html>
|
|
6900
|
+
<head>
|
|
6901
|
+
<title>A/B Comparison Report - ${suite.name}</title>
|
|
6902
|
+
<style>
|
|
6903
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
6904
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #e2e8f0; line-height: 1.6; }
|
|
6905
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
|
6906
|
+
header { text-align: center; margin-bottom: 2rem; }
|
|
6907
|
+
h1 { font-size: 2rem; margin-bottom: 0.5rem; background: linear-gradient(135deg, #f59e0b, #ef4444); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
6908
|
+
.subtitle { color: #94a3b8; }
|
|
6909
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
6910
|
+
.summary-card { background: #1e293b; border-radius: 0.5rem; padding: 1.5rem; text-align: center; }
|
|
6911
|
+
.summary-value { font-size: 2rem; font-weight: bold; }
|
|
6912
|
+
.summary-label { color: #94a3b8; font-size: 0.875rem; }
|
|
6913
|
+
.identical { color: #22c55e; }
|
|
6914
|
+
.similar { color: #f59e0b; }
|
|
6915
|
+
.different { color: #f97316; }
|
|
6916
|
+
.very-different { color: #ef4444; }
|
|
6917
|
+
.result-card { background: #1e293b; border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 1rem; }
|
|
6918
|
+
.result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
|
6919
|
+
.badge { padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
|
|
6920
|
+
.badge-identical { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
|
|
6921
|
+
.badge-similar { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
|
6922
|
+
.badge-different { background: rgba(249, 115, 22, 0.2); color: #f97316; }
|
|
6923
|
+
.badge-very-different { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
|
6924
|
+
.comparison-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem; }
|
|
6925
|
+
.side { background: #0f172a; padding: 1rem; border-radius: 0.375rem; }
|
|
6926
|
+
.side-label { font-weight: 600; color: #f59e0b; margin-bottom: 0.5rem; }
|
|
6927
|
+
.url { color: #94a3b8; font-size: 0.875rem; word-break: break-all; }
|
|
6928
|
+
.diff-list { margin-top: 1rem; }
|
|
6929
|
+
.diff-item { background: #0f172a; padding: 0.5rem 0.75rem; border-radius: 0.375rem; margin-top: 0.5rem; border-left: 3px solid; }
|
|
6930
|
+
.diff-critical { border-color: #ef4444; }
|
|
6931
|
+
.diff-major { border-color: #f97316; }
|
|
6932
|
+
.diff-minor { border-color: #f59e0b; }
|
|
6933
|
+
.diff-info { border-color: #3b82f6; }
|
|
6934
|
+
footer { text-align: center; color: #64748b; padding: 2rem 0; font-size: 0.875rem; }
|
|
6935
|
+
</style>
|
|
6936
|
+
</head>
|
|
6937
|
+
<body>
|
|
6938
|
+
<div class="container">
|
|
6939
|
+
<header>
|
|
6940
|
+
<h1>🔀 A/B Comparison Report</h1>
|
|
6941
|
+
<p class="subtitle">${suite.name}</p>
|
|
6942
|
+
</header>
|
|
6943
|
+
|
|
6944
|
+
<div class="summary-grid">
|
|
6945
|
+
<div class="summary-card">
|
|
6946
|
+
<div class="summary-value">${summary.total}</div>
|
|
6947
|
+
<div class="summary-label">Total Pairs</div>
|
|
6948
|
+
</div>
|
|
6949
|
+
<div class="summary-card">
|
|
6950
|
+
<div class="summary-value identical">${summary.identical}</div>
|
|
6951
|
+
<div class="summary-label">Identical</div>
|
|
6952
|
+
</div>
|
|
6953
|
+
<div class="summary-card">
|
|
6954
|
+
<div class="summary-value similar">${summary.similar}</div>
|
|
6955
|
+
<div class="summary-label">Similar</div>
|
|
6956
|
+
</div>
|
|
6957
|
+
<div class="summary-card">
|
|
6958
|
+
<div class="summary-value different">${summary.different}</div>
|
|
6959
|
+
<div class="summary-label">Different</div>
|
|
6960
|
+
</div>
|
|
6961
|
+
<div class="summary-card">
|
|
6962
|
+
<div class="summary-value very-different">${summary.veryDifferent}</div>
|
|
6963
|
+
<div class="summary-label">Very Different</div>
|
|
6964
|
+
</div>
|
|
6965
|
+
</div>
|
|
6966
|
+
|
|
6967
|
+
<div class="results">
|
|
6968
|
+
${results.map(result => {
|
|
6969
|
+
const badgeClass = `badge-${result.overallStatus.replace("_", "-")}`;
|
|
6970
|
+
return `
|
|
6971
|
+
<div class="result-card">
|
|
6972
|
+
<div class="result-header">
|
|
6973
|
+
<div>
|
|
6974
|
+
<strong>${result.summary}</strong>
|
|
6975
|
+
<p style="color: #94a3b8; font-size: 0.875rem;">Similarity: ${(result.analysis.similarityScore * 100).toFixed(1)}%</p>
|
|
6976
|
+
</div>
|
|
6977
|
+
<span class="badge ${badgeClass}">${result.overallStatus.replace("_", " ").toUpperCase()}</span>
|
|
6978
|
+
</div>
|
|
6979
|
+
<div class="comparison-grid">
|
|
6980
|
+
<div class="side">
|
|
6981
|
+
<div class="side-label">${result.labels.a}</div>
|
|
6982
|
+
<div class="url">${result.urlA}</div>
|
|
6983
|
+
<div style="margin-top: 0.5rem; color: #64748b; font-size: 0.75rem;">Title: ${result.screenshots.a.title}</div>
|
|
6984
|
+
</div>
|
|
6985
|
+
<div class="side">
|
|
6986
|
+
<div class="side-label">${result.labels.b}</div>
|
|
6987
|
+
<div class="url">${result.urlB}</div>
|
|
6988
|
+
<div style="margin-top: 0.5rem; color: #64748b; font-size: 0.75rem;">Title: ${result.screenshots.b.title}</div>
|
|
6989
|
+
</div>
|
|
6990
|
+
</div>
|
|
6991
|
+
${result.differences.length > 0 ? `
|
|
6992
|
+
<div class="diff-list">
|
|
6993
|
+
<strong style="color: #f59e0b;">Differences:</strong>
|
|
6994
|
+
${result.differences.slice(0, 5).map(diff => `
|
|
6995
|
+
<div class="diff-item diff-${diff.severity}">
|
|
6996
|
+
<strong>[${diff.type.toUpperCase()}]</strong> ${diff.description}
|
|
6997
|
+
</div>
|
|
6998
|
+
`).join("")}
|
|
6999
|
+
${result.differences.length > 5 ? `<div style="color: #94a3b8; margin-top: 0.5rem;">...and ${result.differences.length - 5} more</div>` : ""}
|
|
7000
|
+
</div>
|
|
7001
|
+
` : ""}
|
|
7002
|
+
</div>
|
|
7003
|
+
`;
|
|
7004
|
+
}).join("")}
|
|
7005
|
+
</div>
|
|
7006
|
+
|
|
7007
|
+
<footer>
|
|
7008
|
+
Generated by CBrowser v7.3.0 | Test completed in ${(duration / 1000).toFixed(1)}s
|
|
6212
7009
|
</footer>
|
|
6213
7010
|
</div>
|
|
6214
7011
|
</body>
|