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.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.1.0 | Test completed in ${(duration / 1000).toFixed(1)}s
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>