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/browser.d.ts +181 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +455 -1
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +567 -2
- package/dist/cli.js.map +1 -1
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
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
|
|
16
|
-
║
|
|
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");
|