cbrowser 2.4.0 → 3.0.1

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
@@ -12,8 +12,8 @@ const types_js_1 = require("./types.js");
12
12
  function showHelp() {
13
13
  console.log(`
14
14
  ╔══════════════════════════════════════════════════════════════════════════════╗
15
- ║ CBrowser CLI v2.4.0 ║
16
- AI-powered browser automation with devices, geo & performance
15
+ ║ CBrowser CLI v3.0.0 ║
16
+ AI-powered browser automation with natural language & fluent API
17
17
  ╚══════════════════════════════════════════════════════════════════════════════╝
18
18
 
19
19
  NAVIGATION
@@ -77,6 +77,56 @@ NETWORK / HAR
77
77
  har stop [output] Stop and save HAR file
78
78
  network list List captured network requests
79
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
+
80
130
  STORAGE & CLEANUP
81
131
  storage Show storage usage statistics
82
132
  cleanup Clean up old files
@@ -713,6 +763,521 @@ async function main() {
713
763
  }
714
764
  break;
715
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
+ }
716
1281
  default:
717
1282
  console.error(`Unknown command: ${command}`);
718
1283
  console.error("Run 'cbrowser help' for usage");