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/README.md +131 -0
- package/dist/browser.d.ts +87 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +626 -0
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +459 -2
- package/dist/cli.js.map +1 -1
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
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
|
|
17
|
-
║ AI-powered browser automation with
|
|
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");
|