browser-pilot 0.0.12 → 0.0.13
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 +13 -7
- package/dist/actions.cjs +63 -10
- package/dist/actions.d.cts +3 -3
- package/dist/actions.d.ts +3 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser.cjs +629 -57
- package/dist/browser.d.cts +9 -3
- package/dist/browser.d.ts +9 -3
- package/dist/browser.mjs +3 -3
- package/dist/cdp.cjs +1 -1
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +1 -1
- package/dist/{chunk-NLIARNEE.mjs → chunk-A2ZRAEO3.mjs} +63 -10
- package/dist/{chunk-4MBSALQL.mjs → chunk-HP6R3W32.mjs} +1 -1
- package/dist/{chunk-RUWAXHDX.mjs → chunk-VDAMDOS6.mjs} +606 -57
- package/dist/cli.mjs +1145 -127
- package/dist/{client-7Nqka5MV.d.ts → client-DRqxBdHv.d.cts} +1 -1
- package/dist/{client-7Nqka5MV.d.cts → client-DRqxBdHv.d.ts} +1 -1
- package/dist/index.cjs +668 -66
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/{types-j23Iqo2L.d.ts → types-BXMGFtnB.d.cts} +46 -5
- package/dist/{types-BOPu0OQZ.d.cts → types-CzgQjai9.d.ts} +46 -5
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -72,6 +72,12 @@ CONTENT EXTRACTION
|
|
|
72
72
|
{"action": "snapshot"}
|
|
73
73
|
Get accessibility tree (best for understanding page structure).
|
|
74
74
|
|
|
75
|
+
{"action": "forms"}
|
|
76
|
+
List form controls with labels, values, checked state, and options metadata.
|
|
77
|
+
|
|
78
|
+
{"action": "text"}
|
|
79
|
+
Extract visible page text.
|
|
80
|
+
|
|
75
81
|
{"action": "screenshot"}
|
|
76
82
|
{"action": "screenshot", "fullPage": true, "format": "jpeg", "quality": 80}
|
|
77
83
|
Capture screenshot. Formats: png | jpeg | webp.
|
|
@@ -79,6 +85,15 @@ CONTENT EXTRACTION
|
|
|
79
85
|
{"action": "evaluate", "value": "document.title"}
|
|
80
86
|
Run JavaScript and return result.
|
|
81
87
|
|
|
88
|
+
TAB MANAGEMENT
|
|
89
|
+
{"action": "newTab"}
|
|
90
|
+
{"action": "newTab", "url": "https://example.com"}
|
|
91
|
+
Create a new tab and optionally navigate it. Returns { targetId }.
|
|
92
|
+
|
|
93
|
+
{"action": "closeTab"}
|
|
94
|
+
{"action": "closeTab", "targetId": "TARGET_ID"}
|
|
95
|
+
Close the current tab or a specific target by ID.
|
|
96
|
+
|
|
82
97
|
IFRAME NAVIGATION
|
|
83
98
|
{"action": "switchFrame", "selector": "iframe#checkout"}
|
|
84
99
|
Switch context to an iframe. All subsequent actions target the iframe content.
|
|
@@ -110,13 +125,19 @@ COMMON OPTIONS (all actions)
|
|
|
110
125
|
|
|
111
126
|
REF SELECTORS (from snapshot)
|
|
112
127
|
After taking a snapshot, use refs directly:
|
|
113
|
-
bp snapshot -s dev --format text # Shows: button "Submit"
|
|
128
|
+
bp snapshot -s dev --format text # Shows: button "Submit" ref:e4
|
|
114
129
|
bp exec '{"action":"click","selector":"ref:e4"}'
|
|
115
130
|
|
|
116
131
|
Refs are stable until navigation. Prefix with "ref:" to use.
|
|
117
132
|
CLI caches refs per session+URL after snapshot, so they can be reused across exec calls.
|
|
118
133
|
Example: {"action":"fill","selector":"ref:e23","value":"hello"}
|
|
119
134
|
|
|
135
|
+
TEXT / ROLE SELECTORS
|
|
136
|
+
text:Continue Match by accessible text/name (partial match)
|
|
137
|
+
text:="Save Draft" Exact text match
|
|
138
|
+
role:button:Continue Match by role and optional accessible name
|
|
139
|
+
role:textbox:Email Useful when stable CSS selectors are missing
|
|
140
|
+
|
|
120
141
|
MULTI-SELECTOR PATTERN
|
|
121
142
|
All selectors accept arrays: ["#id", ".class", "[aria-label=X]"]
|
|
122
143
|
Tries each in order until one succeeds.
|
|
@@ -129,6 +150,14 @@ SELECTOR PRIORITY (Most to Least Reliable)
|
|
|
129
150
|
4. [aria-label="..."] - Good for buttons without testids
|
|
130
151
|
5. Multi-selector array - Fallback pattern for compatibility
|
|
131
152
|
|
|
153
|
+
ASSERTIONS
|
|
154
|
+
{"action":"assertVisible","selector":"#success"}
|
|
155
|
+
{"action":"assertExists","selector":"#mounted-node"}
|
|
156
|
+
{"action":"assertText","expect":"Welcome back"}
|
|
157
|
+
{"action":"assertUrl","expect":"/dashboard"}
|
|
158
|
+
{"action":"assertValue","selector":"#email","expect":"user@example.com"}
|
|
159
|
+
Assertion steps verify state inline inside a batch workflow.
|
|
160
|
+
|
|
132
161
|
SHADOW DOM
|
|
133
162
|
Selectors automatically pierce shadow DOM (1-2 levels). No special syntax needed.
|
|
134
163
|
For deeper nesting (3+ levels), use refs from snapshot - they work at any depth.
|
|
@@ -1289,6 +1318,9 @@ var BatchExecutor = class {
|
|
|
1289
1318
|
const snapshot = await this.page.snapshot();
|
|
1290
1319
|
return { value: snapshot };
|
|
1291
1320
|
}
|
|
1321
|
+
case "forms": {
|
|
1322
|
+
return { value: await this.page.forms() };
|
|
1323
|
+
}
|
|
1292
1324
|
case "screenshot": {
|
|
1293
1325
|
const data = await this.page.screenshot({
|
|
1294
1326
|
format: step.format,
|
|
@@ -1308,6 +1340,21 @@ var BatchExecutor = class {
|
|
|
1308
1340
|
const text = await this.page.text(selector);
|
|
1309
1341
|
return { text, selectorUsed: selector };
|
|
1310
1342
|
}
|
|
1343
|
+
case "newTab": {
|
|
1344
|
+
const { targetId } = await this.page.cdpClient.send(
|
|
1345
|
+
"Target.createTarget",
|
|
1346
|
+
{
|
|
1347
|
+
url: step.url ?? "about:blank"
|
|
1348
|
+
},
|
|
1349
|
+
null
|
|
1350
|
+
);
|
|
1351
|
+
return { value: { targetId } };
|
|
1352
|
+
}
|
|
1353
|
+
case "closeTab": {
|
|
1354
|
+
const targetId = step.targetId ?? this.page.targetId;
|
|
1355
|
+
await this.page.cdpClient.send("Target.closeTarget", { targetId }, null);
|
|
1356
|
+
return { value: { targetId, closedCurrent: targetId === this.page.targetId } };
|
|
1357
|
+
}
|
|
1311
1358
|
case "switchFrame": {
|
|
1312
1359
|
if (!step.selector) throw new Error("switchFrame requires selector");
|
|
1313
1360
|
await this.page.switchToFrame(step.selector, { timeout, optional });
|
|
@@ -1422,10 +1469,15 @@ var BatchExecutor = class {
|
|
|
1422
1469
|
snap: "snapshot",
|
|
1423
1470
|
accessibility: "snapshot",
|
|
1424
1471
|
a11y: "snapshot",
|
|
1472
|
+
formslist: "forms",
|
|
1425
1473
|
image: "screenshot",
|
|
1426
1474
|
pic: "screenshot",
|
|
1427
1475
|
frame: "switchFrame",
|
|
1428
1476
|
iframe: "switchFrame",
|
|
1477
|
+
newtab: "newTab",
|
|
1478
|
+
opentab: "newTab",
|
|
1479
|
+
createtab: "newTab",
|
|
1480
|
+
closetab: "closeTab",
|
|
1429
1481
|
assert_visible: "assertVisible",
|
|
1430
1482
|
assert_exists: "assertExists",
|
|
1431
1483
|
assert_text: "assertText",
|
|
@@ -1439,7 +1491,7 @@ var BatchExecutor = class {
|
|
|
1439
1491
|
};
|
|
1440
1492
|
const suggestion = aliases[action.toLowerCase()];
|
|
1441
1493
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
1442
|
-
const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
|
|
1494
|
+
const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, forms, screenshot, evaluate, text, newTab, closeTab, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
|
|
1443
1495
|
throw new Error(`Unknown action "${action}".${hint}
|
|
1444
1496
|
|
|
1445
1497
|
Valid actions: ${valid}`);
|
|
@@ -1510,6 +1562,11 @@ var ACTION_ALIASES = {
|
|
|
1510
1562
|
pic: "screenshot",
|
|
1511
1563
|
frame: "switchFrame",
|
|
1512
1564
|
iframe: "switchFrame",
|
|
1565
|
+
formslist: "forms",
|
|
1566
|
+
newtab: "newTab",
|
|
1567
|
+
opentab: "newTab",
|
|
1568
|
+
createtab: "newTab",
|
|
1569
|
+
closetab: "closeTab",
|
|
1513
1570
|
assert_visible: "assertVisible",
|
|
1514
1571
|
assert_exists: "assertExists",
|
|
1515
1572
|
assert_text: "assertText",
|
|
@@ -1546,7 +1603,8 @@ var PROPERTY_ALIASES = {
|
|
|
1546
1603
|
button: "key",
|
|
1547
1604
|
address: "url",
|
|
1548
1605
|
page: "url",
|
|
1549
|
-
path: "url"
|
|
1606
|
+
path: "url",
|
|
1607
|
+
tabId: "targetId"
|
|
1550
1608
|
};
|
|
1551
1609
|
var ACTION_RULES = {
|
|
1552
1610
|
goto: {
|
|
@@ -1647,6 +1705,10 @@ var ACTION_RULES = {
|
|
|
1647
1705
|
fullPage: { type: "boolean" }
|
|
1648
1706
|
}
|
|
1649
1707
|
},
|
|
1708
|
+
forms: {
|
|
1709
|
+
required: {},
|
|
1710
|
+
optional: {}
|
|
1711
|
+
},
|
|
1650
1712
|
evaluate: {
|
|
1651
1713
|
required: { value: { type: "string" } },
|
|
1652
1714
|
optional: {}
|
|
@@ -1661,6 +1723,18 @@ var ACTION_RULES = {
|
|
|
1661
1723
|
required: { selector: { type: "string|string[]" } },
|
|
1662
1724
|
optional: {}
|
|
1663
1725
|
},
|
|
1726
|
+
newTab: {
|
|
1727
|
+
required: {},
|
|
1728
|
+
optional: {
|
|
1729
|
+
url: { type: "string" }
|
|
1730
|
+
}
|
|
1731
|
+
},
|
|
1732
|
+
closeTab: {
|
|
1733
|
+
required: {},
|
|
1734
|
+
optional: {
|
|
1735
|
+
targetId: { type: "string" }
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1664
1738
|
switchToMain: {
|
|
1665
1739
|
required: {},
|
|
1666
1740
|
optional: {}
|
|
@@ -1703,6 +1777,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1703
1777
|
"selector",
|
|
1704
1778
|
"url",
|
|
1705
1779
|
"value",
|
|
1780
|
+
"targetId",
|
|
1706
1781
|
"key",
|
|
1707
1782
|
"combo",
|
|
1708
1783
|
"modifiers",
|
|
@@ -1857,15 +1932,22 @@ function validateSteps(steps) {
|
|
|
1857
1932
|
const rule = ACTION_RULES[action];
|
|
1858
1933
|
for (const key of Object.keys(obj)) {
|
|
1859
1934
|
if (key === "action") continue;
|
|
1860
|
-
if (
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1935
|
+
if (KNOWN_STEP_FIELDS.has(key)) continue;
|
|
1936
|
+
const canonical = PROPERTY_ALIASES[key];
|
|
1937
|
+
if (canonical) {
|
|
1938
|
+
if (!(canonical in obj)) {
|
|
1939
|
+
obj[canonical] = obj[key];
|
|
1940
|
+
}
|
|
1941
|
+
delete obj[key];
|
|
1942
|
+
continue;
|
|
1868
1943
|
}
|
|
1944
|
+
const suggestion = suggestProperty(key);
|
|
1945
|
+
errors.push({
|
|
1946
|
+
stepIndex: i,
|
|
1947
|
+
field: key,
|
|
1948
|
+
message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
|
|
1949
|
+
suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
|
|
1950
|
+
});
|
|
1869
1951
|
}
|
|
1870
1952
|
for (const [field, fieldRule] of Object.entries(rule.required)) {
|
|
1871
1953
|
if (!(field in obj) || obj[field] === void 0) {
|
|
@@ -3370,7 +3452,7 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
3370
3452
|
throw new Error("CDP client is not connected");
|
|
3371
3453
|
}
|
|
3372
3454
|
const id = ++messageId;
|
|
3373
|
-
const effectiveSessionId = sessionId ?? currentSessionId;
|
|
3455
|
+
const effectiveSessionId = sessionId === null ? void 0 : sessionId ?? currentSessionId;
|
|
3374
3456
|
const request = { id, method };
|
|
3375
3457
|
if (params !== void 0) {
|
|
3376
3458
|
request.params = params;
|
|
@@ -3849,6 +3931,285 @@ var RequestInterceptor = class {
|
|
|
3849
3931
|
}
|
|
3850
3932
|
};
|
|
3851
3933
|
|
|
3934
|
+
// src/browser/special-selectors.ts
|
|
3935
|
+
function stripQuotes(value) {
|
|
3936
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3937
|
+
return value.slice(1, -1);
|
|
3938
|
+
}
|
|
3939
|
+
return value;
|
|
3940
|
+
}
|
|
3941
|
+
function parseTextSelector(selector) {
|
|
3942
|
+
if (!selector.startsWith("text:")) return null;
|
|
3943
|
+
let raw = selector.slice(5).trim();
|
|
3944
|
+
let exact = false;
|
|
3945
|
+
if (raw.startsWith("=")) {
|
|
3946
|
+
exact = true;
|
|
3947
|
+
raw = raw.slice(1).trim();
|
|
3948
|
+
}
|
|
3949
|
+
const query = stripQuotes(raw);
|
|
3950
|
+
if (!query) return null;
|
|
3951
|
+
return { query, exact };
|
|
3952
|
+
}
|
|
3953
|
+
function parseRoleSelector(selector) {
|
|
3954
|
+
if (!selector.startsWith("role:")) return null;
|
|
3955
|
+
const body = selector.slice(5);
|
|
3956
|
+
const separator = body.indexOf(":");
|
|
3957
|
+
const role = (separator === -1 ? body : body.slice(0, separator)).trim().toLowerCase();
|
|
3958
|
+
const name = separator === -1 ? void 0 : stripQuotes(body.slice(separator + 1).trim());
|
|
3959
|
+
if (!role) return null;
|
|
3960
|
+
return { role, name: name || void 0 };
|
|
3961
|
+
}
|
|
3962
|
+
var SPECIAL_SELECTOR_SCRIPT = `
|
|
3963
|
+
function bpNormalizeSpace(value) {
|
|
3964
|
+
return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function bpCollectElements(root) {
|
|
3968
|
+
var elements = [];
|
|
3969
|
+
|
|
3970
|
+
function visit(node) {
|
|
3971
|
+
if (!node || typeof node.querySelectorAll !== 'function') return;
|
|
3972
|
+
var matches = node.querySelectorAll('*');
|
|
3973
|
+
for (var i = 0; i < matches.length; i++) {
|
|
3974
|
+
var el = matches[i];
|
|
3975
|
+
elements.push(el);
|
|
3976
|
+
if (el.shadowRoot) {
|
|
3977
|
+
visit(el.shadowRoot);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
if (root && root.documentElement) {
|
|
3983
|
+
elements.push(root.documentElement);
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
visit(root);
|
|
3987
|
+
return elements;
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
function bpIsVisible(el) {
|
|
3991
|
+
if (!el) return false;
|
|
3992
|
+
var style = getComputedStyle(el);
|
|
3993
|
+
if (style.display === 'none') return false;
|
|
3994
|
+
if (style.visibility === 'hidden') return false;
|
|
3995
|
+
if (parseFloat(style.opacity || '1') === 0) return false;
|
|
3996
|
+
var rect = el.getBoundingClientRect();
|
|
3997
|
+
return rect.width > 0 && rect.height > 0;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
function bpInferRole(el) {
|
|
4001
|
+
if (!el || !el.tagName) return '';
|
|
4002
|
+
|
|
4003
|
+
var explicitRole = bpNormalizeSpace(el.getAttribute && el.getAttribute('role'));
|
|
4004
|
+
if (explicitRole) return explicitRole.toLowerCase();
|
|
4005
|
+
|
|
4006
|
+
var tag = el.tagName.toLowerCase();
|
|
4007
|
+
if (tag === 'button') return 'button';
|
|
4008
|
+
if (tag === 'a' && el.hasAttribute('href')) return 'link';
|
|
4009
|
+
if (tag === 'textarea') return 'textbox';
|
|
4010
|
+
if (tag === 'select') return el.multiple ? 'listbox' : 'combobox';
|
|
4011
|
+
if (tag === 'option') return 'option';
|
|
4012
|
+
if (tag === 'summary') return 'button';
|
|
4013
|
+
|
|
4014
|
+
if (tag === 'input') {
|
|
4015
|
+
var type = (el.type || 'text').toLowerCase();
|
|
4016
|
+
if (type === 'checkbox') return 'checkbox';
|
|
4017
|
+
if (type === 'radio') return 'radio';
|
|
4018
|
+
if (type === 'search') return 'searchbox';
|
|
4019
|
+
if (type === 'number') return 'spinbutton';
|
|
4020
|
+
if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image') {
|
|
4021
|
+
return 'button';
|
|
4022
|
+
}
|
|
4023
|
+
return 'textbox';
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
return '';
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
function bpTextFromIdRefs(refs) {
|
|
4030
|
+
if (!refs) return '';
|
|
4031
|
+
var ids = refs.split(/\\s+/).filter(Boolean);
|
|
4032
|
+
var parts = [];
|
|
4033
|
+
for (var i = 0; i < ids.length; i++) {
|
|
4034
|
+
var node = document.getElementById(ids[i]);
|
|
4035
|
+
if (!node) continue;
|
|
4036
|
+
var text = bpNormalizeSpace(node.innerText || node.textContent || '');
|
|
4037
|
+
if (text) parts.push(text);
|
|
4038
|
+
}
|
|
4039
|
+
return bpNormalizeSpace(parts.join(' '));
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
function bpAccessibleName(el) {
|
|
4043
|
+
if (!el) return '';
|
|
4044
|
+
|
|
4045
|
+
var labelledBy = bpTextFromIdRefs(el.getAttribute && el.getAttribute('aria-labelledby'));
|
|
4046
|
+
if (labelledBy) return labelledBy;
|
|
4047
|
+
|
|
4048
|
+
var ariaLabel = bpNormalizeSpace(el.getAttribute && el.getAttribute('aria-label'));
|
|
4049
|
+
if (ariaLabel) return ariaLabel;
|
|
4050
|
+
|
|
4051
|
+
if (el.labels && el.labels.length) {
|
|
4052
|
+
var labels = [];
|
|
4053
|
+
for (var i = 0; i < el.labels.length; i++) {
|
|
4054
|
+
var labelText = bpNormalizeSpace(el.labels[i].innerText || el.labels[i].textContent || '');
|
|
4055
|
+
if (labelText) labels.push(labelText);
|
|
4056
|
+
}
|
|
4057
|
+
if (labels.length) return bpNormalizeSpace(labels.join(' '));
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
if (el.id) {
|
|
4061
|
+
var fallbackLabel = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
|
|
4062
|
+
if (fallbackLabel) {
|
|
4063
|
+
var fallbackText = bpNormalizeSpace(
|
|
4064
|
+
fallbackLabel.innerText || fallbackLabel.textContent || ''
|
|
4065
|
+
);
|
|
4066
|
+
if (fallbackText) return fallbackText;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
var type = (el.type || '').toLowerCase();
|
|
4071
|
+
if (
|
|
4072
|
+
el.tagName === 'INPUT' &&
|
|
4073
|
+
(type === 'submit' || type === 'button' || type === 'reset' || type === 'image')
|
|
4074
|
+
) {
|
|
4075
|
+
var inputValue = bpNormalizeSpace(el.value || el.getAttribute('value'));
|
|
4076
|
+
if (inputValue) return inputValue;
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
var alt = bpNormalizeSpace(el.getAttribute && el.getAttribute('alt'));
|
|
4080
|
+
if (alt) return alt;
|
|
4081
|
+
|
|
4082
|
+
var text = bpNormalizeSpace(el.innerText || el.textContent || '');
|
|
4083
|
+
if (text) return text;
|
|
4084
|
+
|
|
4085
|
+
var placeholder = bpNormalizeSpace(el.getAttribute && el.getAttribute('placeholder'));
|
|
4086
|
+
if (placeholder) return placeholder;
|
|
4087
|
+
|
|
4088
|
+
var title = bpNormalizeSpace(el.getAttribute && el.getAttribute('title'));
|
|
4089
|
+
if (title) return title;
|
|
4090
|
+
|
|
4091
|
+
var value = bpNormalizeSpace(el.value);
|
|
4092
|
+
if (value) return value;
|
|
4093
|
+
|
|
4094
|
+
return bpNormalizeSpace(el.name || el.id || '');
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
function bpIsInteractive(role, el) {
|
|
4098
|
+
if (
|
|
4099
|
+
role === 'button' ||
|
|
4100
|
+
role === 'link' ||
|
|
4101
|
+
role === 'textbox' ||
|
|
4102
|
+
role === 'checkbox' ||
|
|
4103
|
+
role === 'radio' ||
|
|
4104
|
+
role === 'combobox' ||
|
|
4105
|
+
role === 'listbox' ||
|
|
4106
|
+
role === 'option' ||
|
|
4107
|
+
role === 'searchbox' ||
|
|
4108
|
+
role === 'spinbutton' ||
|
|
4109
|
+
role === 'switch' ||
|
|
4110
|
+
role === 'tab'
|
|
4111
|
+
) {
|
|
4112
|
+
return true;
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
if (!el || !el.tagName) return false;
|
|
4116
|
+
var tag = el.tagName.toLowerCase();
|
|
4117
|
+
return tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select' || tag === 'textarea';
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
function bpFindByText(query, exact, includeHidden) {
|
|
4121
|
+
var needle = bpNormalizeSpace(query).toLowerCase();
|
|
4122
|
+
if (!needle) return null;
|
|
4123
|
+
|
|
4124
|
+
var best = null;
|
|
4125
|
+
var bestScore = -1;
|
|
4126
|
+
var elements = bpCollectElements(document);
|
|
4127
|
+
|
|
4128
|
+
for (var i = 0; i < elements.length; i++) {
|
|
4129
|
+
var el = elements[i];
|
|
4130
|
+
if (!includeHidden && !bpIsVisible(el)) continue;
|
|
4131
|
+
|
|
4132
|
+
var text = bpAccessibleName(el);
|
|
4133
|
+
if (!text) continue;
|
|
4134
|
+
|
|
4135
|
+
var haystack = text.toLowerCase();
|
|
4136
|
+
var matched = exact ? haystack === needle : haystack.includes(needle);
|
|
4137
|
+
if (!matched) continue;
|
|
4138
|
+
|
|
4139
|
+
var role = bpInferRole(el);
|
|
4140
|
+
var score = 0;
|
|
4141
|
+
if (bpIsInteractive(role, el)) score += 100;
|
|
4142
|
+
if (haystack === needle) score += 50;
|
|
4143
|
+
if (role === 'button' || role === 'link') score += 10;
|
|
4144
|
+
|
|
4145
|
+
if (score > bestScore) {
|
|
4146
|
+
best = el;
|
|
4147
|
+
bestScore = score;
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
return best;
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
function bpFindByRole(role, name, includeHidden) {
|
|
4155
|
+
var targetRole = bpNormalizeSpace(role).toLowerCase();
|
|
4156
|
+
if (!targetRole) return null;
|
|
4157
|
+
|
|
4158
|
+
var nameNeedle = bpNormalizeSpace(name).toLowerCase();
|
|
4159
|
+
var best = null;
|
|
4160
|
+
var bestScore = -1;
|
|
4161
|
+
var elements = bpCollectElements(document);
|
|
4162
|
+
|
|
4163
|
+
for (var i = 0; i < elements.length; i++) {
|
|
4164
|
+
var el = elements[i];
|
|
4165
|
+
if (!includeHidden && !bpIsVisible(el)) continue;
|
|
4166
|
+
|
|
4167
|
+
var actualRole = bpInferRole(el);
|
|
4168
|
+
if (actualRole !== targetRole) continue;
|
|
4169
|
+
|
|
4170
|
+
var accessibleName = bpAccessibleName(el);
|
|
4171
|
+
if (nameNeedle) {
|
|
4172
|
+
var loweredName = accessibleName.toLowerCase();
|
|
4173
|
+
if (!loweredName.includes(nameNeedle)) continue;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
var score = 0;
|
|
4177
|
+
if (accessibleName) score += 10;
|
|
4178
|
+
if (nameNeedle && accessibleName.toLowerCase() === nameNeedle) score += 20;
|
|
4179
|
+
|
|
4180
|
+
if (score > bestScore) {
|
|
4181
|
+
best = el;
|
|
4182
|
+
bestScore = score;
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
return best;
|
|
4187
|
+
}
|
|
4188
|
+
`;
|
|
4189
|
+
function buildSpecialSelectorLookupExpression(selector, options = {}) {
|
|
4190
|
+
const includeHidden = options.includeHidden === true;
|
|
4191
|
+
const text = parseTextSelector(selector);
|
|
4192
|
+
if (text) {
|
|
4193
|
+
return `(() => {
|
|
4194
|
+
${SPECIAL_SELECTOR_SCRIPT}
|
|
4195
|
+
return bpFindByText(${JSON.stringify(text.query)}, ${text.exact}, ${includeHidden});
|
|
4196
|
+
})()`;
|
|
4197
|
+
}
|
|
4198
|
+
const role = parseRoleSelector(selector);
|
|
4199
|
+
if (role) {
|
|
4200
|
+
return `(() => {
|
|
4201
|
+
${SPECIAL_SELECTOR_SCRIPT}
|
|
4202
|
+
return bpFindByRole(${JSON.stringify(role.role)}, ${JSON.stringify(role.name ?? "")}, ${includeHidden});
|
|
4203
|
+
})()`;
|
|
4204
|
+
}
|
|
4205
|
+
return null;
|
|
4206
|
+
}
|
|
4207
|
+
function buildSpecialSelectorPredicateExpression(selector, options = {}) {
|
|
4208
|
+
const lookup = buildSpecialSelectorLookupExpression(selector, options);
|
|
4209
|
+
if (!lookup) return null;
|
|
4210
|
+
return `(() => !!(${lookup}))()`;
|
|
4211
|
+
}
|
|
4212
|
+
|
|
3852
4213
|
// src/wait/strategies.ts
|
|
3853
4214
|
var DEEP_QUERY_SCRIPT = `
|
|
3854
4215
|
function deepQuery(selector, root = document) {
|
|
@@ -3882,18 +4243,19 @@ function deepQuery(selector, root = document) {
|
|
|
3882
4243
|
}
|
|
3883
4244
|
`;
|
|
3884
4245
|
async function isElementVisible(cdp, selector, contextId) {
|
|
4246
|
+
const specialExpression = buildSpecialSelectorPredicateExpression(selector);
|
|
3885
4247
|
const params = {
|
|
3886
|
-
expression: `(() => {
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
4248
|
+
expression: specialExpression ?? `(() => {
|
|
4249
|
+
${DEEP_QUERY_SCRIPT}
|
|
4250
|
+
const el = deepQuery(${JSON.stringify(selector)});
|
|
4251
|
+
if (!el) return false;
|
|
4252
|
+
const style = getComputedStyle(el);
|
|
4253
|
+
if (style.display === 'none') return false;
|
|
4254
|
+
if (style.visibility === 'hidden') return false;
|
|
4255
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
4256
|
+
const rect = el.getBoundingClientRect();
|
|
4257
|
+
return rect.width > 0 && rect.height > 0;
|
|
4258
|
+
})()`,
|
|
3897
4259
|
returnByValue: true
|
|
3898
4260
|
};
|
|
3899
4261
|
if (contextId !== void 0) {
|
|
@@ -3903,11 +4265,14 @@ async function isElementVisible(cdp, selector, contextId) {
|
|
|
3903
4265
|
return result.result.value === true;
|
|
3904
4266
|
}
|
|
3905
4267
|
async function isElementAttached(cdp, selector, contextId) {
|
|
4268
|
+
const specialExpression = buildSpecialSelectorPredicateExpression(selector, {
|
|
4269
|
+
includeHidden: true
|
|
4270
|
+
});
|
|
3906
4271
|
const params = {
|
|
3907
|
-
expression: `(() => {
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4272
|
+
expression: specialExpression ?? `(() => {
|
|
4273
|
+
${DEEP_QUERY_SCRIPT}
|
|
4274
|
+
return deepQuery(${JSON.stringify(selector)}) !== null;
|
|
4275
|
+
})()`,
|
|
3911
4276
|
returnByValue: true
|
|
3912
4277
|
};
|
|
3913
4278
|
if (contextId !== void 0) {
|
|
@@ -4527,6 +4892,9 @@ var Page = class {
|
|
|
4527
4892
|
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4528
4893
|
});
|
|
4529
4894
|
} catch (e) {
|
|
4895
|
+
if (e instanceof ActionabilityError && e.failureType === "hitTarget" && await this.tryClickAssociatedLabel(objectId)) {
|
|
4896
|
+
return true;
|
|
4897
|
+
}
|
|
4530
4898
|
if (options.optional) return false;
|
|
4531
4899
|
throw e;
|
|
4532
4900
|
}
|
|
@@ -4915,7 +5283,12 @@ var Page = class {
|
|
|
4915
5283
|
returnByValue: true
|
|
4916
5284
|
});
|
|
4917
5285
|
if (!after.result.value) {
|
|
4918
|
-
|
|
5286
|
+
if (await this.tryToggleViaLabel(object.objectId, true)) {
|
|
5287
|
+
return true;
|
|
5288
|
+
}
|
|
5289
|
+
throw new Error(
|
|
5290
|
+
"Clicking the checkbox did not change its state. Tried the associated label too."
|
|
5291
|
+
);
|
|
4919
5292
|
}
|
|
4920
5293
|
return true;
|
|
4921
5294
|
});
|
|
@@ -4967,7 +5340,12 @@ var Page = class {
|
|
|
4967
5340
|
returnByValue: true
|
|
4968
5341
|
});
|
|
4969
5342
|
if (after.result.value) {
|
|
4970
|
-
|
|
5343
|
+
if (await this.tryToggleViaLabel(object.objectId, false)) {
|
|
5344
|
+
return true;
|
|
5345
|
+
}
|
|
5346
|
+
throw new Error(
|
|
5347
|
+
"Clicking the checkbox did not change its state. Tried the associated label too."
|
|
5348
|
+
);
|
|
4971
5349
|
}
|
|
4972
5350
|
return true;
|
|
4973
5351
|
});
|
|
@@ -5291,7 +5669,7 @@ var Page = class {
|
|
|
5291
5669
|
}
|
|
5292
5670
|
const result = await this.cdp.send("Runtime.evaluate", params);
|
|
5293
5671
|
if (result.exceptionDetails) {
|
|
5294
|
-
throw new Error(
|
|
5672
|
+
throw new Error(this.formatEvaluationError(result.exceptionDetails));
|
|
5295
5673
|
}
|
|
5296
5674
|
return result.result.value;
|
|
5297
5675
|
}
|
|
@@ -5343,6 +5721,75 @@ var Page = class {
|
|
|
5343
5721
|
return result.result.value ?? "";
|
|
5344
5722
|
});
|
|
5345
5723
|
}
|
|
5724
|
+
/**
|
|
5725
|
+
* Enumerate form controls on the page with labels and current state.
|
|
5726
|
+
*/
|
|
5727
|
+
async forms() {
|
|
5728
|
+
const result = await this.evaluateInFrame(
|
|
5729
|
+
`(() => {
|
|
5730
|
+
function normalize(value) {
|
|
5731
|
+
return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5734
|
+
function labelFor(el) {
|
|
5735
|
+
if (!el) return '';
|
|
5736
|
+
if (el.labels && el.labels.length) {
|
|
5737
|
+
return normalize(
|
|
5738
|
+
Array.from(el.labels)
|
|
5739
|
+
.map((label) => label.innerText || label.textContent || '')
|
|
5740
|
+
.join(' ')
|
|
5741
|
+
);
|
|
5742
|
+
}
|
|
5743
|
+
var ariaLabel = normalize(el.getAttribute && el.getAttribute('aria-label'));
|
|
5744
|
+
if (ariaLabel) return ariaLabel;
|
|
5745
|
+
if (el.id) {
|
|
5746
|
+
var byFor = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
|
|
5747
|
+
if (byFor) return normalize(byFor.innerText || byFor.textContent || '');
|
|
5748
|
+
}
|
|
5749
|
+
var closest = el.closest && el.closest('label');
|
|
5750
|
+
if (closest) return normalize(closest.innerText || closest.textContent || '');
|
|
5751
|
+
return '';
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5754
|
+
return Array.from(document.querySelectorAll('input, select, textarea')).map((el) => {
|
|
5755
|
+
var tag = el.tagName.toLowerCase();
|
|
5756
|
+
var type = tag === 'input' ? (el.type || 'text').toLowerCase() : tag;
|
|
5757
|
+
var value = null;
|
|
5758
|
+
|
|
5759
|
+
if (tag === 'select') {
|
|
5760
|
+
value = el.multiple
|
|
5761
|
+
? Array.from(el.selectedOptions).map((opt) => opt.value)
|
|
5762
|
+
: el.value || null;
|
|
5763
|
+
} else if (tag === 'textarea' || tag === 'input') {
|
|
5764
|
+
value = typeof el.value === 'string' ? el.value : null;
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
return {
|
|
5768
|
+
tag: tag,
|
|
5769
|
+
type: type,
|
|
5770
|
+
id: el.id || undefined,
|
|
5771
|
+
name: el.getAttribute('name') || undefined,
|
|
5772
|
+
value: value,
|
|
5773
|
+
checked: 'checked' in el ? !!el.checked : undefined,
|
|
5774
|
+
required: !!el.required,
|
|
5775
|
+
disabled: !!el.disabled,
|
|
5776
|
+
label: labelFor(el) || undefined,
|
|
5777
|
+
placeholder: normalize(el.getAttribute && el.getAttribute('placeholder')) || undefined,
|
|
5778
|
+
options:
|
|
5779
|
+
tag === 'select'
|
|
5780
|
+
? Array.from(el.options).map((opt) => ({
|
|
5781
|
+
value: opt.value || '',
|
|
5782
|
+
text: normalize(opt.text || opt.label || ''),
|
|
5783
|
+
selected: !!opt.selected,
|
|
5784
|
+
disabled: !!opt.disabled,
|
|
5785
|
+
}))
|
|
5786
|
+
: undefined,
|
|
5787
|
+
};
|
|
5788
|
+
});
|
|
5789
|
+
})()`
|
|
5790
|
+
);
|
|
5791
|
+
return result.result.value ?? [];
|
|
5792
|
+
}
|
|
5346
5793
|
// ============ File Handling ============
|
|
5347
5794
|
/**
|
|
5348
5795
|
* Set files on a file input
|
|
@@ -5815,7 +6262,8 @@ var Page = class {
|
|
|
5815
6262
|
/**
|
|
5816
6263
|
* Get an accessibility tree snapshot of the page
|
|
5817
6264
|
*/
|
|
5818
|
-
async snapshot() {
|
|
6265
|
+
async snapshot(options = {}) {
|
|
6266
|
+
const roleFilter = new Set((options.roles ?? []).map((role) => role.trim().toLowerCase()));
|
|
5819
6267
|
const [url, title, axTree] = await Promise.all([
|
|
5820
6268
|
this.url(),
|
|
5821
6269
|
this.title(),
|
|
@@ -5836,7 +6284,7 @@ var Page = class {
|
|
|
5836
6284
|
const buildNode = (nodeId) => {
|
|
5837
6285
|
const node = nodeMap.get(nodeId);
|
|
5838
6286
|
if (!node) return null;
|
|
5839
|
-
const role = node.role?.value ?? "generic";
|
|
6287
|
+
const role = (node.role?.value ?? "generic").toLowerCase();
|
|
5840
6288
|
const name = node.name?.value;
|
|
5841
6289
|
const value = node.value?.value;
|
|
5842
6290
|
const ref = nodeRefs.get(nodeId);
|
|
@@ -5852,7 +6300,7 @@ var Page = class {
|
|
|
5852
6300
|
return {
|
|
5853
6301
|
role,
|
|
5854
6302
|
name,
|
|
5855
|
-
value,
|
|
6303
|
+
value: value !== void 0 ? String(value) : void 0,
|
|
5856
6304
|
ref,
|
|
5857
6305
|
children: children.length > 0 ? children : void 0,
|
|
5858
6306
|
disabled,
|
|
@@ -5860,7 +6308,24 @@ var Page = class {
|
|
|
5860
6308
|
};
|
|
5861
6309
|
};
|
|
5862
6310
|
const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
|
|
5863
|
-
|
|
6311
|
+
let accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
|
|
6312
|
+
if (roleFilter.size > 0) {
|
|
6313
|
+
const filteredAccessibilityTree = [];
|
|
6314
|
+
for (const node of nodes) {
|
|
6315
|
+
if (!roleFilter.has((node.role?.value ?? "generic").toLowerCase())) {
|
|
6316
|
+
continue;
|
|
6317
|
+
}
|
|
6318
|
+
const snapshotNode = buildNode(node.nodeId);
|
|
6319
|
+
if (!snapshotNode) {
|
|
6320
|
+
continue;
|
|
6321
|
+
}
|
|
6322
|
+
filteredAccessibilityTree.push({
|
|
6323
|
+
...snapshotNode,
|
|
6324
|
+
children: void 0
|
|
6325
|
+
});
|
|
6326
|
+
}
|
|
6327
|
+
accessibilityTree = filteredAccessibilityTree;
|
|
6328
|
+
}
|
|
5864
6329
|
const interactiveRoles = /* @__PURE__ */ new Set([
|
|
5865
6330
|
"button",
|
|
5866
6331
|
"link",
|
|
@@ -5882,37 +6347,44 @@ var Page = class {
|
|
|
5882
6347
|
]);
|
|
5883
6348
|
const interactiveElements = [];
|
|
5884
6349
|
for (const node of nodes) {
|
|
5885
|
-
const role = node.role?.value;
|
|
5886
|
-
if (role && interactiveRoles.has(role)) {
|
|
6350
|
+
const role = (node.role?.value ?? "").toLowerCase();
|
|
6351
|
+
if (role && interactiveRoles.has(role) && (roleFilter.size === 0 || roleFilter.has(role))) {
|
|
5887
6352
|
const ref = nodeRefs.get(node.nodeId);
|
|
5888
6353
|
const name = node.name?.value ?? "";
|
|
5889
6354
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
6355
|
+
const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
|
|
6356
|
+
const value = node.value?.value;
|
|
5890
6357
|
const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
|
|
5891
6358
|
interactiveElements.push({
|
|
5892
6359
|
ref,
|
|
5893
6360
|
role,
|
|
5894
6361
|
name,
|
|
5895
6362
|
selector,
|
|
5896
|
-
disabled
|
|
6363
|
+
disabled,
|
|
6364
|
+
checked,
|
|
6365
|
+
value: value !== void 0 ? String(value) : void 0
|
|
5897
6366
|
});
|
|
5898
6367
|
}
|
|
5899
6368
|
}
|
|
6369
|
+
const formatNode = (node, depth = 0) => {
|
|
6370
|
+
let line = `${" ".repeat(depth)}- ${node.role}`;
|
|
6371
|
+
if (node.name) line += ` "${node.name}"`;
|
|
6372
|
+
line += ` ref:${node.ref}`;
|
|
6373
|
+
if (node.disabled) line += " (disabled)";
|
|
6374
|
+
if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
|
|
6375
|
+
return line;
|
|
6376
|
+
};
|
|
5900
6377
|
const formatTree = (nodes2, depth = 0) => {
|
|
5901
6378
|
const lines = [];
|
|
5902
6379
|
for (const node of nodes2) {
|
|
5903
|
-
|
|
5904
|
-
if (node.name) line += ` "${node.name}"`;
|
|
5905
|
-
line += ` [ref=${node.ref}]`;
|
|
5906
|
-
if (node.disabled) line += " (disabled)";
|
|
5907
|
-
if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
|
|
5908
|
-
lines.push(line);
|
|
6380
|
+
lines.push(formatNode(node, depth));
|
|
5909
6381
|
if (node.children) {
|
|
5910
6382
|
lines.push(formatTree(node.children, depth + 1));
|
|
5911
6383
|
}
|
|
5912
6384
|
}
|
|
5913
6385
|
return lines.join("\n");
|
|
5914
6386
|
};
|
|
5915
|
-
const text = formatTree(accessibilityTree);
|
|
6387
|
+
const text = roleFilter.size > 0 ? accessibilityTree.map((node) => formatNode(node)).join("\n") : formatTree(accessibilityTree);
|
|
5916
6388
|
const result = {
|
|
5917
6389
|
url,
|
|
5918
6390
|
title,
|
|
@@ -5921,7 +6393,9 @@ var Page = class {
|
|
|
5921
6393
|
interactiveElements,
|
|
5922
6394
|
text
|
|
5923
6395
|
};
|
|
5924
|
-
|
|
6396
|
+
if (roleFilter.size === 0) {
|
|
6397
|
+
this.lastSnapshot = result;
|
|
6398
|
+
}
|
|
5925
6399
|
return result;
|
|
5926
6400
|
}
|
|
5927
6401
|
/**
|
|
@@ -6476,7 +6950,7 @@ var Page = class {
|
|
|
6476
6950
|
}
|
|
6477
6951
|
/**
|
|
6478
6952
|
* Find an element using single or multiple selectors
|
|
6479
|
-
* Supports ref
|
|
6953
|
+
* Supports ref:, text:, and role: selectors.
|
|
6480
6954
|
*/
|
|
6481
6955
|
async findElement(selectors, options = {}) {
|
|
6482
6956
|
const { timeout = DEFAULT_TIMEOUT2 } = options;
|
|
@@ -6543,11 +7017,11 @@ var Page = class {
|
|
|
6543
7017
|
}
|
|
6544
7018
|
}
|
|
6545
7019
|
}
|
|
6546
|
-
const
|
|
6547
|
-
if (
|
|
7020
|
+
const runtimeSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
|
|
7021
|
+
if (runtimeSelectors.length === 0) {
|
|
6548
7022
|
return null;
|
|
6549
7023
|
}
|
|
6550
|
-
const result = await waitForAnyElement(this.cdp,
|
|
7024
|
+
const result = await waitForAnyElement(this.cdp, runtimeSelectors, {
|
|
6551
7025
|
state: "visible",
|
|
6552
7026
|
timeout,
|
|
6553
7027
|
contextId: this.currentFrameContextId ?? void 0
|
|
@@ -6555,6 +7029,14 @@ var Page = class {
|
|
|
6555
7029
|
if (!result.success || !result.selector) {
|
|
6556
7030
|
return null;
|
|
6557
7031
|
}
|
|
7032
|
+
const specialSelectorMatch = await this.resolveSpecialSelector(result.selector);
|
|
7033
|
+
if (specialSelectorMatch) {
|
|
7034
|
+
this._lastMatchedSelector = result.selector;
|
|
7035
|
+
return {
|
|
7036
|
+
...specialSelectorMatch,
|
|
7037
|
+
waitedMs: result.waitedMs
|
|
7038
|
+
};
|
|
7039
|
+
}
|
|
6558
7040
|
await this.ensureRootNode();
|
|
6559
7041
|
const queryResult = await this.cdp.send("DOM.querySelector", {
|
|
6560
7042
|
nodeId: this.rootNodeId,
|
|
@@ -6601,6 +7083,122 @@ var Page = class {
|
|
|
6601
7083
|
waitedMs: result.waitedMs
|
|
6602
7084
|
};
|
|
6603
7085
|
}
|
|
7086
|
+
formatEvaluationError(details) {
|
|
7087
|
+
const description = typeof details.exception?.description === "string" && details.exception.description || typeof details.exception?.value === "string" && details.exception.value || details.text || "Uncaught";
|
|
7088
|
+
return `Evaluation failed: ${description}`;
|
|
7089
|
+
}
|
|
7090
|
+
async resolveSpecialSelector(selector, options = {}) {
|
|
7091
|
+
const expression = buildSpecialSelectorLookupExpression(selector, options);
|
|
7092
|
+
if (!expression) return null;
|
|
7093
|
+
const result = await this.evaluateInFrame(expression, {
|
|
7094
|
+
returnByValue: false
|
|
7095
|
+
});
|
|
7096
|
+
if (!result.result.objectId) {
|
|
7097
|
+
return null;
|
|
7098
|
+
}
|
|
7099
|
+
const resolved = await this.objectIdToNode(result.result.objectId);
|
|
7100
|
+
if (!resolved) {
|
|
7101
|
+
return null;
|
|
7102
|
+
}
|
|
7103
|
+
return {
|
|
7104
|
+
nodeId: resolved.nodeId,
|
|
7105
|
+
backendNodeId: resolved.backendNodeId,
|
|
7106
|
+
selector,
|
|
7107
|
+
waitedMs: 0
|
|
7108
|
+
};
|
|
7109
|
+
}
|
|
7110
|
+
async readCheckedState(objectId) {
|
|
7111
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
7112
|
+
objectId,
|
|
7113
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
7114
|
+
returnByValue: true
|
|
7115
|
+
});
|
|
7116
|
+
return result.result.value === true;
|
|
7117
|
+
}
|
|
7118
|
+
async readInputType(objectId) {
|
|
7119
|
+
const result = await this.cdp.send(
|
|
7120
|
+
"Runtime.callFunctionOn",
|
|
7121
|
+
{
|
|
7122
|
+
objectId,
|
|
7123
|
+
functionDeclaration: 'function() { return this instanceof HTMLInputElement ? String(this.type || "").toLowerCase() : null; }',
|
|
7124
|
+
returnByValue: true
|
|
7125
|
+
}
|
|
7126
|
+
);
|
|
7127
|
+
return result.result.value ?? null;
|
|
7128
|
+
}
|
|
7129
|
+
async getAssociatedLabelNodeId(objectId) {
|
|
7130
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
7131
|
+
objectId,
|
|
7132
|
+
functionDeclaration: `function() {
|
|
7133
|
+
if (!(this instanceof HTMLInputElement)) return null;
|
|
7134
|
+
|
|
7135
|
+
if (this.id) {
|
|
7136
|
+
var labels = Array.from(document.querySelectorAll('label'));
|
|
7137
|
+
for (var i = 0; i < labels.length; i++) {
|
|
7138
|
+
if (labels[i].htmlFor === this.id) return labels[i];
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
|
|
7142
|
+
return this.closest('label');
|
|
7143
|
+
}`,
|
|
7144
|
+
returnByValue: false
|
|
7145
|
+
});
|
|
7146
|
+
if (!result.result.objectId) {
|
|
7147
|
+
return null;
|
|
7148
|
+
}
|
|
7149
|
+
return (await this.objectIdToNode(result.result.objectId))?.nodeId ?? null;
|
|
7150
|
+
}
|
|
7151
|
+
async objectIdToNode(objectId) {
|
|
7152
|
+
const describeResult = await this.cdp.send("DOM.describeNode", {
|
|
7153
|
+
objectId,
|
|
7154
|
+
depth: 0
|
|
7155
|
+
});
|
|
7156
|
+
const backendNodeId = describeResult.node.backendNodeId;
|
|
7157
|
+
if (!backendNodeId) {
|
|
7158
|
+
return null;
|
|
7159
|
+
}
|
|
7160
|
+
if (describeResult.node.nodeId) {
|
|
7161
|
+
return {
|
|
7162
|
+
nodeId: describeResult.node.nodeId,
|
|
7163
|
+
backendNodeId
|
|
7164
|
+
};
|
|
7165
|
+
}
|
|
7166
|
+
await this.ensureRootNode();
|
|
7167
|
+
const pushResult = await this.cdp.send(
|
|
7168
|
+
"DOM.pushNodesByBackendIdsToFrontend",
|
|
7169
|
+
{
|
|
7170
|
+
backendNodeIds: [backendNodeId]
|
|
7171
|
+
}
|
|
7172
|
+
);
|
|
7173
|
+
const nodeId = pushResult.nodeIds?.[0];
|
|
7174
|
+
if (!nodeId) {
|
|
7175
|
+
return null;
|
|
7176
|
+
}
|
|
7177
|
+
return { nodeId, backendNodeId };
|
|
7178
|
+
}
|
|
7179
|
+
async tryClickAssociatedLabel(objectId) {
|
|
7180
|
+
const inputType = await this.readInputType(objectId);
|
|
7181
|
+
if (inputType !== "checkbox" && inputType !== "radio") {
|
|
7182
|
+
return false;
|
|
7183
|
+
}
|
|
7184
|
+
const labelNodeId = await this.getAssociatedLabelNodeId(objectId);
|
|
7185
|
+
if (!labelNodeId) {
|
|
7186
|
+
return false;
|
|
7187
|
+
}
|
|
7188
|
+
try {
|
|
7189
|
+
await this.scrollIntoView(labelNodeId);
|
|
7190
|
+
await this.clickElement(labelNodeId);
|
|
7191
|
+
return true;
|
|
7192
|
+
} catch {
|
|
7193
|
+
return false;
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
async tryToggleViaLabel(objectId, desiredChecked) {
|
|
7197
|
+
if (!await this.tryClickAssociatedLabel(objectId)) {
|
|
7198
|
+
return false;
|
|
7199
|
+
}
|
|
7200
|
+
return await this.readCheckedState(objectId) === desiredChecked;
|
|
7201
|
+
}
|
|
6604
7202
|
/**
|
|
6605
7203
|
* Ensure we have a valid root node ID
|
|
6606
7204
|
*/
|
|
@@ -7018,6 +7616,7 @@ var Browser = class _Browser {
|
|
|
7018
7616
|
cdp;
|
|
7019
7617
|
providerSession;
|
|
7020
7618
|
pages = /* @__PURE__ */ new Map();
|
|
7619
|
+
pageCounter = 0;
|
|
7021
7620
|
constructor(cdp, _provider, providerSession, _options) {
|
|
7022
7621
|
this.cdp = cdp;
|
|
7023
7622
|
this.providerSession = providerSession;
|
|
@@ -7047,7 +7646,11 @@ var Browser = class _Browser {
|
|
|
7047
7646
|
const pageName = name ?? "default";
|
|
7048
7647
|
const cached = this.pages.get(pageName);
|
|
7049
7648
|
if (cached) return cached;
|
|
7050
|
-
const targets = await this.cdp.send(
|
|
7649
|
+
const targets = await this.cdp.send(
|
|
7650
|
+
"Target.getTargets",
|
|
7651
|
+
void 0,
|
|
7652
|
+
null
|
|
7653
|
+
);
|
|
7051
7654
|
let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
7052
7655
|
if (options?.targetUrl) {
|
|
7053
7656
|
const urlFilter = options.targetUrl;
|
|
@@ -7069,16 +7672,24 @@ var Browser = class _Browser {
|
|
|
7069
7672
|
targetId = options.targetId;
|
|
7070
7673
|
} else {
|
|
7071
7674
|
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
7072
|
-
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
|
|
7073
|
-
|
|
7074
|
-
|
|
7675
|
+
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
|
|
7676
|
+
"Target.createTarget",
|
|
7677
|
+
{
|
|
7678
|
+
url: "about:blank"
|
|
7679
|
+
},
|
|
7680
|
+
null
|
|
7681
|
+
)).targetId;
|
|
7075
7682
|
}
|
|
7076
7683
|
} else if (pageTargets.length > 0) {
|
|
7077
7684
|
targetId = pickBestTarget(pageTargets);
|
|
7078
7685
|
} else {
|
|
7079
|
-
const result = await this.cdp.send(
|
|
7080
|
-
|
|
7081
|
-
|
|
7686
|
+
const result = await this.cdp.send(
|
|
7687
|
+
"Target.createTarget",
|
|
7688
|
+
{
|
|
7689
|
+
url: "about:blank"
|
|
7690
|
+
},
|
|
7691
|
+
null
|
|
7692
|
+
);
|
|
7082
7693
|
targetId = result.targetId;
|
|
7083
7694
|
}
|
|
7084
7695
|
await this.cdp.attachToTarget(targetId);
|
|
@@ -7106,13 +7717,17 @@ var Browser = class _Browser {
|
|
|
7106
7717
|
* Create a new page (tab)
|
|
7107
7718
|
*/
|
|
7108
7719
|
async newPage(url = "about:blank") {
|
|
7109
|
-
const result = await this.cdp.send(
|
|
7110
|
-
|
|
7111
|
-
|
|
7720
|
+
const result = await this.cdp.send(
|
|
7721
|
+
"Target.createTarget",
|
|
7722
|
+
{
|
|
7723
|
+
url
|
|
7724
|
+
},
|
|
7725
|
+
null
|
|
7726
|
+
);
|
|
7112
7727
|
await this.cdp.attachToTarget(result.targetId);
|
|
7113
7728
|
const page = new Page(this.cdp, result.targetId);
|
|
7114
7729
|
await page.init();
|
|
7115
|
-
const name = `page-${this.
|
|
7730
|
+
const name = `page-${++this.pageCounter}`;
|
|
7116
7731
|
this.pages.set(name, page);
|
|
7117
7732
|
return page;
|
|
7118
7733
|
}
|
|
@@ -7122,14 +7737,30 @@ var Browser = class _Browser {
|
|
|
7122
7737
|
async closePage(name) {
|
|
7123
7738
|
const page = this.pages.get(name);
|
|
7124
7739
|
if (!page) return;
|
|
7125
|
-
const
|
|
7126
|
-
|
|
7127
|
-
if (pageTargets.length > 0) {
|
|
7128
|
-
await this.cdp.send("Target.closeTarget", {
|
|
7129
|
-
targetId: pageTargets[0].targetId
|
|
7130
|
-
});
|
|
7131
|
-
}
|
|
7740
|
+
const targetId = page.targetId;
|
|
7741
|
+
await this.cdp.send("Target.closeTarget", { targetId }, null);
|
|
7132
7742
|
this.pages.delete(name);
|
|
7743
|
+
const deadline = Date.now() + 5e3;
|
|
7744
|
+
while (Date.now() < deadline) {
|
|
7745
|
+
const { targetInfos } = await this.cdp.send(
|
|
7746
|
+
"Target.getTargets",
|
|
7747
|
+
void 0,
|
|
7748
|
+
null
|
|
7749
|
+
);
|
|
7750
|
+
if (!targetInfos.some((t) => t.targetId === targetId)) return;
|
|
7751
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
7752
|
+
}
|
|
7753
|
+
}
|
|
7754
|
+
/**
|
|
7755
|
+
* List all page targets in the connected browser.
|
|
7756
|
+
*/
|
|
7757
|
+
async listTargets() {
|
|
7758
|
+
const { targetInfos } = await this.cdp.send(
|
|
7759
|
+
"Target.getTargets",
|
|
7760
|
+
void 0,
|
|
7761
|
+
null
|
|
7762
|
+
);
|
|
7763
|
+
return targetInfos.filter((target) => target.type === "page");
|
|
7133
7764
|
}
|
|
7134
7765
|
/**
|
|
7135
7766
|
* Get the WebSocket URL for this browser connection
|
|
@@ -8039,9 +8670,12 @@ Usage:
|
|
|
8039
8670
|
|
|
8040
8671
|
Options:
|
|
8041
8672
|
-p, --provider <type> Provider: generic | browserbase | browserless (default: generic)
|
|
8042
|
-
--url <
|
|
8673
|
+
--url <value> Browser WebSocket URL, or page URL when used with --new-tab
|
|
8674
|
+
--browser-url <ws-url> Explicit browser WebSocket URL
|
|
8675
|
+
--page-url <url> URL to open in the attached page/new tab
|
|
8043
8676
|
-n, --name <id> Custom session name (default: auto-generated)
|
|
8044
8677
|
-r, --resume <id> Resume an existing session by ID
|
|
8678
|
+
--new-tab Create and attach to a fresh tab instead of reusing an existing one
|
|
8045
8679
|
--target-url <str> Filter targets to those whose URL contains this string
|
|
8046
8680
|
--api-key <key> API key for cloud providers
|
|
8047
8681
|
--project-id <id> Project ID for BrowserBase provider
|
|
@@ -8056,6 +8690,7 @@ Examples:
|
|
|
8056
8690
|
bp connect --url ws://localhost:9222/devtools # Explicit WebSocket URL
|
|
8057
8691
|
bp connect --resume dev # Resume a previous session
|
|
8058
8692
|
bp connect --target-url localhost:3000 # Attach to tab matching URL
|
|
8693
|
+
bp connect --new-tab --url https://example.com # Create and attach to a fresh tab
|
|
8059
8694
|
`.trimEnd();
|
|
8060
8695
|
function parseConnectArgs(args) {
|
|
8061
8696
|
const options = {};
|
|
@@ -8065,10 +8700,16 @@ function parseConnectArgs(args) {
|
|
|
8065
8700
|
options.provider = args[++i];
|
|
8066
8701
|
} else if (arg === "--url") {
|
|
8067
8702
|
options.url = args[++i];
|
|
8703
|
+
} else if (arg === "--browser-url") {
|
|
8704
|
+
options.browserUrl = args[++i];
|
|
8705
|
+
} else if (arg === "--page-url") {
|
|
8706
|
+
options.pageUrl = args[++i];
|
|
8068
8707
|
} else if (arg === "--name" || arg === "-n") {
|
|
8069
8708
|
options.name = args[++i];
|
|
8070
8709
|
} else if (arg === "--resume" || arg === "-r") {
|
|
8071
8710
|
options.resume = args[++i];
|
|
8711
|
+
} else if (arg === "--new-tab") {
|
|
8712
|
+
options.newTab = true;
|
|
8072
8713
|
} else if (arg === "--target-url") {
|
|
8073
8714
|
options.targetUrl = args[++i];
|
|
8074
8715
|
} else if (arg === "--api-key") {
|
|
@@ -8103,7 +8744,14 @@ async function connectCommand(args, globalOptions) {
|
|
|
8103
8744
|
return;
|
|
8104
8745
|
}
|
|
8105
8746
|
const provider = options.provider ?? "generic";
|
|
8106
|
-
let wsUrl = options.url;
|
|
8747
|
+
let wsUrl = options.browserUrl ?? options.url;
|
|
8748
|
+
let pageUrl = options.pageUrl;
|
|
8749
|
+
if (options.newTab && options.url && !options.url.startsWith("ws://") && !options.url.startsWith("wss://")) {
|
|
8750
|
+
pageUrl = options.url;
|
|
8751
|
+
if (!options.browserUrl) {
|
|
8752
|
+
wsUrl = void 0;
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
8107
8755
|
if (provider === "generic" && !wsUrl) {
|
|
8108
8756
|
try {
|
|
8109
8757
|
wsUrl = await getBrowserWebSocketUrl("localhost:9222");
|
|
@@ -8121,8 +8769,10 @@ async function connectCommand(args, globalOptions) {
|
|
|
8121
8769
|
projectId: options.projectId
|
|
8122
8770
|
};
|
|
8123
8771
|
const browser = await connect(connectOptions);
|
|
8124
|
-
const
|
|
8125
|
-
|
|
8772
|
+
const page = options.newTab ? await browser.newPage(pageUrl ?? "about:blank") : await browser.page(
|
|
8773
|
+
void 0,
|
|
8774
|
+
options.targetUrl ? { targetUrl: options.targetUrl } : void 0
|
|
8775
|
+
);
|
|
8126
8776
|
const currentUrl = await page.url();
|
|
8127
8777
|
const sessionId = options.name ?? generateSessionId();
|
|
8128
8778
|
const session = {
|
|
@@ -8835,6 +9485,7 @@ Usage:
|
|
|
8835
9485
|
|
|
8836
9486
|
Options:
|
|
8837
9487
|
-f, --file <path> Read JavaScript from a file
|
|
9488
|
+
--wrap Wrap the expression in an async IIFE
|
|
8838
9489
|
-s, --session <id> Session to use (default: most recent)
|
|
8839
9490
|
-f, --format <fmt> Output format: json | pretty (default: pretty)
|
|
8840
9491
|
--json Alias for -f json
|
|
@@ -8853,12 +9504,25 @@ function parseEvalArgs(args) {
|
|
|
8853
9504
|
const arg = args[i];
|
|
8854
9505
|
if (arg === "-f" || arg === "--file") {
|
|
8855
9506
|
options.file = args[++i];
|
|
9507
|
+
} else if (arg === "--wrap") {
|
|
9508
|
+
options.wrap = true;
|
|
8856
9509
|
} else if (!expression && !arg.startsWith("-")) {
|
|
8857
9510
|
expression = arg;
|
|
8858
9511
|
}
|
|
8859
9512
|
}
|
|
8860
9513
|
return { expression, options };
|
|
8861
9514
|
}
|
|
9515
|
+
function normalizeEvalExpression(expression, wrap = false) {
|
|
9516
|
+
const trimmed = expression.trim();
|
|
9517
|
+
const needsWrap = wrap || trimmed.includes("=>") || /\bawait\b/.test(trimmed);
|
|
9518
|
+
if (!needsWrap) {
|
|
9519
|
+
return trimmed;
|
|
9520
|
+
}
|
|
9521
|
+
if (wrap || /\bawait\b/.test(trimmed)) {
|
|
9522
|
+
return `(async () => (${trimmed}))()`;
|
|
9523
|
+
}
|
|
9524
|
+
return `(() => (${trimmed}))()`;
|
|
9525
|
+
}
|
|
8862
9526
|
async function evalCommand(args, globalOptions) {
|
|
8863
9527
|
if (globalOptions.help) {
|
|
8864
9528
|
console.log(EVAL_HELP);
|
|
@@ -8885,7 +9549,10 @@ async function evalCommand(args, globalOptions) {
|
|
|
8885
9549
|
const session = await resolveSession(globalOptions.session);
|
|
8886
9550
|
const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
|
|
8887
9551
|
try {
|
|
8888
|
-
const step = {
|
|
9552
|
+
const step = {
|
|
9553
|
+
action: "evaluate",
|
|
9554
|
+
value: normalizeEvalExpression(expression, evalOptions.wrap)
|
|
9555
|
+
};
|
|
8889
9556
|
const result = await page.batch([step]);
|
|
8890
9557
|
const stepResult = result.steps[0];
|
|
8891
9558
|
if (!stepResult.success) {
|
|
@@ -9079,6 +9746,7 @@ Usage:
|
|
|
9079
9746
|
|
|
9080
9747
|
Options:
|
|
9081
9748
|
-f, --file <path> Read actions from a JSON file
|
|
9749
|
+
-o, --output <path> Write command output to a file instead of stdout
|
|
9082
9750
|
--dialog <mode> Handle native dialogs: accept | dismiss
|
|
9083
9751
|
-s, --session <id> Session to use (default: most recent)
|
|
9084
9752
|
-f, --format <fmt> Output format: json | pretty (default: pretty)
|
|
@@ -9109,12 +9777,33 @@ function parseExecArgs(args) {
|
|
|
9109
9777
|
}
|
|
9110
9778
|
} else if (arg === "-f" || arg === "--file") {
|
|
9111
9779
|
options.file = args[++i];
|
|
9780
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
9781
|
+
options.outputFile = args[++i];
|
|
9112
9782
|
} else if (!actionsJson && !arg.startsWith("-")) {
|
|
9113
9783
|
actionsJson = arg;
|
|
9114
9784
|
}
|
|
9115
9785
|
}
|
|
9116
9786
|
return { actionsJson, options };
|
|
9117
9787
|
}
|
|
9788
|
+
async function getCurrentUrlSafe(page, fallback) {
|
|
9789
|
+
try {
|
|
9790
|
+
return await page.url();
|
|
9791
|
+
} catch {
|
|
9792
|
+
return fallback;
|
|
9793
|
+
}
|
|
9794
|
+
}
|
|
9795
|
+
async function captureFinalUrl(page, steps, fallback) {
|
|
9796
|
+
const currentUrl = await getCurrentUrlSafe(page, fallback);
|
|
9797
|
+
if (currentUrl !== fallback) {
|
|
9798
|
+
return currentUrl;
|
|
9799
|
+
}
|
|
9800
|
+
const mightNavigate = steps.some((step) => step.action === "click" || step.action === "submit");
|
|
9801
|
+
if (!mightNavigate) {
|
|
9802
|
+
return currentUrl;
|
|
9803
|
+
}
|
|
9804
|
+
await new Promise((resolve3) => setTimeout(resolve3, 200));
|
|
9805
|
+
return getCurrentUrlSafe(page, currentUrl);
|
|
9806
|
+
}
|
|
9118
9807
|
async function execCommand(args, globalOptions) {
|
|
9119
9808
|
if (globalOptions.help) {
|
|
9120
9809
|
console.log(EXEC_HELP);
|
|
@@ -9173,8 +9862,12 @@ Run 'bp actions' for complete action reference.${evalTip}`
|
|
|
9173
9862
|
}
|
|
9174
9863
|
const steps = Array.isArray(actions) ? actions : [actions];
|
|
9175
9864
|
const urlBefore = await page.url();
|
|
9865
|
+
const currentTargetId = page.targetId;
|
|
9866
|
+
const closesCurrentTarget = steps.some(
|
|
9867
|
+
(step) => step.action === "closeTab" && (!step.targetId || step.targetId === currentTargetId)
|
|
9868
|
+
);
|
|
9176
9869
|
const result = await page.batch(steps);
|
|
9177
|
-
const urlAfter = await page
|
|
9870
|
+
const urlAfter = closesCurrentTarget ? urlBefore : await captureFinalUrl(page, steps, urlBefore);
|
|
9178
9871
|
for (const stepResult of result.steps) {
|
|
9179
9872
|
logger.logCommand(
|
|
9180
9873
|
stepResult.action,
|
|
@@ -9196,9 +9889,14 @@ Run 'bp actions' for complete action reference.${evalTip}`
|
|
|
9196
9889
|
urlBefore,
|
|
9197
9890
|
urlAfter
|
|
9198
9891
|
});
|
|
9199
|
-
const currentUrl = await page
|
|
9892
|
+
const currentUrl = closesCurrentTarget ? urlBefore : await captureFinalUrl(page, steps, urlAfter);
|
|
9200
9893
|
const hasSnapshot = steps.some((step) => step.action === "snapshot");
|
|
9201
|
-
if (
|
|
9894
|
+
if (closesCurrentTarget) {
|
|
9895
|
+
await updateSession(session.id, {
|
|
9896
|
+
currentUrl,
|
|
9897
|
+
targetId: void 0
|
|
9898
|
+
});
|
|
9899
|
+
} else if (hasSnapshot) {
|
|
9202
9900
|
await updateSession(session.id, {
|
|
9203
9901
|
currentUrl,
|
|
9204
9902
|
metadata: {
|
|
@@ -9221,16 +9919,21 @@ Run 'bp actions' for complete action reference.${evalTip}`
|
|
|
9221
9919
|
text: s.text,
|
|
9222
9920
|
result: s.result
|
|
9223
9921
|
}));
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9922
|
+
const payload = {
|
|
9923
|
+
success: result.success,
|
|
9924
|
+
stoppedAtIndex: result.stoppedAtIndex,
|
|
9925
|
+
steps: outputSteps,
|
|
9926
|
+
totalDurationMs: result.totalDurationMs,
|
|
9927
|
+
currentUrl
|
|
9928
|
+
};
|
|
9929
|
+
if (execOptions.outputFile) {
|
|
9930
|
+
const fs3 = await import("fs/promises");
|
|
9931
|
+
await fs3.writeFile(execOptions.outputFile, renderOutput(payload, globalOptions.format));
|
|
9932
|
+
process.stderr.write(`Wrote output to ${execOptions.outputFile}
|
|
9933
|
+
`);
|
|
9934
|
+
} else {
|
|
9935
|
+
output(payload, globalOptions.format);
|
|
9936
|
+
}
|
|
9234
9937
|
const failedEval = result.steps.find((s) => s.action === "evaluate" && !s.success);
|
|
9235
9938
|
if (failedEval) {
|
|
9236
9939
|
console.error(
|
|
@@ -9243,6 +9946,116 @@ Tip: Use "bp eval 'expression'" for simpler JavaScript inspection/debugging (no
|
|
|
9243
9946
|
}
|
|
9244
9947
|
}
|
|
9245
9948
|
|
|
9949
|
+
// src/cli/commands/form-utils.ts
|
|
9950
|
+
function fieldIdentifier(field) {
|
|
9951
|
+
if (field.id) return `#${field.id}`;
|
|
9952
|
+
if (field.name) return `name=${field.name}`;
|
|
9953
|
+
return `<${field.tag}>`;
|
|
9954
|
+
}
|
|
9955
|
+
function fieldState(field) {
|
|
9956
|
+
if (field.type === "checkbox" || field.type === "radio") {
|
|
9957
|
+
return field.checked ? "checked" : "unchecked";
|
|
9958
|
+
}
|
|
9959
|
+
if (Array.isArray(field.value)) {
|
|
9960
|
+
return field.value.length > 0 ? field.value.join(", ") : '""';
|
|
9961
|
+
}
|
|
9962
|
+
if (typeof field.value === "string") {
|
|
9963
|
+
return JSON.stringify(field.value);
|
|
9964
|
+
}
|
|
9965
|
+
return '""';
|
|
9966
|
+
}
|
|
9967
|
+
function fieldMeta(field) {
|
|
9968
|
+
const bits = [field.label];
|
|
9969
|
+
if (field.required) bits.push("required");
|
|
9970
|
+
if (field.disabled) bits.push("disabled");
|
|
9971
|
+
return bits.filter(Boolean).join(" | ");
|
|
9972
|
+
}
|
|
9973
|
+
function formatFormFieldsPretty(fields) {
|
|
9974
|
+
const lines = [];
|
|
9975
|
+
const seenRadioGroups = /* @__PURE__ */ new Set();
|
|
9976
|
+
for (const field of fields) {
|
|
9977
|
+
if (field.type === "radio" && field.name) {
|
|
9978
|
+
if (seenRadioGroups.has(field.name)) continue;
|
|
9979
|
+
seenRadioGroups.add(field.name);
|
|
9980
|
+
const group = fields.filter(
|
|
9981
|
+
(candidate) => candidate.type === "radio" && candidate.name === field.name
|
|
9982
|
+
);
|
|
9983
|
+
const options = group.map((candidate) => {
|
|
9984
|
+
const label = candidate.label || candidate.value || candidate.id || "(unnamed)";
|
|
9985
|
+
return candidate.checked ? `${label} [checked]` : label;
|
|
9986
|
+
}).join(" | ");
|
|
9987
|
+
const meta2 = fieldMeta(group.find((candidate) => candidate.label) ?? field);
|
|
9988
|
+
lines.push(` ${fieldIdentifier(field)} radio ${options}${meta2 ? ` ${meta2}` : ""}`);
|
|
9989
|
+
continue;
|
|
9990
|
+
}
|
|
9991
|
+
let state = fieldState(field);
|
|
9992
|
+
if (field.options?.length) {
|
|
9993
|
+
const options = field.options.map((option) => option.selected ? `${option.text} [selected]` : option.text).join(" | ");
|
|
9994
|
+
state += options ? ` ${options}` : "";
|
|
9995
|
+
}
|
|
9996
|
+
const meta = fieldMeta(field);
|
|
9997
|
+
lines.push(` ${fieldIdentifier(field)} ${field.type} ${state}${meta ? ` ${meta}` : ""}`);
|
|
9998
|
+
}
|
|
9999
|
+
return lines;
|
|
10000
|
+
}
|
|
10001
|
+
function formatInteractiveElementsPretty(elements, limit = elements.length) {
|
|
10002
|
+
return elements.slice(0, limit).map((element) => {
|
|
10003
|
+
let line = ` ref:${element.ref} ${element.role}`;
|
|
10004
|
+
if (element.name) {
|
|
10005
|
+
line += ` "${element.name}"`;
|
|
10006
|
+
}
|
|
10007
|
+
if (element.disabled) {
|
|
10008
|
+
line += " (disabled)";
|
|
10009
|
+
}
|
|
10010
|
+
if (element.checked !== void 0) {
|
|
10011
|
+
line += element.checked ? " (checked)" : " (unchecked)";
|
|
10012
|
+
}
|
|
10013
|
+
return line;
|
|
10014
|
+
});
|
|
10015
|
+
}
|
|
10016
|
+
|
|
10017
|
+
// src/cli/commands/forms.ts
|
|
10018
|
+
var FORMS_HELP = `
|
|
10019
|
+
bp forms - List form controls on the current page
|
|
10020
|
+
|
|
10021
|
+
Usage:
|
|
10022
|
+
bp forms [options]
|
|
10023
|
+
|
|
10024
|
+
Options:
|
|
10025
|
+
-s, --session <id> Session to use (default: most recent)
|
|
10026
|
+
-f, --format <fmt> json | pretty (default: pretty)
|
|
10027
|
+
--json Alias for -f json
|
|
10028
|
+
--trace Enable debug tracing
|
|
10029
|
+
-h, --help Show this help
|
|
10030
|
+
|
|
10031
|
+
Examples:
|
|
10032
|
+
bp forms
|
|
10033
|
+
bp forms --json
|
|
10034
|
+
`.trimEnd();
|
|
10035
|
+
async function formsCommand(_args, globalOptions) {
|
|
10036
|
+
if (globalOptions.help) {
|
|
10037
|
+
process.stdout.write(`${FORMS_HELP}
|
|
10038
|
+
`);
|
|
10039
|
+
return;
|
|
10040
|
+
}
|
|
10041
|
+
const session = await resolveSession(globalOptions.session);
|
|
10042
|
+
const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
|
|
10043
|
+
try {
|
|
10044
|
+
const [forms, currentUrl] = await Promise.all([page.forms(), page.url()]);
|
|
10045
|
+
if (globalOptions.format === "json") {
|
|
10046
|
+
output(forms, "json");
|
|
10047
|
+
} else if (forms.length === 0) {
|
|
10048
|
+
process.stdout.write("No form controls found.\n");
|
|
10049
|
+
} else {
|
|
10050
|
+
process.stdout.write(`${formatFormFieldsPretty(forms).join("\n")}
|
|
10051
|
+
`);
|
|
10052
|
+
}
|
|
10053
|
+
await updateSession(session.id, { currentUrl });
|
|
10054
|
+
} finally {
|
|
10055
|
+
await browser.disconnect();
|
|
10056
|
+
}
|
|
10057
|
+
}
|
|
10058
|
+
|
|
9246
10059
|
// src/cli/commands/list.ts
|
|
9247
10060
|
var HELP = `
|
|
9248
10061
|
bp list - List sessions and view action logs
|
|
@@ -9781,6 +10594,91 @@ Timeout reached (${options.timeout}ms).`);
|
|
|
9781
10594
|
}
|
|
9782
10595
|
}
|
|
9783
10596
|
|
|
10597
|
+
// src/cli/commands/page.ts
|
|
10598
|
+
var PAGE_HELP = `
|
|
10599
|
+
bp page - Show a compact overview of the current page
|
|
10600
|
+
|
|
10601
|
+
Usage:
|
|
10602
|
+
bp page [options]
|
|
10603
|
+
|
|
10604
|
+
Options:
|
|
10605
|
+
-s, --session <id> Session to use (default: most recent)
|
|
10606
|
+
-f, --format <fmt> json | pretty (default: pretty)
|
|
10607
|
+
--json Alias for -f json
|
|
10608
|
+
--trace Enable debug tracing
|
|
10609
|
+
-h, --help Show this help
|
|
10610
|
+
|
|
10611
|
+
Examples:
|
|
10612
|
+
bp page
|
|
10613
|
+
bp page --json
|
|
10614
|
+
`.trimEnd();
|
|
10615
|
+
async function getHeadings(page) {
|
|
10616
|
+
return page.evaluate(`(() => {
|
|
10617
|
+
return Array.from(document.querySelectorAll('h1, h2, h3'))
|
|
10618
|
+
.map((el) => ({
|
|
10619
|
+
level: el.tagName.toLowerCase(),
|
|
10620
|
+
text: (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim(),
|
|
10621
|
+
}))
|
|
10622
|
+
.filter((heading) => heading.text);
|
|
10623
|
+
})()`);
|
|
10624
|
+
}
|
|
10625
|
+
function formatPageSummary(summary) {
|
|
10626
|
+
const lines = [`URL: ${summary.url}`, `Title: ${summary.title}`];
|
|
10627
|
+
lines.push("", "Headings:");
|
|
10628
|
+
if (summary.headings.length === 0) {
|
|
10629
|
+
lines.push(" (none)");
|
|
10630
|
+
} else {
|
|
10631
|
+
for (const heading of summary.headings) {
|
|
10632
|
+
lines.push(` ${heading.level}: ${heading.text}`);
|
|
10633
|
+
}
|
|
10634
|
+
}
|
|
10635
|
+
lines.push("", "Form fields:");
|
|
10636
|
+
if (summary.forms.length === 0) {
|
|
10637
|
+
lines.push(" (none)");
|
|
10638
|
+
} else {
|
|
10639
|
+
lines.push(...formatFormFieldsPretty(summary.forms));
|
|
10640
|
+
}
|
|
10641
|
+
lines.push("", "Actions:");
|
|
10642
|
+
if (summary.interactiveElements.length === 0) {
|
|
10643
|
+
lines.push(" (none)");
|
|
10644
|
+
} else {
|
|
10645
|
+
lines.push(...formatInteractiveElementsPretty(summary.interactiveElements, 20));
|
|
10646
|
+
}
|
|
10647
|
+
return lines.join("\n");
|
|
10648
|
+
}
|
|
10649
|
+
async function pageCommand(_args, globalOptions) {
|
|
10650
|
+
if (globalOptions.help) {
|
|
10651
|
+
process.stdout.write(`${PAGE_HELP}
|
|
10652
|
+
`);
|
|
10653
|
+
return;
|
|
10654
|
+
}
|
|
10655
|
+
const session = await resolveSession(globalOptions.session);
|
|
10656
|
+
const { browser, page } = await attachSession(session, { trace: globalOptions.trace });
|
|
10657
|
+
try {
|
|
10658
|
+
const [url, title, headings, forms, snapshot] = await Promise.all([
|
|
10659
|
+
page.url(),
|
|
10660
|
+
page.title(),
|
|
10661
|
+
getHeadings(page),
|
|
10662
|
+
page.forms(),
|
|
10663
|
+
page.snapshot()
|
|
10664
|
+
]);
|
|
10665
|
+
const summary = {
|
|
10666
|
+
url,
|
|
10667
|
+
title,
|
|
10668
|
+
headings,
|
|
10669
|
+
forms,
|
|
10670
|
+
interactiveElements: snapshot.interactiveElements
|
|
10671
|
+
};
|
|
10672
|
+
output(
|
|
10673
|
+
globalOptions.format === "json" ? summary : formatPageSummary(summary),
|
|
10674
|
+
globalOptions.format
|
|
10675
|
+
);
|
|
10676
|
+
await updateSession(session.id, { currentUrl: url });
|
|
10677
|
+
} finally {
|
|
10678
|
+
await browser.disconnect();
|
|
10679
|
+
}
|
|
10680
|
+
}
|
|
10681
|
+
|
|
9784
10682
|
// src/cli/commands/quickstart.ts
|
|
9785
10683
|
var QUICKSTART = `
|
|
9786
10684
|
browser-pilot CLI - Quick Start Guide
|
|
@@ -9797,13 +10695,13 @@ STEP 3: GET PAGE SNAPSHOT
|
|
|
9797
10695
|
bp snapshot -i
|
|
9798
10696
|
|
|
9799
10697
|
Shows only interactive elements (buttons, inputs, links) with refs:
|
|
9800
|
-
button "Sign In"
|
|
9801
|
-
textbox "Email"
|
|
9802
|
-
link "Forgot password?"
|
|
10698
|
+
button "Sign In" ref:e2
|
|
10699
|
+
textbox "Email" ref:e3
|
|
10700
|
+
link "Forgot password?" ref:e6
|
|
9803
10701
|
|
|
9804
10702
|
Other formats:
|
|
9805
10703
|
bp snapshot --format text # Full accessibility tree (all elements)
|
|
9806
|
-
bp snapshot
|
|
10704
|
+
bp snapshot --json # Full snapshot as JSON
|
|
9807
10705
|
|
|
9808
10706
|
STEP 4: INTERACT USING REFS
|
|
9809
10707
|
bp exec '{"action":"fill","selector":"ref:e3","value":"test@example.com"}'
|
|
@@ -9822,6 +10720,13 @@ FOR AI AGENTS
|
|
|
9822
10720
|
bp snapshot -i --json
|
|
9823
10721
|
bp exec '{"action":"click","selector":"ref:e3"}' --json
|
|
9824
10722
|
|
|
10723
|
+
PAGE DISCOVERY SHORTCUTS
|
|
10724
|
+
bp page # URL, title, headings, forms, and interactive controls
|
|
10725
|
+
bp forms # Structured list of form fields only
|
|
10726
|
+
bp targets # All available browser tabs
|
|
10727
|
+
bp connect --new-tab --url https://example.com
|
|
10728
|
+
# Start from a fresh tab instead of reusing one
|
|
10729
|
+
|
|
9825
10730
|
TIPS
|
|
9826
10731
|
\u2022 Refs (e1, e2...) are stable within a page - prefer them over CSS selectors
|
|
9827
10732
|
\u2022 After navigation, take a new snapshot to get updated refs
|
|
@@ -11636,7 +12541,9 @@ Usage:
|
|
|
11636
12541
|
|
|
11637
12542
|
Options:
|
|
11638
12543
|
-i, --interactive Show only interactive elements (buttons, inputs, links)
|
|
11639
|
-
-f, --format <type> Output format: full | interactive | text (default:
|
|
12544
|
+
-f, --format <type> Output format: full | interactive | text (default: text)
|
|
12545
|
+
--role <roles> Filter snapshot to accessibility roles (for example: radio,checkbox)
|
|
12546
|
+
-o, --output <path> Write command output to a file instead of stdout
|
|
11640
12547
|
-d, --diff <file> Compare current page against a saved snapshot JSON
|
|
11641
12548
|
--inspect Inject visual ref labels onto the page (auto-removes after 10s)
|
|
11642
12549
|
--keep Keep visual ref labels visible (use with --inspect)
|
|
@@ -11647,22 +12554,31 @@ Options:
|
|
|
11647
12554
|
-h, --help Show this help
|
|
11648
12555
|
|
|
11649
12556
|
Examples:
|
|
12557
|
+
bp snapshot # Full accessibility tree as readable text
|
|
11650
12558
|
bp snapshot -i # Interactive elements only (best for AI agents)
|
|
11651
|
-
bp snapshot --
|
|
12559
|
+
bp snapshot --role radio,checkbox # Focus on specific control roles
|
|
11652
12560
|
bp snapshot --json > page.json # Save full snapshot to file
|
|
11653
12561
|
bp snapshot --diff before.json # Show what changed since before.json
|
|
11654
12562
|
bp snapshot --inspect # Visual ref labels on the page
|
|
11655
12563
|
`.trimEnd();
|
|
11656
12564
|
function parseSnapshotArgs(args) {
|
|
11657
|
-
const options = {
|
|
12565
|
+
const options = {
|
|
12566
|
+
format: "text"
|
|
12567
|
+
};
|
|
11658
12568
|
for (let i = 0; i < args.length; i++) {
|
|
11659
12569
|
const arg = args[i];
|
|
11660
12570
|
if (arg === "--format" || arg === "-f") {
|
|
11661
12571
|
options.format = args[++i];
|
|
12572
|
+
options.formatExplicit = true;
|
|
11662
12573
|
} else if (arg === "--diff" || arg === "-d") {
|
|
11663
12574
|
options.diffFile = args[++i];
|
|
11664
12575
|
} else if (arg === "--interactive" || arg === "-i") {
|
|
11665
12576
|
options.format = "interactive";
|
|
12577
|
+
options.formatExplicit = true;
|
|
12578
|
+
} else if (arg === "--role") {
|
|
12579
|
+
options.roles = args[++i]?.split(",").map((role) => role.trim().toLowerCase()).filter(Boolean);
|
|
12580
|
+
} else if (arg === "--output" || arg === "-o") {
|
|
12581
|
+
options.outputFile = args[++i];
|
|
11666
12582
|
} else if (arg === "--inspect") {
|
|
11667
12583
|
options.inspect = true;
|
|
11668
12584
|
} else if (arg === "--keep") {
|
|
@@ -11673,6 +12589,11 @@ function parseSnapshotArgs(args) {
|
|
|
11673
12589
|
}
|
|
11674
12590
|
return options;
|
|
11675
12591
|
}
|
|
12592
|
+
function writeInfo(message, asStderr = false) {
|
|
12593
|
+
const stream = asStderr ? process.stderr : process.stdout;
|
|
12594
|
+
stream.write(message.endsWith("\n") ? message : `${message}
|
|
12595
|
+
`);
|
|
12596
|
+
}
|
|
11676
12597
|
function sleep7(ms) {
|
|
11677
12598
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
11678
12599
|
}
|
|
@@ -11698,7 +12619,8 @@ async function snapshotCommand(args, globalOptions) {
|
|
|
11698
12619
|
});
|
|
11699
12620
|
try {
|
|
11700
12621
|
const page = await browser.page(void 0, { targetId: session.targetId });
|
|
11701
|
-
const snapshot = await page.snapshot();
|
|
12622
|
+
const snapshot = await page.snapshot(options.roles?.length ? { roles: options.roles } : {});
|
|
12623
|
+
const infoToStderr = globalOptions.format === "json" || !!options.outputFile;
|
|
11702
12624
|
await updateSession(session.id, {
|
|
11703
12625
|
currentUrl: snapshot.url,
|
|
11704
12626
|
metadata: {
|
|
@@ -11716,43 +12638,117 @@ async function snapshotCommand(args, globalOptions) {
|
|
|
11716
12638
|
const beforeContent = fs2.readFileSync(options.diffFile, "utf-8");
|
|
11717
12639
|
const beforeSnapshot = JSON.parse(beforeContent);
|
|
11718
12640
|
const diff = diffSnapshots(beforeSnapshot, snapshot);
|
|
11719
|
-
if (
|
|
12641
|
+
if (options.outputFile) {
|
|
12642
|
+
fs2.writeFileSync(options.outputFile, renderOutput(diff, globalOptions.format));
|
|
12643
|
+
writeInfo(`Wrote output to ${options.outputFile}`, true);
|
|
12644
|
+
} else if (globalOptions.format === "json") {
|
|
11720
12645
|
output(diff, "json");
|
|
11721
12646
|
} else {
|
|
11722
|
-
|
|
12647
|
+
writeInfo(formatDiffPretty(diff));
|
|
11723
12648
|
}
|
|
11724
12649
|
return;
|
|
11725
12650
|
}
|
|
11726
12651
|
if (options.inspect) {
|
|
11727
12652
|
await injectRefOverlay(page, snapshot);
|
|
11728
|
-
|
|
12653
|
+
writeInfo("Overlay injected. Element refs are now visible on the page.", infoToStderr);
|
|
11729
12654
|
if (options.keep) {
|
|
11730
|
-
|
|
11731
|
-
"Overlay will remain visible. Use removeRefOverlay() or refresh the page to remove."
|
|
12655
|
+
writeInfo(
|
|
12656
|
+
"Overlay will remain visible. Use removeRefOverlay() or refresh the page to remove.",
|
|
12657
|
+
infoToStderr
|
|
11732
12658
|
);
|
|
11733
12659
|
} else {
|
|
11734
|
-
|
|
12660
|
+
writeInfo("Overlay will be removed in 10 seconds...", infoToStderr);
|
|
11735
12661
|
await sleep7(1e4);
|
|
11736
12662
|
await removeRefOverlay(page);
|
|
11737
|
-
|
|
12663
|
+
writeInfo("Overlay removed.", infoToStderr);
|
|
11738
12664
|
}
|
|
11739
12665
|
}
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
12666
|
+
const shouldForceFullJson = globalOptions.format === "json" && !options.formatExplicit;
|
|
12667
|
+
let payload = snapshot;
|
|
12668
|
+
if (!shouldForceFullJson) {
|
|
12669
|
+
switch (options.format) {
|
|
12670
|
+
case "interactive":
|
|
12671
|
+
payload = snapshot.interactiveElements;
|
|
12672
|
+
break;
|
|
12673
|
+
case "text":
|
|
12674
|
+
payload = snapshot.text;
|
|
12675
|
+
break;
|
|
12676
|
+
default:
|
|
12677
|
+
payload = snapshot;
|
|
12678
|
+
break;
|
|
12679
|
+
}
|
|
12680
|
+
}
|
|
12681
|
+
if (options.outputFile) {
|
|
12682
|
+
fs2.writeFileSync(options.outputFile, renderOutput(payload, globalOptions.format));
|
|
12683
|
+
writeInfo(`Wrote output to ${options.outputFile}`, true);
|
|
12684
|
+
} else {
|
|
12685
|
+
output(payload, globalOptions.format);
|
|
11750
12686
|
}
|
|
11751
12687
|
} finally {
|
|
11752
12688
|
await browser.disconnect();
|
|
11753
12689
|
}
|
|
11754
12690
|
}
|
|
11755
12691
|
|
|
12692
|
+
// src/cli/commands/targets.ts
|
|
12693
|
+
var TARGETS_HELP = `
|
|
12694
|
+
bp targets - List page tabs available in the connected browser
|
|
12695
|
+
|
|
12696
|
+
Usage:
|
|
12697
|
+
bp targets [options]
|
|
12698
|
+
|
|
12699
|
+
Options:
|
|
12700
|
+
-s, --session <id> Session to use (default: most recent)
|
|
12701
|
+
-f, --format <fmt> json | pretty (default: pretty)
|
|
12702
|
+
--json Alias for -f json
|
|
12703
|
+
--trace Enable debug tracing
|
|
12704
|
+
-h, --help Show this help
|
|
12705
|
+
|
|
12706
|
+
Examples:
|
|
12707
|
+
bp targets
|
|
12708
|
+
bp targets --json
|
|
12709
|
+
`.trimEnd();
|
|
12710
|
+
function formatTargetsPretty(targets) {
|
|
12711
|
+
if (targets.length === 0) {
|
|
12712
|
+
return "No page targets found.";
|
|
12713
|
+
}
|
|
12714
|
+
return targets.map((target) => {
|
|
12715
|
+
const lines = [
|
|
12716
|
+
`${target.title || "(untitled)"}`,
|
|
12717
|
+
` targetId: ${target.targetId}`,
|
|
12718
|
+
` url: ${target.url}`
|
|
12719
|
+
];
|
|
12720
|
+
if (target.attached) {
|
|
12721
|
+
lines.push(" attached: true");
|
|
12722
|
+
}
|
|
12723
|
+
return lines.join("\n");
|
|
12724
|
+
}).join("\n\n");
|
|
12725
|
+
}
|
|
12726
|
+
async function targetsCommand(_args, globalOptions) {
|
|
12727
|
+
if (globalOptions.help) {
|
|
12728
|
+
process.stdout.write(`${TARGETS_HELP}
|
|
12729
|
+
`);
|
|
12730
|
+
return;
|
|
12731
|
+
}
|
|
12732
|
+
const session = globalOptions.session ? await loadSession(globalOptions.session) : await getDefaultSession();
|
|
12733
|
+
if (!session) {
|
|
12734
|
+
throw new Error('No session found. Run "bp connect" first.');
|
|
12735
|
+
}
|
|
12736
|
+
const browser = await connect({
|
|
12737
|
+
provider: session.provider,
|
|
12738
|
+
wsUrl: session.wsUrl,
|
|
12739
|
+
debug: globalOptions.trace
|
|
12740
|
+
});
|
|
12741
|
+
try {
|
|
12742
|
+
const targets = await browser.listTargets();
|
|
12743
|
+
output(
|
|
12744
|
+
globalOptions.format === "json" ? targets : formatTargetsPretty(targets),
|
|
12745
|
+
globalOptions.format
|
|
12746
|
+
);
|
|
12747
|
+
} finally {
|
|
12748
|
+
await browser.disconnect();
|
|
12749
|
+
}
|
|
12750
|
+
}
|
|
12751
|
+
|
|
11756
12752
|
// src/cli/commands/text.ts
|
|
11757
12753
|
var TEXT_HELP = `
|
|
11758
12754
|
bp text - Extract text content from the current page
|
|
@@ -11832,6 +12828,9 @@ Commands:
|
|
|
11832
12828
|
connect Create browser session
|
|
11833
12829
|
exec Execute actions
|
|
11834
12830
|
eval Evaluate JavaScript expression
|
|
12831
|
+
page Show a compact page overview
|
|
12832
|
+
forms List form controls on the page
|
|
12833
|
+
targets List available browser tabs
|
|
11835
12834
|
run Run a workflow file (JSON steps)
|
|
11836
12835
|
record Record browser actions to JSON
|
|
11837
12836
|
audio Audio I/O for voice agent testing
|
|
@@ -11896,34 +12895,43 @@ function parseGlobalOptions(args) {
|
|
|
11896
12895
|
return { options, remaining };
|
|
11897
12896
|
}
|
|
11898
12897
|
function output(data, format = "pretty") {
|
|
12898
|
+
const text = renderOutput(data, format);
|
|
12899
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
12900
|
+
`);
|
|
12901
|
+
}
|
|
12902
|
+
function renderOutput(data, format = "pretty") {
|
|
11899
12903
|
if (format === "json") {
|
|
11900
|
-
|
|
11901
|
-
}
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
12904
|
+
return JSON.stringify(data, null, 2);
|
|
12905
|
+
}
|
|
12906
|
+
if (typeof data === "string") {
|
|
12907
|
+
return data;
|
|
12908
|
+
}
|
|
12909
|
+
if (Array.isArray(data)) {
|
|
12910
|
+
return JSON.stringify(data, null, 2);
|
|
12911
|
+
}
|
|
12912
|
+
if (typeof data === "object" && data !== null) {
|
|
12913
|
+
const lines = [];
|
|
12914
|
+
const { truncated } = prettyPrint(data, lines);
|
|
12915
|
+
if (truncated) {
|
|
12916
|
+
lines.push("", "(Output truncated. Use --json for full data)");
|
|
11911
12917
|
}
|
|
12918
|
+
return lines.join("\n");
|
|
11912
12919
|
}
|
|
12920
|
+
return String(data);
|
|
11913
12921
|
}
|
|
11914
|
-
function prettyPrint(obj, indent = 0) {
|
|
12922
|
+
function prettyPrint(obj, lines, indent = 0) {
|
|
11915
12923
|
const prefix = " ".repeat(indent);
|
|
11916
12924
|
let truncated = false;
|
|
11917
12925
|
for (const [key, value] of Object.entries(obj)) {
|
|
11918
12926
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
11919
|
-
|
|
11920
|
-
const result = prettyPrint(value, indent + 1);
|
|
12927
|
+
lines.push(`${prefix}${key}:`);
|
|
12928
|
+
const result = prettyPrint(value, lines, indent + 1);
|
|
11921
12929
|
if (result.truncated) truncated = true;
|
|
11922
12930
|
} else if (Array.isArray(value)) {
|
|
11923
|
-
|
|
12931
|
+
lines.push(`${prefix}${key}: [${value.length} items]`);
|
|
11924
12932
|
truncated = true;
|
|
11925
12933
|
} else {
|
|
11926
|
-
|
|
12934
|
+
lines.push(`${prefix}${key}: ${value}`);
|
|
11927
12935
|
}
|
|
11928
12936
|
}
|
|
11929
12937
|
return { truncated };
|
|
@@ -11954,6 +12962,15 @@ async function main() {
|
|
|
11954
12962
|
case "eval":
|
|
11955
12963
|
await evalCommand(remaining, options);
|
|
11956
12964
|
break;
|
|
12965
|
+
case "page":
|
|
12966
|
+
await pageCommand(remaining, options);
|
|
12967
|
+
break;
|
|
12968
|
+
case "forms":
|
|
12969
|
+
await formsCommand(remaining, options);
|
|
12970
|
+
break;
|
|
12971
|
+
case "targets":
|
|
12972
|
+
await targetsCommand(remaining, options);
|
|
12973
|
+
break;
|
|
11957
12974
|
case "snapshot":
|
|
11958
12975
|
await snapshotCommand(remaining, options);
|
|
11959
12976
|
break;
|
|
@@ -12011,5 +13028,6 @@ if (import.meta.main) {
|
|
|
12011
13028
|
}
|
|
12012
13029
|
export {
|
|
12013
13030
|
output,
|
|
12014
|
-
parseGlobalOptions
|
|
13031
|
+
parseGlobalOptions,
|
|
13032
|
+
renderOutput
|
|
12015
13033
|
};
|