cbrowser 2.3.0 → 3.0.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 +278 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +943 -3
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +954 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +31 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +42 -6
- package/dist/config.js.map +1 -1
- package/dist/types.d.ts +257 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +87 -0
- package/dist/types.js.map +1 -1
- package/package.json +9 -2
package/dist/cli.js
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
const browser_js_1 = require("./browser.js");
|
|
10
10
|
const personas_js_1 = require("./personas.js");
|
|
11
|
+
const types_js_1 = require("./types.js");
|
|
11
12
|
function showHelp() {
|
|
12
13
|
console.log(`
|
|
13
14
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
14
|
-
║ CBrowser CLI
|
|
15
|
-
║
|
|
15
|
+
║ CBrowser CLI v3.0.0 ║
|
|
16
|
+
║ AI-powered browser automation with natural language & fluent API ║
|
|
16
17
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
17
18
|
|
|
18
19
|
NAVIGATION
|
|
@@ -30,6 +31,7 @@ AUTONOMOUS JOURNEYS
|
|
|
30
31
|
journey <persona> Run autonomous exploration
|
|
31
32
|
--start <url> Starting URL (required)
|
|
32
33
|
--goal <goal> What to accomplish
|
|
34
|
+
--record-video Record journey as video
|
|
33
35
|
|
|
34
36
|
PERSONAS
|
|
35
37
|
persona list List available personas
|
|
@@ -41,6 +43,90 @@ SESSION MANAGEMENT
|
|
|
41
43
|
session list List all saved sessions
|
|
42
44
|
session delete <name> Delete a saved session
|
|
43
45
|
|
|
46
|
+
COOKIE MANAGEMENT
|
|
47
|
+
cookie list List all cookies for current page
|
|
48
|
+
--url <url> Navigate to URL first
|
|
49
|
+
cookie set <name> <value> Set a cookie
|
|
50
|
+
--domain <domain> Cookie domain
|
|
51
|
+
--path <path> Cookie path (default: /)
|
|
52
|
+
cookie delete <name> Delete a cookie
|
|
53
|
+
--domain <domain> Only delete for this domain
|
|
54
|
+
cookie clear Clear all cookies
|
|
55
|
+
|
|
56
|
+
DEVICE EMULATION
|
|
57
|
+
device list List available device presets
|
|
58
|
+
device set <name> Set device emulation for session
|
|
59
|
+
Example: cbrowser device set iphone-15
|
|
60
|
+
|
|
61
|
+
GEOLOCATION
|
|
62
|
+
geo list List available location presets
|
|
63
|
+
geo set <location> Set geolocation (preset name or lat,lon)
|
|
64
|
+
Examples:
|
|
65
|
+
cbrowser geo set new-york
|
|
66
|
+
cbrowser geo set 37.7749,-122.4194
|
|
67
|
+
|
|
68
|
+
PERFORMANCE
|
|
69
|
+
perf [url] Collect performance metrics
|
|
70
|
+
perf audit [url] Run performance audit against budget
|
|
71
|
+
--budget-lcp <ms> LCP budget (default: 2500)
|
|
72
|
+
--budget-fcp <ms> FCP budget (default: 1800)
|
|
73
|
+
--budget-cls <score> CLS budget (default: 0.1)
|
|
74
|
+
|
|
75
|
+
NETWORK / HAR
|
|
76
|
+
har start Start recording HAR
|
|
77
|
+
har stop [output] Stop and save HAR file
|
|
78
|
+
network list List captured network requests
|
|
79
|
+
|
|
80
|
+
VISUAL REGRESSION (v2.5.0)
|
|
81
|
+
visual save <name> Save baseline screenshot
|
|
82
|
+
--url <url> Navigate to URL first
|
|
83
|
+
visual compare <name> Compare current page against baseline
|
|
84
|
+
--threshold <n> Diff threshold 0-1 (default: 0.1)
|
|
85
|
+
visual list List all saved baselines
|
|
86
|
+
visual delete <name> Delete a baseline
|
|
87
|
+
|
|
88
|
+
ACCESSIBILITY (v2.5.0)
|
|
89
|
+
a11y audit Run WCAG accessibility audit
|
|
90
|
+
--url <url> Navigate to URL first
|
|
91
|
+
a11y audit [url] Audit a specific URL
|
|
92
|
+
|
|
93
|
+
TEST RECORDING (v2.5.0)
|
|
94
|
+
record start Start recording interactions
|
|
95
|
+
--url <url> Navigate to URL to begin recording
|
|
96
|
+
record stop Stop recording and show actions
|
|
97
|
+
record save <name> Save recorded test
|
|
98
|
+
record list List saved recordings
|
|
99
|
+
record generate <name> Generate Playwright test code
|
|
100
|
+
|
|
101
|
+
TEST EXPORT (v2.5.0)
|
|
102
|
+
export junit <name> [output] Export test results as JUnit XML
|
|
103
|
+
export tap <name> [output] Export test results as TAP format
|
|
104
|
+
|
|
105
|
+
WEBHOOKS (v2.5.0)
|
|
106
|
+
webhook add <name> <url> Add webhook notification
|
|
107
|
+
--events <events> Comma-separated: test.pass,test.fail,journey.complete
|
|
108
|
+
--format <format> slack, discord, or generic
|
|
109
|
+
webhook list List configured webhooks
|
|
110
|
+
webhook delete <name> Delete a webhook
|
|
111
|
+
webhook test <name> Send test notification
|
|
112
|
+
|
|
113
|
+
PARALLEL EXECUTION (v2.5.0)
|
|
114
|
+
parallel devices <url> Run same URL across multiple devices
|
|
115
|
+
--devices <list> Comma-separated device names (default: all)
|
|
116
|
+
--concurrency <n> Max parallel browsers (default: 3)
|
|
117
|
+
parallel urls <urls> Run same task across multiple URLs
|
|
118
|
+
--concurrency <n> Max parallel browsers (default: 3)
|
|
119
|
+
parallel perf <urls> Performance audit multiple URLs in parallel
|
|
120
|
+
--concurrency <n> Max parallel browsers (default: 3)
|
|
121
|
+
|
|
122
|
+
NATURAL LANGUAGE (v3.0.0)
|
|
123
|
+
run "<command>" Execute natural language command
|
|
124
|
+
Examples:
|
|
125
|
+
cbrowser run "go to https://example.com"
|
|
126
|
+
cbrowser run "click the login button"
|
|
127
|
+
cbrowser run "type 'hello' in the search box"
|
|
128
|
+
script <file> Execute script file with natural language commands
|
|
129
|
+
|
|
44
130
|
STORAGE & CLEANUP
|
|
45
131
|
storage Show storage usage statistics
|
|
46
132
|
cleanup Clean up old files
|
|
@@ -50,23 +136,53 @@ STORAGE & CLEANUP
|
|
|
50
136
|
--keep-journeys <n> Keep at least N journeys (default: 5)
|
|
51
137
|
|
|
52
138
|
OPTIONS
|
|
53
|
-
--browser <type> Browser
|
|
139
|
+
--browser <type> Browser: chromium, firefox, webkit (default: chromium)
|
|
140
|
+
--device <name> Device preset: iphone-15, pixel-8, ipad-pro-12, etc.
|
|
141
|
+
--geo <location> Location preset or lat,lon coordinates
|
|
142
|
+
--locale <locale> Browser locale (e.g., en-US, fr-FR)
|
|
143
|
+
--timezone <tz> Timezone (e.g., America/New_York)
|
|
144
|
+
--record-video Enable video recording
|
|
54
145
|
--force Bypass red zone safety checks
|
|
55
146
|
--headless Run browser in headless mode
|
|
56
147
|
|
|
57
148
|
ENVIRONMENT VARIABLES
|
|
58
149
|
CBROWSER_DATA_DIR Custom data directory (default: ~/.cbrowser)
|
|
59
150
|
CBROWSER_BROWSER Browser engine (chromium/firefox/webkit)
|
|
151
|
+
CBROWSER_DEVICE Device preset name
|
|
152
|
+
CBROWSER_LOCALE Browser locale
|
|
153
|
+
CBROWSER_TIMEZONE Timezone
|
|
60
154
|
CBROWSER_HEADLESS Run headless by default (true/false)
|
|
61
155
|
CBROWSER_TIMEOUT Default timeout in ms (default: 30000)
|
|
156
|
+
CBROWSER_RECORD_VIDEO Record video by default (true/false)
|
|
157
|
+
|
|
158
|
+
CONFIG FILE
|
|
159
|
+
CBrowser looks for config in these locations:
|
|
160
|
+
.cbrowserrc.json Project config
|
|
161
|
+
.cbrowserrc Project config
|
|
162
|
+
cbrowser.config.json Project config
|
|
163
|
+
~/.cbrowser/config.json User config
|
|
164
|
+
~/.cbrowserrc.json User config
|
|
165
|
+
|
|
166
|
+
Example .cbrowserrc.json:
|
|
167
|
+
{
|
|
168
|
+
"browser": "chromium",
|
|
169
|
+
"device": "iphone-15",
|
|
170
|
+
"geolocation": "new-york",
|
|
171
|
+
"locale": "en-US",
|
|
172
|
+
"recordVideo": true,
|
|
173
|
+
"performanceBudget": {
|
|
174
|
+
"lcp": 2500,
|
|
175
|
+
"cls": 0.1
|
|
176
|
+
}
|
|
177
|
+
}
|
|
62
178
|
|
|
63
179
|
EXAMPLES
|
|
64
180
|
npx cbrowser navigate "https://example.com"
|
|
65
|
-
npx cbrowser
|
|
66
|
-
npx cbrowser
|
|
67
|
-
npx cbrowser
|
|
68
|
-
npx cbrowser
|
|
69
|
-
npx cbrowser
|
|
181
|
+
npx cbrowser navigate "https://example.com" --device iphone-15
|
|
182
|
+
npx cbrowser navigate "https://example.com" --geo san-francisco
|
|
183
|
+
npx cbrowser perf audit "https://example.com" --budget-lcp 2000
|
|
184
|
+
npx cbrowser journey first-timer --start "https://example.com" --record-video
|
|
185
|
+
npx cbrowser cookie list --url "https://example.com"
|
|
70
186
|
`);
|
|
71
187
|
}
|
|
72
188
|
function parseArgs(args) {
|
|
@@ -98,6 +214,22 @@ function formatBytes(bytes) {
|
|
|
98
214
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
99
215
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
100
216
|
}
|
|
217
|
+
function parseGeoLocation(location) {
|
|
218
|
+
// Check if it's a preset
|
|
219
|
+
if (types_js_1.LOCATION_PRESETS[location]) {
|
|
220
|
+
return types_js_1.LOCATION_PRESETS[location];
|
|
221
|
+
}
|
|
222
|
+
// Try parsing as lat,lon
|
|
223
|
+
const parts = location.split(",");
|
|
224
|
+
if (parts.length === 2) {
|
|
225
|
+
const lat = parseFloat(parts[0]);
|
|
226
|
+
const lon = parseFloat(parts[1]);
|
|
227
|
+
if (!isNaN(lat) && !isNaN(lon)) {
|
|
228
|
+
return { latitude: lat, longitude: lon };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
101
233
|
async function main() {
|
|
102
234
|
const { command, args, options } = parseArgs(process.argv.slice(2));
|
|
103
235
|
if (command === "help" || options.help) {
|
|
@@ -108,9 +240,24 @@ async function main() {
|
|
|
108
240
|
const browserType = options.browser === "firefox" ? "firefox"
|
|
109
241
|
: options.browser === "webkit" ? "webkit"
|
|
110
242
|
: "chromium";
|
|
243
|
+
// Parse geolocation
|
|
244
|
+
let geolocation = undefined;
|
|
245
|
+
if (options.geo) {
|
|
246
|
+
geolocation = parseGeoLocation(options.geo);
|
|
247
|
+
if (!geolocation) {
|
|
248
|
+
console.error(`Invalid geolocation: ${options.geo}`);
|
|
249
|
+
console.error("Use a preset name (new-york, london, etc.) or lat,lon format");
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
111
253
|
const browser = new browser_js_1.CBrowser({
|
|
112
254
|
browser: browserType,
|
|
113
255
|
headless: options.headless === true || options.headless === "true",
|
|
256
|
+
device: options.device,
|
|
257
|
+
geolocation,
|
|
258
|
+
locale: options.locale,
|
|
259
|
+
timezone: options.timezone,
|
|
260
|
+
recordVideo: options["record-video"] === true,
|
|
114
261
|
});
|
|
115
262
|
try {
|
|
116
263
|
switch (command) {
|
|
@@ -332,6 +479,805 @@ async function main() {
|
|
|
332
479
|
console.log(` TOTAL: ${result.deleted} files | ${formatBytes(result.freedBytes)} ${cleanupOptions.dryRun ? "would be " : ""}freed`);
|
|
333
480
|
break;
|
|
334
481
|
}
|
|
482
|
+
// =========================================================================
|
|
483
|
+
// Cookie Management
|
|
484
|
+
// =========================================================================
|
|
485
|
+
case "cookie": {
|
|
486
|
+
const subcommand = args[0];
|
|
487
|
+
switch (subcommand) {
|
|
488
|
+
case "list": {
|
|
489
|
+
if (options.url) {
|
|
490
|
+
await browser.navigate(options.url);
|
|
491
|
+
}
|
|
492
|
+
const cookies = await browser.getCookies();
|
|
493
|
+
if (cookies.length === 0) {
|
|
494
|
+
console.log("No cookies found");
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
console.log("\n🍪 Cookies:\n");
|
|
498
|
+
for (const cookie of cookies) {
|
|
499
|
+
console.log(` ${cookie.name}`);
|
|
500
|
+
console.log(` Value: ${cookie.value.substring(0, 50)}${cookie.value.length > 50 ? "..." : ""}`);
|
|
501
|
+
console.log(` Domain: ${cookie.domain}`);
|
|
502
|
+
console.log(` Path: ${cookie.path}`);
|
|
503
|
+
console.log(` Expires: ${cookie.expires === -1 ? "Session" : new Date(cookie.expires * 1000).toISOString()}`);
|
|
504
|
+
console.log("");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case "set": {
|
|
510
|
+
const name = args[1];
|
|
511
|
+
const value = args[2];
|
|
512
|
+
if (!name || !value) {
|
|
513
|
+
console.error("Usage: cbrowser cookie set <name> <value> [--domain <domain>]");
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
if (options.url) {
|
|
517
|
+
await browser.navigate(options.url);
|
|
518
|
+
}
|
|
519
|
+
const domain = options.domain || "localhost";
|
|
520
|
+
const path = options.path || "/";
|
|
521
|
+
await browser.setCookies([{
|
|
522
|
+
name,
|
|
523
|
+
value,
|
|
524
|
+
domain,
|
|
525
|
+
path,
|
|
526
|
+
expires: -1,
|
|
527
|
+
httpOnly: false,
|
|
528
|
+
secure: false,
|
|
529
|
+
sameSite: "Lax",
|
|
530
|
+
}]);
|
|
531
|
+
console.log(`✓ Cookie set: ${name}=${value}`);
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case "delete": {
|
|
535
|
+
const name = args[1];
|
|
536
|
+
if (!name) {
|
|
537
|
+
console.error("Usage: cbrowser cookie delete <name> [--domain <domain>]");
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
if (options.url) {
|
|
541
|
+
await browser.navigate(options.url);
|
|
542
|
+
}
|
|
543
|
+
await browser.deleteCookie(name, options.domain);
|
|
544
|
+
console.log(`✓ Cookie deleted: ${name}`);
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "clear": {
|
|
548
|
+
await browser.clearCookies();
|
|
549
|
+
console.log("✓ All cookies cleared");
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
default:
|
|
553
|
+
console.error("Usage: cbrowser cookie [list|set|delete|clear]");
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
// =========================================================================
|
|
558
|
+
// Device Emulation
|
|
559
|
+
// =========================================================================
|
|
560
|
+
case "device": {
|
|
561
|
+
const subcommand = args[0];
|
|
562
|
+
switch (subcommand) {
|
|
563
|
+
case "list": {
|
|
564
|
+
console.log("\n📱 Available Device Presets:\n");
|
|
565
|
+
for (const [name, device] of Object.entries(types_js_1.DEVICE_PRESETS)) {
|
|
566
|
+
console.log(` ${name}`);
|
|
567
|
+
console.log(` ${device.name}`);
|
|
568
|
+
console.log(` ${device.viewport.width}x${device.viewport.height} @${device.deviceScaleFactor}x`);
|
|
569
|
+
console.log(` Mobile: ${device.isMobile} | Touch: ${device.hasTouch}`);
|
|
570
|
+
console.log("");
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case "set": {
|
|
575
|
+
const deviceName = args[1];
|
|
576
|
+
if (!deviceName) {
|
|
577
|
+
console.error("Usage: cbrowser device set <name>");
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
if (!types_js_1.DEVICE_PRESETS[deviceName]) {
|
|
581
|
+
console.error(`Unknown device: ${deviceName}`);
|
|
582
|
+
console.error("Run 'cbrowser device list' to see available devices");
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
console.log(`✓ Device set: ${deviceName}`);
|
|
586
|
+
console.log(" Note: Device emulation applies to new browser sessions");
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
default:
|
|
590
|
+
console.error("Usage: cbrowser device [list|set]");
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
// =========================================================================
|
|
595
|
+
// Geolocation
|
|
596
|
+
// =========================================================================
|
|
597
|
+
case "geo": {
|
|
598
|
+
const subcommand = args[0];
|
|
599
|
+
switch (subcommand) {
|
|
600
|
+
case "list": {
|
|
601
|
+
console.log("\n🌍 Available Location Presets:\n");
|
|
602
|
+
for (const [name, loc] of Object.entries(types_js_1.LOCATION_PRESETS)) {
|
|
603
|
+
console.log(` ${name}`);
|
|
604
|
+
console.log(` Lat: ${loc.latitude}, Lon: ${loc.longitude}`);
|
|
605
|
+
console.log("");
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case "set": {
|
|
610
|
+
const location = args[1];
|
|
611
|
+
if (!location) {
|
|
612
|
+
console.error("Usage: cbrowser geo set <location>");
|
|
613
|
+
console.error(" Location can be a preset name or lat,lon coordinates");
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
const geo = parseGeoLocation(location);
|
|
617
|
+
if (!geo) {
|
|
618
|
+
console.error(`Invalid location: ${location}`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
await browser.setGeolocationRuntime(geo);
|
|
622
|
+
console.log(`✓ Geolocation set: ${geo.latitude}, ${geo.longitude}`);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
default:
|
|
626
|
+
console.error("Usage: cbrowser geo [list|set]");
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
// =========================================================================
|
|
631
|
+
// Performance
|
|
632
|
+
// =========================================================================
|
|
633
|
+
case "perf": {
|
|
634
|
+
const subcommand = args[0];
|
|
635
|
+
if (subcommand === "audit") {
|
|
636
|
+
const url = args[1];
|
|
637
|
+
if (url) {
|
|
638
|
+
await browser.navigate(url);
|
|
639
|
+
}
|
|
640
|
+
else if (options.url) {
|
|
641
|
+
await browser.navigate(options.url);
|
|
642
|
+
}
|
|
643
|
+
// Create performance budget from options
|
|
644
|
+
const budget = {
|
|
645
|
+
lcp: options["budget-lcp"] ? parseInt(options["budget-lcp"]) : 2500,
|
|
646
|
+
fcp: options["budget-fcp"] ? parseInt(options["budget-fcp"]) : 1800,
|
|
647
|
+
cls: options["budget-cls"] ? parseFloat(options["budget-cls"]) : 0.1,
|
|
648
|
+
};
|
|
649
|
+
// Temporarily set budget in config
|
|
650
|
+
browser.config.performanceBudget = budget;
|
|
651
|
+
const result = await browser.auditPerformance();
|
|
652
|
+
console.log("\n📊 Performance Audit:\n");
|
|
653
|
+
console.log(` URL: ${result.url}`);
|
|
654
|
+
console.log(` Result: ${result.passed ? "✓ PASSED" : "✗ FAILED"}`);
|
|
655
|
+
console.log("");
|
|
656
|
+
console.log(" Metrics:");
|
|
657
|
+
if (result.metrics.lcp)
|
|
658
|
+
console.log(` LCP: ${result.metrics.lcp.toFixed(0)}ms (${result.metrics.lcpRating})`);
|
|
659
|
+
if (result.metrics.fcp)
|
|
660
|
+
console.log(` FCP: ${result.metrics.fcp.toFixed(0)}ms`);
|
|
661
|
+
if (result.metrics.cls !== undefined)
|
|
662
|
+
console.log(` CLS: ${result.metrics.cls.toFixed(3)} (${result.metrics.clsRating})`);
|
|
663
|
+
if (result.metrics.ttfb)
|
|
664
|
+
console.log(` TTFB: ${result.metrics.ttfb.toFixed(0)}ms`);
|
|
665
|
+
if (result.metrics.load)
|
|
666
|
+
console.log(` Load: ${result.metrics.load.toFixed(0)}ms`);
|
|
667
|
+
if (result.metrics.resourceCount)
|
|
668
|
+
console.log(` Resources: ${result.metrics.resourceCount}`);
|
|
669
|
+
if (result.metrics.transferSize)
|
|
670
|
+
console.log(` Transfer: ${formatBytes(result.metrics.transferSize)}`);
|
|
671
|
+
if (result.violations.length > 0) {
|
|
672
|
+
console.log("");
|
|
673
|
+
console.log(" ⚠️ Budget Violations:");
|
|
674
|
+
for (const v of result.violations) {
|
|
675
|
+
console.log(` - ${v}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
// Just collect metrics
|
|
681
|
+
const url = args[0];
|
|
682
|
+
if (url) {
|
|
683
|
+
await browser.navigate(url);
|
|
684
|
+
}
|
|
685
|
+
else if (options.url) {
|
|
686
|
+
await browser.navigate(options.url);
|
|
687
|
+
}
|
|
688
|
+
const metrics = await browser.getPerformanceMetrics();
|
|
689
|
+
console.log("\n📊 Performance Metrics:\n");
|
|
690
|
+
if (metrics.lcp)
|
|
691
|
+
console.log(` LCP: ${metrics.lcp.toFixed(0)}ms (${metrics.lcpRating})`);
|
|
692
|
+
if (metrics.fcp)
|
|
693
|
+
console.log(` FCP: ${metrics.fcp.toFixed(0)}ms`);
|
|
694
|
+
if (metrics.cls !== undefined)
|
|
695
|
+
console.log(` CLS: ${metrics.cls.toFixed(3)} (${metrics.clsRating})`);
|
|
696
|
+
if (metrics.ttfb)
|
|
697
|
+
console.log(` TTFB: ${metrics.ttfb.toFixed(0)}ms`);
|
|
698
|
+
if (metrics.domContentLoaded)
|
|
699
|
+
console.log(` DOMContentLoaded: ${metrics.domContentLoaded.toFixed(0)}ms`);
|
|
700
|
+
if (metrics.load)
|
|
701
|
+
console.log(` Load: ${metrics.load.toFixed(0)}ms`);
|
|
702
|
+
if (metrics.resourceCount)
|
|
703
|
+
console.log(` Resources: ${metrics.resourceCount}`);
|
|
704
|
+
if (metrics.transferSize)
|
|
705
|
+
console.log(` Transfer Size: ${formatBytes(metrics.transferSize)}`);
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
// =========================================================================
|
|
710
|
+
// HAR Recording
|
|
711
|
+
// =========================================================================
|
|
712
|
+
case "har": {
|
|
713
|
+
const subcommand = args[0];
|
|
714
|
+
switch (subcommand) {
|
|
715
|
+
case "start": {
|
|
716
|
+
browser.startHarRecording();
|
|
717
|
+
console.log("✓ HAR recording started");
|
|
718
|
+
console.log(" Navigate and interact with pages, then run 'cbrowser har stop'");
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
case "stop": {
|
|
722
|
+
const output = args[1];
|
|
723
|
+
const filename = await browser.exportHar(output);
|
|
724
|
+
console.log(`✓ HAR saved: ${filename}`);
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
default:
|
|
728
|
+
console.error("Usage: cbrowser har [start|stop]");
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
// =========================================================================
|
|
733
|
+
// Network
|
|
734
|
+
// =========================================================================
|
|
735
|
+
case "network": {
|
|
736
|
+
const subcommand = args[0];
|
|
737
|
+
switch (subcommand) {
|
|
738
|
+
case "list": {
|
|
739
|
+
const requests = browser.getNetworkRequests();
|
|
740
|
+
if (requests.length === 0) {
|
|
741
|
+
console.log("No network requests captured");
|
|
742
|
+
console.log("Navigate to a page first to capture requests");
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
console.log(`\n🌐 Network Requests (${requests.length}):\n`);
|
|
746
|
+
for (const req of requests.slice(-20)) {
|
|
747
|
+
console.log(` ${req.method} ${req.url.substring(0, 80)}${req.url.length > 80 ? "..." : ""}`);
|
|
748
|
+
console.log(` Type: ${req.resourceType} | Time: ${req.timestamp}`);
|
|
749
|
+
}
|
|
750
|
+
if (requests.length > 20) {
|
|
751
|
+
console.log(`\n ... and ${requests.length - 20} more requests`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case "clear": {
|
|
757
|
+
browser.clearNetworkHistory();
|
|
758
|
+
console.log("✓ Network history cleared");
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
default:
|
|
762
|
+
console.error("Usage: cbrowser network [list|clear]");
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
// =========================================================================
|
|
767
|
+
// Visual Regression (Tier 2)
|
|
768
|
+
// =========================================================================
|
|
769
|
+
case "visual": {
|
|
770
|
+
const subcommand = args[0];
|
|
771
|
+
switch (subcommand) {
|
|
772
|
+
case "save": {
|
|
773
|
+
const name = args[1];
|
|
774
|
+
if (!name) {
|
|
775
|
+
console.error("Usage: cbrowser visual save <name> [--url <url>]");
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
if (options.url) {
|
|
779
|
+
await browser.navigate(options.url);
|
|
780
|
+
}
|
|
781
|
+
const path = await browser.saveBaseline(name);
|
|
782
|
+
console.log(`✓ Baseline saved: ${name}`);
|
|
783
|
+
console.log(` Path: ${path}`);
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case "compare": {
|
|
787
|
+
const name = args[1];
|
|
788
|
+
if (!name) {
|
|
789
|
+
console.error("Usage: cbrowser visual compare <name> [--threshold <n>]");
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
if (options.url) {
|
|
793
|
+
await browser.navigate(options.url);
|
|
794
|
+
}
|
|
795
|
+
const threshold = options.threshold ? parseFloat(options.threshold) : 0.1;
|
|
796
|
+
const result = await browser.compareBaseline(name, threshold);
|
|
797
|
+
console.log("\n🔍 Visual Comparison:\n");
|
|
798
|
+
console.log(` Baseline: ${name}`);
|
|
799
|
+
console.log(` Difference: ${(result.diffPercentage * 100).toFixed(2)}%`);
|
|
800
|
+
console.log(` Threshold: ${(threshold * 100).toFixed(0)}%`);
|
|
801
|
+
console.log(` Result: ${result.passed ? "✓ PASSED" : "✗ FAILED"}`);
|
|
802
|
+
if (result.diffPath) {
|
|
803
|
+
console.log(` Diff image: ${result.diffPath}`);
|
|
804
|
+
}
|
|
805
|
+
if (!result.passed) {
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
case "list": {
|
|
811
|
+
const baselines = browser.listBaselines();
|
|
812
|
+
if (baselines.length === 0) {
|
|
813
|
+
console.log("No baselines saved");
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
console.log("\n📸 Visual Baselines:\n");
|
|
817
|
+
for (const b of baselines) {
|
|
818
|
+
console.log(` - ${b}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
case "delete": {
|
|
824
|
+
const name = args[1];
|
|
825
|
+
if (!name) {
|
|
826
|
+
console.error("Usage: cbrowser visual delete <name>");
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
// Delete baseline file
|
|
830
|
+
const fs = await import("fs");
|
|
831
|
+
const path = await import("path");
|
|
832
|
+
const baselinePath = path.join(browser.getDataDir(), "baselines", `${name}.png`);
|
|
833
|
+
if (fs.existsSync(baselinePath)) {
|
|
834
|
+
fs.unlinkSync(baselinePath);
|
|
835
|
+
console.log(`✓ Baseline deleted: ${name}`);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
console.error(`✗ Baseline not found: ${name}`);
|
|
839
|
+
process.exit(1);
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
default:
|
|
844
|
+
console.error("Usage: cbrowser visual [save|compare|list|delete]");
|
|
845
|
+
}
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
// =========================================================================
|
|
849
|
+
// Accessibility (Tier 2)
|
|
850
|
+
// =========================================================================
|
|
851
|
+
case "a11y": {
|
|
852
|
+
const subcommand = args[0];
|
|
853
|
+
if (subcommand === "audit") {
|
|
854
|
+
const url = args[1];
|
|
855
|
+
if (url) {
|
|
856
|
+
await browser.navigate(url);
|
|
857
|
+
}
|
|
858
|
+
else if (options.url) {
|
|
859
|
+
await browser.navigate(options.url);
|
|
860
|
+
}
|
|
861
|
+
const result = await browser.auditAccessibility();
|
|
862
|
+
console.log("\n♿ Accessibility Audit:\n");
|
|
863
|
+
console.log(` URL: ${result.url}`);
|
|
864
|
+
console.log(` Score: ${result.score}/100`);
|
|
865
|
+
console.log(` Passes: ${result.passes}`);
|
|
866
|
+
console.log(` Violations: ${result.violations.length}`);
|
|
867
|
+
if (result.violations.length > 0) {
|
|
868
|
+
console.log("\n ⚠️ Violations:\n");
|
|
869
|
+
for (const v of result.violations) {
|
|
870
|
+
console.log(` [${v.impact.toUpperCase()}] ${v.id}`);
|
|
871
|
+
console.log(` ${v.description}`);
|
|
872
|
+
console.log(` Help: ${v.helpUrl}`);
|
|
873
|
+
console.log("");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
console.error("Usage: cbrowser a11y audit [url]");
|
|
879
|
+
}
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
// =========================================================================
|
|
883
|
+
// Test Recording (Tier 2)
|
|
884
|
+
// =========================================================================
|
|
885
|
+
case "record": {
|
|
886
|
+
const subcommand = args[0];
|
|
887
|
+
switch (subcommand) {
|
|
888
|
+
case "start": {
|
|
889
|
+
const url = options.url;
|
|
890
|
+
await browser.startRecording(url);
|
|
891
|
+
console.log("✓ Recording started");
|
|
892
|
+
if (url) {
|
|
893
|
+
console.log(` Navigated to: ${url}`);
|
|
894
|
+
}
|
|
895
|
+
console.log(" Interact with the page, then run 'cbrowser record stop'");
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
case "stop": {
|
|
899
|
+
const actions = browser.stopRecording();
|
|
900
|
+
console.log(`✓ Recording stopped`);
|
|
901
|
+
console.log(` Captured ${actions.length} actions`);
|
|
902
|
+
if (actions.length > 0) {
|
|
903
|
+
console.log("\n Actions:");
|
|
904
|
+
for (const action of actions) {
|
|
905
|
+
console.log(` ${action.type}: ${action.selector || action.url || action.value || ""}`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
case "save": {
|
|
911
|
+
const name = args[1];
|
|
912
|
+
if (!name) {
|
|
913
|
+
console.error("Usage: cbrowser record save <name>");
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
const path = browser.saveRecording(name);
|
|
917
|
+
console.log(`✓ Recording saved: ${name}`);
|
|
918
|
+
console.log(` Path: ${path}`);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
case "list": {
|
|
922
|
+
const fs = await import("fs");
|
|
923
|
+
const path = await import("path");
|
|
924
|
+
const recordingsDir = path.join(browser.getDataDir(), "recordings");
|
|
925
|
+
if (!fs.existsSync(recordingsDir)) {
|
|
926
|
+
console.log("No recordings saved");
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
const files = fs.readdirSync(recordingsDir).filter((f) => f.endsWith(".json"));
|
|
930
|
+
if (files.length === 0) {
|
|
931
|
+
console.log("No recordings saved");
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
console.log("\n🎬 Saved Recordings:\n");
|
|
935
|
+
for (const f of files) {
|
|
936
|
+
console.log(` - ${f.replace(".json", "")}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
case "generate": {
|
|
943
|
+
const name = args[1];
|
|
944
|
+
if (!name) {
|
|
945
|
+
console.error("Usage: cbrowser record generate <name>");
|
|
946
|
+
process.exit(1);
|
|
947
|
+
}
|
|
948
|
+
const fs = await import("fs");
|
|
949
|
+
const path = await import("path");
|
|
950
|
+
const recordingPath = path.join(browser.getDataDir(), "recordings", `${name}.json`);
|
|
951
|
+
if (!fs.existsSync(recordingPath)) {
|
|
952
|
+
console.error(`Recording not found: ${name}`);
|
|
953
|
+
process.exit(1);
|
|
954
|
+
}
|
|
955
|
+
const recording = JSON.parse(fs.readFileSync(recordingPath, "utf-8"));
|
|
956
|
+
const code = browser.generateTestCode(name, recording.actions);
|
|
957
|
+
console.log(code);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
default:
|
|
961
|
+
console.error("Usage: cbrowser record [start|stop|save|list|generate]");
|
|
962
|
+
}
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
// =========================================================================
|
|
966
|
+
// Test Export (Tier 2)
|
|
967
|
+
// =========================================================================
|
|
968
|
+
case "export": {
|
|
969
|
+
const format = args[0];
|
|
970
|
+
const name = args[1];
|
|
971
|
+
const output = args[2];
|
|
972
|
+
if (!format || !name) {
|
|
973
|
+
console.error("Usage: cbrowser export [junit|tap] <name> [output]");
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
// Load test results (for now, create a mock suite)
|
|
977
|
+
const fs = await import("fs");
|
|
978
|
+
const path = await import("path");
|
|
979
|
+
const resultsPath = path.join(browser.getDataDir(), "results", `${name}.json`);
|
|
980
|
+
let suite;
|
|
981
|
+
if (fs.existsSync(resultsPath)) {
|
|
982
|
+
suite = JSON.parse(fs.readFileSync(resultsPath, "utf-8"));
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
console.error(`Test results not found: ${name}`);
|
|
986
|
+
console.error("Run tests first to generate results");
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
if (format === "junit") {
|
|
990
|
+
const exportPath = browser.exportJUnit(suite, output);
|
|
991
|
+
console.log(`✓ JUnit XML exported: ${exportPath}`);
|
|
992
|
+
}
|
|
993
|
+
else if (format === "tap") {
|
|
994
|
+
const exportPath = browser.exportTAP(suite, output);
|
|
995
|
+
console.log(`✓ TAP exported: ${exportPath}`);
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
console.error("Unknown export format. Use 'junit' or 'tap'");
|
|
999
|
+
process.exit(1);
|
|
1000
|
+
}
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
// =========================================================================
|
|
1004
|
+
// Webhooks (Tier 2)
|
|
1005
|
+
// =========================================================================
|
|
1006
|
+
case "webhook": {
|
|
1007
|
+
const subcommand = args[0];
|
|
1008
|
+
const fs = await import("fs");
|
|
1009
|
+
const path = await import("path");
|
|
1010
|
+
const webhooksPath = path.join(browser.getDataDir(), "webhooks.json");
|
|
1011
|
+
// Load existing webhooks
|
|
1012
|
+
let webhooks = [];
|
|
1013
|
+
if (fs.existsSync(webhooksPath)) {
|
|
1014
|
+
webhooks = JSON.parse(fs.readFileSync(webhooksPath, "utf-8"));
|
|
1015
|
+
}
|
|
1016
|
+
switch (subcommand) {
|
|
1017
|
+
case "add": {
|
|
1018
|
+
const name = args[1];
|
|
1019
|
+
const url = args[2];
|
|
1020
|
+
if (!name || !url) {
|
|
1021
|
+
console.error("Usage: cbrowser webhook add <name> <url> [--events <events>] [--format <format>]");
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
const events = options.events
|
|
1025
|
+
? options.events.split(",")
|
|
1026
|
+
: ["test.fail", "journey.complete"];
|
|
1027
|
+
const format = options.format || "generic";
|
|
1028
|
+
// Remove existing webhook with same name
|
|
1029
|
+
webhooks = webhooks.filter(w => w.name !== name);
|
|
1030
|
+
webhooks.push({ name, url, events, format });
|
|
1031
|
+
fs.writeFileSync(webhooksPath, JSON.stringify(webhooks, null, 2));
|
|
1032
|
+
console.log(`✓ Webhook added: ${name}`);
|
|
1033
|
+
console.log(` URL: ${url}`);
|
|
1034
|
+
console.log(` Events: ${events.join(", ")}`);
|
|
1035
|
+
console.log(` Format: ${format}`);
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
case "list": {
|
|
1039
|
+
if (webhooks.length === 0) {
|
|
1040
|
+
console.log("No webhooks configured");
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
console.log("\n🔔 Configured Webhooks:\n");
|
|
1044
|
+
for (const w of webhooks) {
|
|
1045
|
+
console.log(` ${w.name}`);
|
|
1046
|
+
console.log(` URL: ${w.url}`);
|
|
1047
|
+
console.log(` Events: ${w.events.join(", ")}`);
|
|
1048
|
+
console.log(` Format: ${w.format}`);
|
|
1049
|
+
console.log("");
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
case "delete": {
|
|
1055
|
+
const name = args[1];
|
|
1056
|
+
if (!name) {
|
|
1057
|
+
console.error("Usage: cbrowser webhook delete <name>");
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
}
|
|
1060
|
+
const originalLength = webhooks.length;
|
|
1061
|
+
webhooks = webhooks.filter(w => w.name !== name);
|
|
1062
|
+
if (webhooks.length < originalLength) {
|
|
1063
|
+
fs.writeFileSync(webhooksPath, JSON.stringify(webhooks, null, 2));
|
|
1064
|
+
console.log(`✓ Webhook deleted: ${name}`);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
console.error(`✗ Webhook not found: ${name}`);
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
case "test": {
|
|
1073
|
+
const name = args[1];
|
|
1074
|
+
if (!name) {
|
|
1075
|
+
console.error("Usage: cbrowser webhook test <name>");
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
}
|
|
1078
|
+
const webhook = webhooks.find(w => w.name === name);
|
|
1079
|
+
if (!webhook) {
|
|
1080
|
+
console.error(`✗ Webhook not found: ${name}`);
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
// Send test notification
|
|
1084
|
+
const testPayload = webhook.format === "slack"
|
|
1085
|
+
? { text: "🔔 CBrowser test notification" }
|
|
1086
|
+
: webhook.format === "discord"
|
|
1087
|
+
? { content: "🔔 CBrowser test notification" }
|
|
1088
|
+
: { event: "test", message: "CBrowser test notification", timestamp: new Date().toISOString() };
|
|
1089
|
+
try {
|
|
1090
|
+
const response = await fetch(webhook.url, {
|
|
1091
|
+
method: "POST",
|
|
1092
|
+
headers: { "Content-Type": "application/json" },
|
|
1093
|
+
body: JSON.stringify(testPayload),
|
|
1094
|
+
});
|
|
1095
|
+
if (response.ok) {
|
|
1096
|
+
console.log(`✓ Test notification sent to: ${name}`);
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
console.error(`✗ Webhook returned ${response.status}`);
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch (e) {
|
|
1104
|
+
console.error(`✗ Failed to send notification: ${e.message}`);
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
default:
|
|
1110
|
+
console.error("Usage: cbrowser webhook [add|list|delete|test]");
|
|
1111
|
+
}
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
// =========================================================================
|
|
1115
|
+
// Parallel Execution (Tier 2)
|
|
1116
|
+
// =========================================================================
|
|
1117
|
+
case "parallel": {
|
|
1118
|
+
const subcommand = args[0];
|
|
1119
|
+
switch (subcommand) {
|
|
1120
|
+
case "devices": {
|
|
1121
|
+
const url = args[1];
|
|
1122
|
+
if (!url) {
|
|
1123
|
+
console.error("Usage: cbrowser parallel devices <url> [--devices <list>] [--concurrency <n>]");
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
const deviceList = options.devices
|
|
1127
|
+
? options.devices.split(",")
|
|
1128
|
+
: Object.keys(types_js_1.DEVICE_PRESETS);
|
|
1129
|
+
const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
|
|
1130
|
+
console.log(`\n🚀 Running parallel device tests...`);
|
|
1131
|
+
console.log(` URL: ${url}`);
|
|
1132
|
+
console.log(` Devices: ${deviceList.length}`);
|
|
1133
|
+
console.log(` Concurrency: ${concurrency}\n`);
|
|
1134
|
+
const results = await browser_js_1.CBrowser.parallelDevices(deviceList, async (b, device) => {
|
|
1135
|
+
const nav = await b.navigate(url);
|
|
1136
|
+
const screenshot = await b.screenshot();
|
|
1137
|
+
return { title: nav.title, loadTime: nav.loadTime, screenshot };
|
|
1138
|
+
}, { maxConcurrency: concurrency });
|
|
1139
|
+
console.log("📊 Results:\n");
|
|
1140
|
+
for (const r of results) {
|
|
1141
|
+
if (r.error) {
|
|
1142
|
+
console.log(` ✗ ${r.device}: ${r.error} (${r.duration}ms)`);
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
console.log(` ✓ ${r.device}: ${r.result?.title} - ${r.result?.loadTime}ms (${r.duration}ms total)`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const passed = results.filter(r => !r.error).length;
|
|
1149
|
+
console.log(`\n Summary: ${passed}/${results.length} passed`);
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
case "urls": {
|
|
1153
|
+
const urls = args.slice(1);
|
|
1154
|
+
if (urls.length === 0) {
|
|
1155
|
+
console.error("Usage: cbrowser parallel urls <url1> <url2> ... [--concurrency <n>]");
|
|
1156
|
+
process.exit(1);
|
|
1157
|
+
}
|
|
1158
|
+
const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
|
|
1159
|
+
console.log(`\n🚀 Running parallel URL tests...`);
|
|
1160
|
+
console.log(` URLs: ${urls.length}`);
|
|
1161
|
+
console.log(` Concurrency: ${concurrency}\n`);
|
|
1162
|
+
const results = await browser_js_1.CBrowser.parallelUrls(urls, async (b, url) => {
|
|
1163
|
+
const nav = await b.navigate(url);
|
|
1164
|
+
return { title: nav.title, loadTime: nav.loadTime };
|
|
1165
|
+
}, { maxConcurrency: concurrency });
|
|
1166
|
+
console.log("📊 Results:\n");
|
|
1167
|
+
for (const r of results) {
|
|
1168
|
+
if (r.error) {
|
|
1169
|
+
console.log(` ✗ ${r.url}: ${r.error}`);
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
console.log(` ✓ ${r.url}: ${r.result?.title} (${r.result?.loadTime}ms)`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
case "perf": {
|
|
1178
|
+
const urls = args.slice(1);
|
|
1179
|
+
if (urls.length === 0) {
|
|
1180
|
+
console.error("Usage: cbrowser parallel perf <url1> <url2> ... [--concurrency <n>]");
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
|
|
1184
|
+
console.log(`\n🚀 Running parallel performance audits...`);
|
|
1185
|
+
console.log(` URLs: ${urls.length}`);
|
|
1186
|
+
console.log(` Concurrency: ${concurrency}\n`);
|
|
1187
|
+
const results = await browser_js_1.CBrowser.parallelUrls(urls, async (b, url) => {
|
|
1188
|
+
await b.navigate(url);
|
|
1189
|
+
return await b.getPerformanceMetrics();
|
|
1190
|
+
}, { maxConcurrency: concurrency });
|
|
1191
|
+
console.log("📊 Performance Results:\n");
|
|
1192
|
+
for (const r of results) {
|
|
1193
|
+
if (r.error) {
|
|
1194
|
+
console.log(` ✗ ${r.url}: ${r.error}`);
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
const m = r.result;
|
|
1198
|
+
console.log(` ✓ ${r.url}`);
|
|
1199
|
+
if (m?.lcp)
|
|
1200
|
+
console.log(` LCP: ${m.lcp.toFixed(0)}ms (${m.lcpRating})`);
|
|
1201
|
+
if (m?.fcp)
|
|
1202
|
+
console.log(` FCP: ${m.fcp.toFixed(0)}ms`);
|
|
1203
|
+
if (m?.cls !== undefined)
|
|
1204
|
+
console.log(` CLS: ${m.cls.toFixed(3)}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
default:
|
|
1210
|
+
console.error("Usage: cbrowser parallel [devices|urls|perf]");
|
|
1211
|
+
}
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
// =========================================================================
|
|
1215
|
+
// Natural Language (Tier 3)
|
|
1216
|
+
// =========================================================================
|
|
1217
|
+
case "run": {
|
|
1218
|
+
const nlCommand = args.join(" ");
|
|
1219
|
+
if (!nlCommand) {
|
|
1220
|
+
console.error("Usage: cbrowser run \"<natural language command>\"");
|
|
1221
|
+
console.error("Examples:");
|
|
1222
|
+
console.error(" cbrowser run \"go to https://example.com\"");
|
|
1223
|
+
console.error(" cbrowser run \"click the login button\"");
|
|
1224
|
+
console.error(" cbrowser run \"type 'hello' in the search box\"");
|
|
1225
|
+
process.exit(1);
|
|
1226
|
+
}
|
|
1227
|
+
console.log(`\n🗣️ Executing: "${nlCommand}"\n`);
|
|
1228
|
+
const result = await (0, browser_js_1.executeNaturalLanguage)(browser, nlCommand);
|
|
1229
|
+
if (result.success) {
|
|
1230
|
+
console.log(`✓ Action: ${result.action}`);
|
|
1231
|
+
if (result.result && typeof result.result === "object") {
|
|
1232
|
+
const r = result.result;
|
|
1233
|
+
if (r.url)
|
|
1234
|
+
console.log(` URL: ${r.url}`);
|
|
1235
|
+
if (r.title)
|
|
1236
|
+
console.log(` Title: ${r.title}`);
|
|
1237
|
+
if (r.message)
|
|
1238
|
+
console.log(` ${r.message}`);
|
|
1239
|
+
if (r.screenshot)
|
|
1240
|
+
console.log(` Screenshot: ${r.screenshot}`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
console.error(`✗ ${result.error}`);
|
|
1245
|
+
process.exit(1);
|
|
1246
|
+
}
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
case "script": {
|
|
1250
|
+
const scriptFile = args[0];
|
|
1251
|
+
if (!scriptFile) {
|
|
1252
|
+
console.error("Usage: cbrowser script <file>");
|
|
1253
|
+
process.exit(1);
|
|
1254
|
+
}
|
|
1255
|
+
const fs = await import("fs");
|
|
1256
|
+
if (!fs.existsSync(scriptFile)) {
|
|
1257
|
+
console.error(`Script file not found: ${scriptFile}`);
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
}
|
|
1260
|
+
const content = fs.readFileSync(scriptFile, "utf-8");
|
|
1261
|
+
const commands = content.split("\n").filter(line => line.trim() && !line.trim().startsWith("#"));
|
|
1262
|
+
console.log(`\n📜 Executing script: ${scriptFile}`);
|
|
1263
|
+
console.log(` Commands: ${commands.length}\n`);
|
|
1264
|
+
const results = await (0, browser_js_1.executeNaturalLanguageScript)(browser, commands);
|
|
1265
|
+
for (const r of results) {
|
|
1266
|
+
if (r.success) {
|
|
1267
|
+
console.log(`✓ ${r.command}`);
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
console.log(`✗ ${r.command}`);
|
|
1271
|
+
console.log(` Error: ${r.error}`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const passed = results.filter(r => r.success).length;
|
|
1275
|
+
console.log(`\n Summary: ${passed}/${results.length} commands succeeded`);
|
|
1276
|
+
if (passed < results.length) {
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
335
1281
|
default:
|
|
336
1282
|
console.error(`Unknown command: ${command}`);
|
|
337
1283
|
console.error("Run 'cbrowser help' for usage");
|