cbrowser 5.3.0 → 6.1.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/cli.js CHANGED
@@ -13,8 +13,8 @@ const mcp_server_js_1 = require("./mcp-server.js");
13
13
  function showHelp() {
14
14
  console.log(`
15
15
  ╔══════════════════════════════════════════════════════════════════════════════╗
16
- ║ CBrowser CLI v5.3.0 ║
17
- ║ AI-powered browser automation with smart retry & assertions
16
+ ║ CBrowser CLI v6.1.0 ║
17
+ ║ AI-powered browser automation with natural language test suites
18
18
  ╚══════════════════════════════════════════════════════════════════════════════╝
19
19
 
20
20
  NAVIGATION
@@ -36,6 +36,46 @@ AUTONOMOUS JOURNEYS
36
36
  --goal <goal> What to accomplish
37
37
  --record-video Record journey as video
38
38
 
39
+ MULTI-PERSONA COMPARISON (v6.0.0)
40
+ compare-personas Compare multiple personas on the same journey
41
+ --start <url> Starting URL (required)
42
+ --goal <goal> What to accomplish (required)
43
+ --personas <list> Comma-separated persona names
44
+ --concurrency <n> Max parallel browsers (default: 3)
45
+ --output <file> Save JSON report to file
46
+ --html Generate HTML report
47
+ Examples:
48
+ cbrowser compare-personas --start "https://example.com" \\
49
+ --goal "Complete checkout" \\
50
+ --personas power-user,first-timer,elderly-user,mobile-user
51
+
52
+ NATURAL LANGUAGE TEST SUITES (v6.1.0)
53
+ test-suite <file.txt> Run tests written in plain English
54
+ --continue-on-failure Keep running after a test fails
55
+ --screenshot-on-failure Take screenshots on failure (default: true)
56
+ --output <file> Save JSON report to file
57
+ --html Generate HTML report
58
+ --timeout <ms> Timeout per step (default: 30000)
59
+ test-suite --inline "..." Run inline test (semicolon-separated steps)
60
+ Examples:
61
+ cbrowser test-suite login-flow.txt --html
62
+ cbrowser test-suite --inline "go to https://example.com ; click login ; verify url contains /dashboard"
63
+
64
+ Test File Format:
65
+ # Test: Login Flow
66
+ go to https://example.com
67
+ click the login button
68
+ type "user@example.com" in email field
69
+ type "password123" in password field
70
+ click submit
71
+ verify url contains "/dashboard"
72
+
73
+ # Test: Search
74
+ go to https://example.com
75
+ type "test query" in search box
76
+ click search button
77
+ verify page contains "results"
78
+
39
79
  PERSONAS
40
80
  persona list List all personas (built-in + custom)
41
81
  persona create "<desc>" Create persona from natural language description
@@ -286,6 +326,291 @@ function formatBytes(bytes) {
286
326
  return `${(bytes / 1024).toFixed(1)} KB`;
287
327
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
288
328
  }
329
+ function generateHtmlReport(comparison) {
330
+ const rows = comparison.personas.map((p) => `
331
+ <tr class="${p.success ? 'success' : 'failure'}">
332
+ <td><strong>${p.persona}</strong><br><small>${p.description}</small></td>
333
+ <td>${p.success ? '✓' : '✗'}</td>
334
+ <td>${(p.totalTime / 1000).toFixed(1)}s</td>
335
+ <td>${p.stepCount}</td>
336
+ <td>${p.frictionCount}</td>
337
+ <td>${p.techLevel}</td>
338
+ <td>${p.device}</td>
339
+ <td><small>${p.frictionPoints.join('<br>') || '-'}</small></td>
340
+ </tr>
341
+ `).join('');
342
+ return `<!DOCTYPE html>
343
+ <html lang="en">
344
+ <head>
345
+ <meta charset="UTF-8">
346
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
347
+ <title>Persona Comparison Report</title>
348
+ <style>
349
+ * { box-sizing: border-box; }
350
+ body {
351
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
352
+ max-width: 1400px;
353
+ margin: 0 auto;
354
+ padding: 2rem;
355
+ background: #f5f5f5;
356
+ }
357
+ h1 { color: #1a1a1a; border-bottom: 3px solid #3b82f6; padding-bottom: 0.5rem; }
358
+ .meta { background: white; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
359
+ .meta p { margin: 0.25rem 0; }
360
+ table {
361
+ width: 100%;
362
+ border-collapse: collapse;
363
+ background: white;
364
+ border-radius: 8px;
365
+ overflow: hidden;
366
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
367
+ }
368
+ th, td {
369
+ padding: 1rem;
370
+ text-align: left;
371
+ border-bottom: 1px solid #eee;
372
+ }
373
+ th { background: #1a1a1a; color: white; }
374
+ tr.success { background: #ecfdf5; }
375
+ tr.failure { background: #fef2f2; }
376
+ .summary {
377
+ display: grid;
378
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
379
+ gap: 1rem;
380
+ margin: 1rem 0;
381
+ }
382
+ .stat {
383
+ background: white;
384
+ padding: 1rem;
385
+ border-radius: 8px;
386
+ text-align: center;
387
+ }
388
+ .stat .value { font-size: 2rem; font-weight: bold; color: #3b82f6; }
389
+ .stat .label { color: #666; font-size: 0.875rem; }
390
+ .recommendations {
391
+ background: #fffbeb;
392
+ border: 1px solid #fbbf24;
393
+ border-radius: 8px;
394
+ padding: 1rem;
395
+ margin-top: 1rem;
396
+ }
397
+ .recommendations h3 { margin-top: 0; }
398
+ .recommendations ul { margin: 0; padding-left: 1.5rem; }
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <h1>🎭 Multi-Persona Comparison Report</h1>
403
+
404
+ <div class="meta">
405
+ <p><strong>URL:</strong> ${comparison.url}</p>
406
+ <p><strong>Goal:</strong> ${comparison.goal}</p>
407
+ <p><strong>Timestamp:</strong> ${comparison.timestamp}</p>
408
+ <p><strong>Total Duration:</strong> ${(comparison.duration / 1000).toFixed(1)}s</p>
409
+ </div>
410
+
411
+ <div class="summary">
412
+ <div class="stat">
413
+ <div class="value">${comparison.summary.successCount}/${comparison.summary.totalPersonas}</div>
414
+ <div class="label">Success Rate</div>
415
+ </div>
416
+ <div class="stat">
417
+ <div class="value">${(comparison.summary.avgCompletionTime / 1000).toFixed(1)}s</div>
418
+ <div class="label">Avg Completion Time</div>
419
+ </div>
420
+ <div class="stat">
421
+ <div class="value">${comparison.summary.fastestPersona}</div>
422
+ <div class="label">Fastest</div>
423
+ </div>
424
+ <div class="stat">
425
+ <div class="value">${comparison.summary.mostFriction}</div>
426
+ <div class="label">Most Friction</div>
427
+ </div>
428
+ </div>
429
+
430
+ <table>
431
+ <thead>
432
+ <tr>
433
+ <th>Persona</th>
434
+ <th>Success</th>
435
+ <th>Time</th>
436
+ <th>Steps</th>
437
+ <th>Friction</th>
438
+ <th>Tech Level</th>
439
+ <th>Device</th>
440
+ <th>Issues</th>
441
+ </tr>
442
+ </thead>
443
+ <tbody>
444
+ ${rows}
445
+ </tbody>
446
+ </table>
447
+
448
+ <div class="recommendations">
449
+ <h3>💡 Recommendations</h3>
450
+ <ul>
451
+ ${comparison.recommendations.map((r) => `<li>${r}</li>`).join('')}
452
+ </ul>
453
+ </div>
454
+
455
+ <p style="color: #999; text-align: center; margin-top: 2rem;">
456
+ Generated by CBrowser v6.0.0 - Multi-Persona Comparison
457
+ </p>
458
+ </body>
459
+ </html>`;
460
+ }
461
+ function generateTestSuiteHtmlReport(result) {
462
+ const testRows = result.testResults.map((t) => {
463
+ const stepDetails = t.stepResults.map((s) => `
464
+ <tr class="${s.passed ? 'step-pass' : 'step-fail'}">
465
+ <td class="step-indent">${s.instruction}</td>
466
+ <td>${s.passed ? '✓' : '✗'}</td>
467
+ <td>${s.duration}ms</td>
468
+ <td>${s.error || '-'}</td>
469
+ </tr>
470
+ `).join('');
471
+ return `
472
+ <tr class="${t.passed ? 'success' : 'failure'}">
473
+ <td><strong>${t.name}</strong></td>
474
+ <td>${t.passed ? '✓ PASS' : '✗ FAIL'}</td>
475
+ <td>${(t.duration / 1000).toFixed(1)}s</td>
476
+ <td>${t.stepResults.length} steps</td>
477
+ <td>${t.error || '-'}</td>
478
+ </tr>
479
+ ${stepDetails}
480
+ `;
481
+ }).join('');
482
+ const passRate = result.summary.passRate.toFixed(0);
483
+ const passColor = result.summary.passRate === 100 ? '#10b981' : result.summary.passRate >= 80 ? '#f59e0b' : '#ef4444';
484
+ return `<!DOCTYPE html>
485
+ <html lang="en">
486
+ <head>
487
+ <meta charset="UTF-8">
488
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
489
+ <title>Test Suite Report - ${result.name}</title>
490
+ <style>
491
+ * { box-sizing: border-box; }
492
+ body {
493
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
494
+ max-width: 1200px;
495
+ margin: 0 auto;
496
+ padding: 2rem;
497
+ background: #f5f5f5;
498
+ }
499
+ h1 { color: #1a1a1a; border-bottom: 3px solid #3b82f6; padding-bottom: 0.5rem; }
500
+ .meta { background: white; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
501
+ .meta p { margin: 0.25rem 0; }
502
+ table {
503
+ width: 100%;
504
+ border-collapse: collapse;
505
+ background: white;
506
+ border-radius: 8px;
507
+ overflow: hidden;
508
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
509
+ margin-bottom: 1rem;
510
+ }
511
+ th, td {
512
+ padding: 0.75rem 1rem;
513
+ text-align: left;
514
+ border-bottom: 1px solid #eee;
515
+ }
516
+ th { background: #1a1a1a; color: white; }
517
+ tr.success { background: #ecfdf5; }
518
+ tr.failure { background: #fef2f2; }
519
+ tr.step-pass { background: #f8fafc; }
520
+ tr.step-fail { background: #fff5f5; }
521
+ .step-indent { padding-left: 2rem; font-size: 0.875rem; color: #666; }
522
+ .summary {
523
+ display: grid;
524
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
525
+ gap: 1rem;
526
+ margin: 1rem 0;
527
+ }
528
+ .stat {
529
+ background: white;
530
+ padding: 1rem;
531
+ border-radius: 8px;
532
+ text-align: center;
533
+ }
534
+ .stat .value { font-size: 1.75rem; font-weight: bold; }
535
+ .stat .label { color: #666; font-size: 0.875rem; }
536
+ .pass-rate { color: ${passColor}; }
537
+ .failures {
538
+ background: #fef2f2;
539
+ border: 1px solid #fecaca;
540
+ border-radius: 8px;
541
+ padding: 1rem;
542
+ margin-top: 1rem;
543
+ }
544
+ .failures h3 { margin-top: 0; color: #dc2626; }
545
+ .failures ul { margin: 0; padding-left: 1.5rem; }
546
+ code { background: #f1f5f9; padding: 0.125rem 0.25rem; border-radius: 4px; font-size: 0.875rem; }
547
+ </style>
548
+ </head>
549
+ <body>
550
+ <h1>🧪 Natural Language Test Report</h1>
551
+
552
+ <div class="meta">
553
+ <p><strong>Suite:</strong> ${result.name}</p>
554
+ <p><strong>Timestamp:</strong> ${result.timestamp}</p>
555
+ <p><strong>Duration:</strong> ${(result.duration / 1000).toFixed(1)}s</p>
556
+ </div>
557
+
558
+ <div class="summary">
559
+ <div class="stat">
560
+ <div class="value pass-rate">${passRate}%</div>
561
+ <div class="label">Pass Rate</div>
562
+ </div>
563
+ <div class="stat">
564
+ <div class="value">${result.summary.passed}</div>
565
+ <div class="label">Passed</div>
566
+ </div>
567
+ <div class="stat">
568
+ <div class="value" style="color: ${result.summary.failed > 0 ? '#ef4444' : '#10b981'};">${result.summary.failed}</div>
569
+ <div class="label">Failed</div>
570
+ </div>
571
+ <div class="stat">
572
+ <div class="value">${result.summary.total}</div>
573
+ <div class="label">Total Tests</div>
574
+ </div>
575
+ </div>
576
+
577
+ <table>
578
+ <thead>
579
+ <tr>
580
+ <th>Test / Step</th>
581
+ <th>Status</th>
582
+ <th>Duration</th>
583
+ <th>Steps</th>
584
+ <th>Error</th>
585
+ </tr>
586
+ </thead>
587
+ <tbody>
588
+ ${testRows}
589
+ </tbody>
590
+ </table>
591
+
592
+ ${result.summary.failed > 0 ? `
593
+ <div class="failures">
594
+ <h3>❌ Failed Tests</h3>
595
+ <ul>
596
+ ${result.testResults.filter(t => !t.passed).map(t => `
597
+ <li>
598
+ <strong>${t.name}</strong>: ${t.error}
599
+ <ul>
600
+ ${t.stepResults.filter(s => !s.passed).map(s => `<li><code>${s.instruction}</code> - ${s.error}</li>`).join('')}
601
+ </ul>
602
+ </li>
603
+ `).join('')}
604
+ </ul>
605
+ </div>
606
+ ` : ''}
607
+
608
+ <p style="color: #999; text-align: center; margin-top: 2rem;">
609
+ Generated by CBrowser v6.1.0 - Natural Language Test Suites
610
+ </p>
611
+ </body>
612
+ </html>`;
613
+ }
289
614
  function parseGeoLocation(location) {
290
615
  // Check if it's a preset
291
616
  if (types_js_1.LOCATION_PRESETS[location]) {
@@ -681,6 +1006,58 @@ async function main() {
681
1006
  }
682
1007
  break;
683
1008
  }
1009
+ // =========================================================================
1010
+ // Tier 6: Multi-Persona Comparison (v6.0.0)
1011
+ // =========================================================================
1012
+ case "compare-personas": {
1013
+ const startUrl = options.start;
1014
+ const goal = options.goal;
1015
+ const personaList = options.personas;
1016
+ if (!startUrl) {
1017
+ console.error("Error: --start URL required");
1018
+ process.exit(1);
1019
+ }
1020
+ if (!goal) {
1021
+ console.error("Error: --goal required");
1022
+ process.exit(1);
1023
+ }
1024
+ // Default to comparing all built-in personas if none specified
1025
+ const personaNames = personaList
1026
+ ? personaList.split(",").map((p) => p.trim())
1027
+ : Object.keys(personas_js_1.BUILTIN_PERSONAS);
1028
+ const concurrency = options.concurrency
1029
+ ? parseInt(options.concurrency)
1030
+ : 3;
1031
+ const comparison = await (0, browser_js_1.comparePersonas)({
1032
+ startUrl,
1033
+ goal,
1034
+ personas: personaNames,
1035
+ maxConcurrency: concurrency,
1036
+ headless,
1037
+ });
1038
+ // Print formatted report
1039
+ const report = (0, browser_js_1.formatComparisonReport)(comparison);
1040
+ console.log(report);
1041
+ // Save JSON output if requested
1042
+ if (options.output) {
1043
+ const fs = await import("fs");
1044
+ fs.writeFileSync(options.output, JSON.stringify(comparison, null, 2));
1045
+ console.log(`\n📄 JSON report saved: ${options.output}`);
1046
+ }
1047
+ // Generate HTML report if requested
1048
+ if (options.html) {
1049
+ const fs = await import("fs");
1050
+ const htmlReport = generateHtmlReport(comparison);
1051
+ const htmlPath = options.output?.replace(".json", ".html") || "comparison-report.html";
1052
+ fs.writeFileSync(htmlPath, htmlReport);
1053
+ console.log(`\n🌐 HTML report saved: ${htmlPath}`);
1054
+ }
1055
+ // Exit with error if any personas failed
1056
+ if (comparison.summary.failureCount > 0) {
1057
+ process.exit(1);
1058
+ }
1059
+ break;
1060
+ }
684
1061
  case "persona": {
685
1062
  const subcommand = args[0];
686
1063
  switch (subcommand) {
@@ -1955,6 +2332,86 @@ async function main() {
1955
2332
  console.log("✓ Browser state reset (cookies, localStorage cleared)");
1956
2333
  break;
1957
2334
  }
2335
+ // =========================================================================
2336
+ // Natural Language Test Suites (Tier 6)
2337
+ // =========================================================================
2338
+ case "test-suite": {
2339
+ const filepath = args[0];
2340
+ const inlineTest = options.inline;
2341
+ if (!filepath && !inlineTest) {
2342
+ console.error("Usage: cbrowser test-suite <file.txt> [--continue-on-failure] [--output <report.json>]");
2343
+ console.error(" cbrowser test-suite --inline \"go to https://... ; click login ; verify ...\"");
2344
+ console.error("");
2345
+ console.error("Options:");
2346
+ console.error(" --continue-on-failure Continue running after a test fails");
2347
+ console.error(" --screenshot-on-failure Take screenshots on failure (default: true)");
2348
+ console.error(" --output <file> Save JSON report to file");
2349
+ console.error(" --html Generate HTML report");
2350
+ console.error(" --timeout <ms> Timeout per step (default: 30000)");
2351
+ console.error("");
2352
+ console.error("Test File Format:");
2353
+ console.error(" # Test: Login Flow");
2354
+ console.error(" go to https://example.com");
2355
+ console.error(" click the login button");
2356
+ console.error(" type \"user@example.com\" in email field");
2357
+ console.error(" verify url contains \"/dashboard\"");
2358
+ process.exit(1);
2359
+ }
2360
+ let suite;
2361
+ if (inlineTest) {
2362
+ // Parse inline test - semicolons separate steps
2363
+ const steps = inlineTest.split(";").map(s => s.trim()).filter(s => s);
2364
+ const testCase = {
2365
+ name: "Inline Test",
2366
+ steps: steps.map(s => (0, browser_js_1.parseNLInstruction)(s)),
2367
+ };
2368
+ suite = { name: "Inline Suite", tests: [testCase] };
2369
+ }
2370
+ else {
2371
+ // Load from file
2372
+ const fs = await import("fs");
2373
+ if (!fs.existsSync(filepath)) {
2374
+ console.error(`Test file not found: ${filepath}`);
2375
+ process.exit(1);
2376
+ }
2377
+ const content = fs.readFileSync(filepath, "utf-8");
2378
+ const suiteName = filepath.split("/").pop()?.replace(/\.[^.]+$/, "") || "Test Suite";
2379
+ suite = (0, browser_js_1.parseNLTestSuite)(content, suiteName);
2380
+ }
2381
+ console.log(`\n📝 Parsed ${suite.tests.length} test(s) from ${inlineTest ? "inline" : filepath}`);
2382
+ for (const test of suite.tests) {
2383
+ console.log(` - ${test.name}: ${test.steps.length} steps`);
2384
+ }
2385
+ const suiteOptions = {
2386
+ stepTimeout: options.timeout ? parseInt(options.timeout) : 30000,
2387
+ continueOnFailure: options["continue-on-failure"] === true,
2388
+ screenshotOnFailure: options["screenshot-on-failure"] !== false,
2389
+ headless,
2390
+ };
2391
+ const result = await (0, browser_js_1.runNLTestSuite)(suite, suiteOptions);
2392
+ // Print formatted report
2393
+ const report = (0, browser_js_1.formatNLTestReport)(result);
2394
+ console.log(report);
2395
+ // Save JSON output if requested
2396
+ if (options.output) {
2397
+ const fs = await import("fs");
2398
+ fs.writeFileSync(options.output, JSON.stringify(result, null, 2));
2399
+ console.log(`\n📄 JSON report saved: ${options.output}`);
2400
+ }
2401
+ // Generate HTML report if requested
2402
+ if (options.html) {
2403
+ const fs = await import("fs");
2404
+ const htmlReport = generateTestSuiteHtmlReport(result);
2405
+ const htmlPath = options.output?.replace(".json", ".html") || "test-report.html";
2406
+ fs.writeFileSync(htmlPath, htmlReport);
2407
+ console.log(`\n🌐 HTML report saved: ${htmlPath}`);
2408
+ }
2409
+ // Exit with error code if any tests failed
2410
+ if (result.summary.failed > 0) {
2411
+ process.exit(1);
2412
+ }
2413
+ break;
2414
+ }
1958
2415
  default:
1959
2416
  console.error(`Unknown command: ${command}`);
1960
2417
  console.error("Run 'cbrowser help' for usage");