browser-pilot 0.0.6 → 0.0.8
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/actions.cjs +20 -3
- package/dist/actions.d.cts +4 -4
- package/dist/actions.d.ts +4 -4
- package/dist/actions.mjs +1 -1
- package/dist/browser.cjs +357 -34
- package/dist/browser.d.cts +7 -3
- package/dist/browser.d.ts +7 -3
- package/dist/browser.mjs +6 -5
- package/dist/{chunk-PCNEJAJ7.mjs → chunk-JN44FHTK.mjs} +331 -36
- package/dist/{chunk-6RB3GKQP.mjs → chunk-ZTQ37YQT.mjs} +35 -3
- package/dist/cli.cjs +2049 -217
- package/dist/cli.mjs +1556 -45
- package/dist/index.cjs +357 -34
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +5 -5
- package/dist/{types-TVlTA7nH.d.cts → types-DklIxnbO.d.cts} +37 -3
- package/dist/{types-CbdmaocU.d.ts → types-Pv8KzZ6l.d.ts} +37 -3
- package/package.json +1 -1
package/dist/actions.cjs
CHANGED
|
@@ -25,6 +25,19 @@ __export(actions_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(actions_exports);
|
|
27
27
|
|
|
28
|
+
// src/browser/types.ts
|
|
29
|
+
var ElementNotFoundError = class extends Error {
|
|
30
|
+
selectors;
|
|
31
|
+
hints;
|
|
32
|
+
constructor(selectors, hints) {
|
|
33
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
34
|
+
super(`Element not found: ${selectorList.join(", ")}`);
|
|
35
|
+
this.name = "ElementNotFoundError";
|
|
36
|
+
this.selectors = selectorList;
|
|
37
|
+
this.hints = hints;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
28
41
|
// src/actions/executor.ts
|
|
29
42
|
var DEFAULT_TIMEOUT = 3e4;
|
|
30
43
|
var BatchExecutor = class {
|
|
@@ -56,13 +69,15 @@ var BatchExecutor = class {
|
|
|
56
69
|
});
|
|
57
70
|
} catch (error) {
|
|
58
71
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
72
|
+
const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
|
|
59
73
|
results.push({
|
|
60
74
|
index: i,
|
|
61
75
|
action: step.action,
|
|
62
76
|
selector: step.selector,
|
|
63
77
|
success: false,
|
|
64
78
|
durationMs: Date.now() - stepStart,
|
|
65
|
-
error: errorMessage
|
|
79
|
+
error: errorMessage,
|
|
80
|
+
hints
|
|
66
81
|
});
|
|
67
82
|
if (onFail === "stop" && !step.optional) {
|
|
68
83
|
return {
|
|
@@ -258,10 +273,12 @@ var BatchExecutor = class {
|
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
275
|
/**
|
|
261
|
-
* Get the
|
|
262
|
-
*
|
|
276
|
+
* Get the actual selector that matched the element.
|
|
277
|
+
* Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
|
|
263
278
|
*/
|
|
264
279
|
getUsedSelector(selector) {
|
|
280
|
+
const matched = this.page.getLastMatchedSelector();
|
|
281
|
+
if (matched) return matched;
|
|
265
282
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
266
283
|
}
|
|
267
284
|
};
|
package/dist/actions.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-
|
|
2
|
-
export { A as ActionType, c as StepResult } from './types-
|
|
1
|
+
import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-DklIxnbO.cjs';
|
|
2
|
+
export { A as ActionType, c as StepResult } from './types-DklIxnbO.cjs';
|
|
3
3
|
import './client-7Nqka5MV.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -18,8 +18,8 @@ declare class BatchExecutor {
|
|
|
18
18
|
*/
|
|
19
19
|
private executeStep;
|
|
20
20
|
/**
|
|
21
|
-
* Get the
|
|
22
|
-
*
|
|
21
|
+
* Get the actual selector that matched the element.
|
|
22
|
+
* Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
|
|
23
23
|
*/
|
|
24
24
|
private getUsedSelector;
|
|
25
25
|
}
|
package/dist/actions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-
|
|
2
|
-
export { A as ActionType, c as StepResult } from './types-
|
|
1
|
+
import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-Pv8KzZ6l.js';
|
|
2
|
+
export { A as ActionType, c as StepResult } from './types-Pv8KzZ6l.js';
|
|
3
3
|
import './client-7Nqka5MV.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -18,8 +18,8 @@ declare class BatchExecutor {
|
|
|
18
18
|
*/
|
|
19
19
|
private executeStep;
|
|
20
20
|
/**
|
|
21
|
-
* Get the
|
|
22
|
-
*
|
|
21
|
+
* Get the actual selector that matched the element.
|
|
22
|
+
* Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
|
|
23
23
|
*/
|
|
24
24
|
private getUsedSelector;
|
|
25
25
|
}
|
package/dist/actions.mjs
CHANGED
package/dist/browser.cjs
CHANGED
|
@@ -457,6 +457,31 @@ function createProvider(options) {
|
|
|
457
457
|
}
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// src/browser/types.ts
|
|
461
|
+
var ElementNotFoundError = class extends Error {
|
|
462
|
+
selectors;
|
|
463
|
+
hints;
|
|
464
|
+
constructor(selectors, hints) {
|
|
465
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
466
|
+
super(`Element not found: ${selectorList.join(", ")}`);
|
|
467
|
+
this.name = "ElementNotFoundError";
|
|
468
|
+
this.selectors = selectorList;
|
|
469
|
+
this.hints = hints;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
var TimeoutError = class extends Error {
|
|
473
|
+
constructor(message = "Operation timed out") {
|
|
474
|
+
super(message);
|
|
475
|
+
this.name = "TimeoutError";
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
var NavigationError = class extends Error {
|
|
479
|
+
constructor(message) {
|
|
480
|
+
super(message);
|
|
481
|
+
this.name = "NavigationError";
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
460
485
|
// src/actions/executor.ts
|
|
461
486
|
var DEFAULT_TIMEOUT = 3e4;
|
|
462
487
|
var BatchExecutor = class {
|
|
@@ -488,13 +513,15 @@ var BatchExecutor = class {
|
|
|
488
513
|
});
|
|
489
514
|
} catch (error) {
|
|
490
515
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
516
|
+
const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
|
|
491
517
|
results.push({
|
|
492
518
|
index: i,
|
|
493
519
|
action: step.action,
|
|
494
520
|
selector: step.selector,
|
|
495
521
|
success: false,
|
|
496
522
|
durationMs: Date.now() - stepStart,
|
|
497
|
-
error: errorMessage
|
|
523
|
+
error: errorMessage,
|
|
524
|
+
hints
|
|
498
525
|
});
|
|
499
526
|
if (onFail === "stop" && !step.optional) {
|
|
500
527
|
return {
|
|
@@ -690,10 +717,12 @@ var BatchExecutor = class {
|
|
|
690
717
|
}
|
|
691
718
|
}
|
|
692
719
|
/**
|
|
693
|
-
* Get the
|
|
694
|
-
*
|
|
720
|
+
* Get the actual selector that matched the element.
|
|
721
|
+
* Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
|
|
695
722
|
*/
|
|
696
723
|
getUsedSelector(selector) {
|
|
724
|
+
const matched = this.page.getLastMatchedSelector();
|
|
725
|
+
if (matched) return matched;
|
|
697
726
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
698
727
|
}
|
|
699
728
|
};
|
|
@@ -1085,33 +1114,256 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
1085
1114
|
});
|
|
1086
1115
|
}
|
|
1087
1116
|
|
|
1088
|
-
// src/browser/
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1117
|
+
// src/browser/fuzzy-match.ts
|
|
1118
|
+
function jaroWinkler(a, b) {
|
|
1119
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
1120
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
1121
|
+
if (a === b) return 1;
|
|
1122
|
+
const s1 = a.toLowerCase();
|
|
1123
|
+
const s2 = b.toLowerCase();
|
|
1124
|
+
const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
|
|
1125
|
+
const s1Matches = new Array(s1.length).fill(false);
|
|
1126
|
+
const s2Matches = new Array(s2.length).fill(false);
|
|
1127
|
+
let matches = 0;
|
|
1128
|
+
let transpositions = 0;
|
|
1129
|
+
for (let i = 0; i < s1.length; i++) {
|
|
1130
|
+
const start = Math.max(0, i - matchWindow);
|
|
1131
|
+
const end = Math.min(i + matchWindow + 1, s2.length);
|
|
1132
|
+
for (let j = start; j < end; j++) {
|
|
1133
|
+
if (s2Matches[j] || s1[i] !== s2[j]) continue;
|
|
1134
|
+
s1Matches[i] = true;
|
|
1135
|
+
s2Matches[j] = true;
|
|
1136
|
+
matches++;
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (matches === 0) return 0;
|
|
1141
|
+
let k = 0;
|
|
1142
|
+
for (let i = 0; i < s1.length; i++) {
|
|
1143
|
+
if (!s1Matches[i]) continue;
|
|
1144
|
+
while (!s2Matches[k]) k++;
|
|
1145
|
+
if (s1[i] !== s2[k]) transpositions++;
|
|
1146
|
+
k++;
|
|
1147
|
+
}
|
|
1148
|
+
const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
|
1149
|
+
let prefix = 0;
|
|
1150
|
+
for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
|
|
1151
|
+
if (s1[i] === s2[i]) {
|
|
1152
|
+
prefix++;
|
|
1153
|
+
} else {
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1096
1156
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1157
|
+
const WINKLER_SCALING = 0.1;
|
|
1158
|
+
return jaro + prefix * WINKLER_SCALING * (1 - jaro);
|
|
1159
|
+
}
|
|
1160
|
+
function stringSimilarity(a, b) {
|
|
1161
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
1162
|
+
const lowerA = a.toLowerCase();
|
|
1163
|
+
const lowerB = b.toLowerCase();
|
|
1164
|
+
if (lowerA === lowerB) return 1;
|
|
1165
|
+
const jw = jaroWinkler(a, b);
|
|
1166
|
+
let containsBonus = 0;
|
|
1167
|
+
if (lowerB.includes(lowerA)) {
|
|
1168
|
+
containsBonus = 0.2;
|
|
1169
|
+
} else if (lowerA.includes(lowerB)) {
|
|
1170
|
+
containsBonus = 0.1;
|
|
1171
|
+
}
|
|
1172
|
+
return Math.min(1, jw + containsBonus);
|
|
1173
|
+
}
|
|
1174
|
+
function scoreElement(query, element) {
|
|
1175
|
+
const lowerQuery = query.toLowerCase();
|
|
1176
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
1177
|
+
let nameScore = 0;
|
|
1178
|
+
if (element.name) {
|
|
1179
|
+
const lowerName = element.name.toLowerCase();
|
|
1180
|
+
if (lowerName === lowerQuery) {
|
|
1181
|
+
nameScore = 1;
|
|
1182
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
1183
|
+
nameScore = 0.8;
|
|
1184
|
+
} else if (words.length > 0) {
|
|
1185
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
1186
|
+
nameScore = matchedWords.length / words.length * 0.7;
|
|
1187
|
+
} else {
|
|
1188
|
+
nameScore = stringSimilarity(query, element.name) * 0.6;
|
|
1189
|
+
}
|
|
1102
1190
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1191
|
+
let roleScore = 0;
|
|
1192
|
+
const lowerRole = element.role.toLowerCase();
|
|
1193
|
+
if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
|
|
1194
|
+
roleScore = 0.3;
|
|
1195
|
+
} else if (words.some((w) => lowerRole.includes(w))) {
|
|
1196
|
+
roleScore = 0.2;
|
|
1108
1197
|
}
|
|
1198
|
+
let selectorScore = 0;
|
|
1199
|
+
const lowerSelector = element.selector.toLowerCase();
|
|
1200
|
+
if (words.some((w) => lowerSelector.includes(w))) {
|
|
1201
|
+
selectorScore = 0.2;
|
|
1202
|
+
}
|
|
1203
|
+
const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
|
|
1204
|
+
return totalScore;
|
|
1205
|
+
}
|
|
1206
|
+
function explainMatch(query, element, score) {
|
|
1207
|
+
const reasons = [];
|
|
1208
|
+
const lowerQuery = query.toLowerCase();
|
|
1209
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
1210
|
+
if (element.name) {
|
|
1211
|
+
const lowerName = element.name.toLowerCase();
|
|
1212
|
+
if (lowerName === lowerQuery) {
|
|
1213
|
+
reasons.push("exact name match");
|
|
1214
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
1215
|
+
reasons.push("name contains query");
|
|
1216
|
+
} else if (words.some((w) => lowerName.includes(w))) {
|
|
1217
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
1218
|
+
reasons.push(`name contains: ${matchedWords.join(", ")}`);
|
|
1219
|
+
} else if (stringSimilarity(query, element.name) > 0.5) {
|
|
1220
|
+
reasons.push("similar name");
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const lowerRole = element.role.toLowerCase();
|
|
1224
|
+
if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
|
|
1225
|
+
reasons.push(`role: ${element.role}`);
|
|
1226
|
+
}
|
|
1227
|
+
if (words.some((w) => element.selector.toLowerCase().includes(w))) {
|
|
1228
|
+
reasons.push("selector match");
|
|
1229
|
+
}
|
|
1230
|
+
if (reasons.length === 0) {
|
|
1231
|
+
reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
|
|
1232
|
+
}
|
|
1233
|
+
return reasons.join(", ");
|
|
1234
|
+
}
|
|
1235
|
+
function fuzzyMatchElements(query, elements, maxResults = 5) {
|
|
1236
|
+
if (!query || query.length === 0) {
|
|
1237
|
+
return [];
|
|
1238
|
+
}
|
|
1239
|
+
const THRESHOLD = 0.3;
|
|
1240
|
+
const scored = elements.map((element) => ({
|
|
1241
|
+
element,
|
|
1242
|
+
score: scoreElement(query, element)
|
|
1243
|
+
}));
|
|
1244
|
+
return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
|
|
1245
|
+
element: s.element,
|
|
1246
|
+
score: s.score,
|
|
1247
|
+
matchReason: explainMatch(query, s.element, s.score)
|
|
1248
|
+
}));
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/browser/hint-generator.ts
|
|
1252
|
+
var ACTION_ROLE_MAP = {
|
|
1253
|
+
click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
|
|
1254
|
+
fill: ["textbox", "searchbox", "textarea"],
|
|
1255
|
+
type: ["textbox", "searchbox", "textarea"],
|
|
1256
|
+
submit: ["button", "form"],
|
|
1257
|
+
select: ["combobox", "listbox", "option"],
|
|
1258
|
+
check: ["checkbox", "radio", "switch"],
|
|
1259
|
+
uncheck: ["checkbox", "switch"],
|
|
1260
|
+
focus: [],
|
|
1261
|
+
// Any focusable element
|
|
1262
|
+
hover: [],
|
|
1263
|
+
// Any element
|
|
1264
|
+
clear: ["textbox", "searchbox", "textarea"]
|
|
1109
1265
|
};
|
|
1266
|
+
function extractIntent(selectors) {
|
|
1267
|
+
const patterns = [];
|
|
1268
|
+
let text = "";
|
|
1269
|
+
for (const selector of selectors) {
|
|
1270
|
+
if (selector.startsWith("ref:")) {
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
|
|
1274
|
+
if (idMatch) {
|
|
1275
|
+
patterns.push(idMatch[1]);
|
|
1276
|
+
}
|
|
1277
|
+
const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
|
|
1278
|
+
if (ariaMatch) {
|
|
1279
|
+
patterns.push(ariaMatch[1]);
|
|
1280
|
+
}
|
|
1281
|
+
const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
|
|
1282
|
+
if (testidMatch) {
|
|
1283
|
+
patterns.push(testidMatch[1]);
|
|
1284
|
+
}
|
|
1285
|
+
const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
|
|
1286
|
+
if (classMatch) {
|
|
1287
|
+
patterns.push(classMatch[1]);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
patterns.sort((a, b) => b.length - a.length);
|
|
1291
|
+
text = patterns[0] ?? selectors[0] ?? "";
|
|
1292
|
+
return { text, patterns };
|
|
1293
|
+
}
|
|
1294
|
+
function getHintType(selector) {
|
|
1295
|
+
if (selector.startsWith("ref:")) return "ref";
|
|
1296
|
+
if (selector.includes("data-testid")) return "testid";
|
|
1297
|
+
if (selector.includes("aria-label")) return "aria";
|
|
1298
|
+
if (selector.startsWith("#")) return "id";
|
|
1299
|
+
return "css";
|
|
1300
|
+
}
|
|
1301
|
+
function getConfidence(score) {
|
|
1302
|
+
if (score >= 0.8) return "high";
|
|
1303
|
+
if (score >= 0.5) return "medium";
|
|
1304
|
+
return "low";
|
|
1305
|
+
}
|
|
1306
|
+
function diversifyHints(candidates, maxHints) {
|
|
1307
|
+
const hints = [];
|
|
1308
|
+
const usedTypes = /* @__PURE__ */ new Set();
|
|
1309
|
+
for (const candidate of candidates) {
|
|
1310
|
+
if (hints.length >= maxHints) break;
|
|
1311
|
+
const refSelector = `ref:${candidate.element.ref}`;
|
|
1312
|
+
const hintType = getHintType(refSelector);
|
|
1313
|
+
if (!usedTypes.has(hintType)) {
|
|
1314
|
+
hints.push({
|
|
1315
|
+
selector: refSelector,
|
|
1316
|
+
reason: candidate.matchReason,
|
|
1317
|
+
confidence: getConfidence(candidate.score),
|
|
1318
|
+
element: {
|
|
1319
|
+
ref: candidate.element.ref,
|
|
1320
|
+
role: candidate.element.role,
|
|
1321
|
+
name: candidate.element.name,
|
|
1322
|
+
disabled: candidate.element.disabled
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
usedTypes.add(hintType);
|
|
1326
|
+
} else if (hints.length < maxHints) {
|
|
1327
|
+
hints.push({
|
|
1328
|
+
selector: refSelector,
|
|
1329
|
+
reason: candidate.matchReason,
|
|
1330
|
+
confidence: getConfidence(candidate.score),
|
|
1331
|
+
element: {
|
|
1332
|
+
ref: candidate.element.ref,
|
|
1333
|
+
role: candidate.element.role,
|
|
1334
|
+
name: candidate.element.name,
|
|
1335
|
+
disabled: candidate.element.disabled
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return hints;
|
|
1341
|
+
}
|
|
1342
|
+
async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
|
|
1343
|
+
let snapshot;
|
|
1344
|
+
try {
|
|
1345
|
+
snapshot = await page.snapshot();
|
|
1346
|
+
} catch {
|
|
1347
|
+
return [];
|
|
1348
|
+
}
|
|
1349
|
+
const intent = extractIntent(failedSelectors);
|
|
1350
|
+
const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
|
|
1351
|
+
let candidates = snapshot.interactiveElements;
|
|
1352
|
+
if (roleFilter.length > 0) {
|
|
1353
|
+
candidates = candidates.filter((el) => roleFilter.includes(el.role));
|
|
1354
|
+
}
|
|
1355
|
+
const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
|
|
1356
|
+
if (matches.length === 0) {
|
|
1357
|
+
return [];
|
|
1358
|
+
}
|
|
1359
|
+
return diversifyHints(matches, maxHints);
|
|
1360
|
+
}
|
|
1110
1361
|
|
|
1111
1362
|
// src/browser/page.ts
|
|
1112
1363
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
1113
1364
|
var Page = class {
|
|
1114
1365
|
cdp;
|
|
1366
|
+
_targetId;
|
|
1115
1367
|
rootNodeId = null;
|
|
1116
1368
|
batchExecutor;
|
|
1117
1369
|
emulationState = {};
|
|
@@ -1130,10 +1382,19 @@ var Page = class {
|
|
|
1130
1382
|
frameExecutionContexts = /* @__PURE__ */ new Map();
|
|
1131
1383
|
/** Current frame's execution context ID (null = main frame default) */
|
|
1132
1384
|
currentFrameContextId = null;
|
|
1133
|
-
|
|
1385
|
+
/** Last matched selector from findElement (for selectorUsed tracking) */
|
|
1386
|
+
_lastMatchedSelector;
|
|
1387
|
+
constructor(cdp, targetId) {
|
|
1134
1388
|
this.cdp = cdp;
|
|
1389
|
+
this._targetId = targetId;
|
|
1135
1390
|
this.batchExecutor = new BatchExecutor(this);
|
|
1136
1391
|
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Get the CDP target ID for this page
|
|
1394
|
+
*/
|
|
1395
|
+
get targetId() {
|
|
1396
|
+
return this._targetId;
|
|
1397
|
+
}
|
|
1137
1398
|
/**
|
|
1138
1399
|
* Get the underlying CDP client for advanced operations.
|
|
1139
1400
|
* Use with caution - prefer high-level Page methods when possible.
|
|
@@ -1141,6 +1402,13 @@ var Page = class {
|
|
|
1141
1402
|
get cdpClient() {
|
|
1142
1403
|
return this.cdp;
|
|
1143
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Get the last matched selector from findElement (for selectorUsed tracking).
|
|
1407
|
+
* Returns undefined if no selector has been matched yet.
|
|
1408
|
+
*/
|
|
1409
|
+
getLastMatchedSelector() {
|
|
1410
|
+
return this._lastMatchedSelector;
|
|
1411
|
+
}
|
|
1144
1412
|
/**
|
|
1145
1413
|
* Initialize the page (enable required CDP domains)
|
|
1146
1414
|
*/
|
|
@@ -1260,7 +1528,9 @@ var Page = class {
|
|
|
1260
1528
|
const element = await this.findElement(selector, options);
|
|
1261
1529
|
if (!element) {
|
|
1262
1530
|
if (options.optional) return false;
|
|
1263
|
-
|
|
1531
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1532
|
+
const hints = await generateHints(this, selectorList, "click");
|
|
1533
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1264
1534
|
}
|
|
1265
1535
|
await this.scrollIntoView(element.nodeId);
|
|
1266
1536
|
const submitResult = await this.evaluateInFrame(
|
|
@@ -1296,7 +1566,9 @@ var Page = class {
|
|
|
1296
1566
|
const element = await this.findElement(selector, options);
|
|
1297
1567
|
if (!element) {
|
|
1298
1568
|
if (options.optional) return false;
|
|
1299
|
-
|
|
1569
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1570
|
+
const hints = await generateHints(this, selectorList, "fill");
|
|
1571
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1300
1572
|
}
|
|
1301
1573
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
1302
1574
|
if (clear) {
|
|
@@ -1374,7 +1646,9 @@ var Page = class {
|
|
|
1374
1646
|
const element = await this.findElement(selector, options);
|
|
1375
1647
|
if (!element) {
|
|
1376
1648
|
if (options.optional) return false;
|
|
1377
|
-
|
|
1649
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1650
|
+
const hints = await generateHints(this, selectorList, "select");
|
|
1651
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1378
1652
|
}
|
|
1379
1653
|
const values = Array.isArray(value) ? value : [value];
|
|
1380
1654
|
await this.cdp.send("Runtime.evaluate", {
|
|
@@ -1435,7 +1709,9 @@ var Page = class {
|
|
|
1435
1709
|
const element = await this.findElement(selector, options);
|
|
1436
1710
|
if (!element) {
|
|
1437
1711
|
if (options.optional) return false;
|
|
1438
|
-
|
|
1712
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1713
|
+
const hints = await generateHints(this, selectorList, "check");
|
|
1714
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1439
1715
|
}
|
|
1440
1716
|
const result = await this.cdp.send("Runtime.evaluate", {
|
|
1441
1717
|
expression: `(() => {
|
|
@@ -1455,7 +1731,9 @@ var Page = class {
|
|
|
1455
1731
|
const element = await this.findElement(selector, options);
|
|
1456
1732
|
if (!element) {
|
|
1457
1733
|
if (options.optional) return false;
|
|
1458
|
-
|
|
1734
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1735
|
+
const hints = await generateHints(this, selectorList, "uncheck");
|
|
1736
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1459
1737
|
}
|
|
1460
1738
|
const result = await this.cdp.send("Runtime.evaluate", {
|
|
1461
1739
|
expression: `(() => {
|
|
@@ -1475,13 +1753,40 @@ var Page = class {
|
|
|
1475
1753
|
* - 'auto' (default): Attempt to detect navigation for 1 second, then assume client-side handling
|
|
1476
1754
|
* - true: Wait for full navigation (traditional forms)
|
|
1477
1755
|
* - false: Return immediately (AJAX forms where you'll wait for something else)
|
|
1756
|
+
*
|
|
1757
|
+
* When targeting a <form> element directly, uses form.requestSubmit() which fires
|
|
1758
|
+
* the submit event and triggers HTML5 validation.
|
|
1478
1759
|
*/
|
|
1479
1760
|
async submit(selector, options = {}) {
|
|
1480
1761
|
const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
|
|
1481
1762
|
const element = await this.findElement(selector, options);
|
|
1482
1763
|
if (!element) {
|
|
1483
1764
|
if (options.optional) return false;
|
|
1484
|
-
|
|
1765
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1766
|
+
const hints = await generateHints(this, selectorList, "submit");
|
|
1767
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1768
|
+
}
|
|
1769
|
+
const isFormElement = await this.evaluateInFrame(
|
|
1770
|
+
`(() => {
|
|
1771
|
+
const el = document.querySelector(${JSON.stringify(element.selector)});
|
|
1772
|
+
return el instanceof HTMLFormElement;
|
|
1773
|
+
})()`
|
|
1774
|
+
);
|
|
1775
|
+
if (isFormElement.result.value) {
|
|
1776
|
+
await this.evaluateInFrame(
|
|
1777
|
+
`(() => {
|
|
1778
|
+
const form = document.querySelector(${JSON.stringify(element.selector)});
|
|
1779
|
+
if (form && form instanceof HTMLFormElement) {
|
|
1780
|
+
form.requestSubmit();
|
|
1781
|
+
}
|
|
1782
|
+
})()`
|
|
1783
|
+
);
|
|
1784
|
+
if (shouldWait === true) {
|
|
1785
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
1786
|
+
} else if (shouldWait === "auto") {
|
|
1787
|
+
await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep2(500)]);
|
|
1788
|
+
}
|
|
1789
|
+
return true;
|
|
1485
1790
|
}
|
|
1486
1791
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
1487
1792
|
if (method.includes("enter")) {
|
|
@@ -1552,7 +1857,9 @@ var Page = class {
|
|
|
1552
1857
|
const element = await this.findElement(selector, options);
|
|
1553
1858
|
if (!element) {
|
|
1554
1859
|
if (options.optional) return false;
|
|
1555
|
-
|
|
1860
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1861
|
+
const hints = await generateHints(this, selectorList, "focus");
|
|
1862
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1556
1863
|
}
|
|
1557
1864
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
1558
1865
|
return true;
|
|
@@ -1565,7 +1872,9 @@ var Page = class {
|
|
|
1565
1872
|
const element = await this.findElement(selector, options);
|
|
1566
1873
|
if (!element) {
|
|
1567
1874
|
if (options.optional) return false;
|
|
1568
|
-
|
|
1875
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1876
|
+
const hints = await generateHints(this, selectorList, "hover");
|
|
1877
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1569
1878
|
}
|
|
1570
1879
|
await this.scrollIntoView(element.nodeId);
|
|
1571
1880
|
const box = await this.getBoxModel(element.nodeId);
|
|
@@ -2520,6 +2829,7 @@ var Page = class {
|
|
|
2520
2829
|
async findElement(selectors, options = {}) {
|
|
2521
2830
|
const { timeout = DEFAULT_TIMEOUT2 } = options;
|
|
2522
2831
|
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
2832
|
+
this._lastMatchedSelector = void 0;
|
|
2523
2833
|
for (const selector of selectorList) {
|
|
2524
2834
|
if (selector.startsWith("ref:")) {
|
|
2525
2835
|
const ref = selector.slice(4);
|
|
@@ -2536,6 +2846,7 @@ var Page = class {
|
|
|
2536
2846
|
}
|
|
2537
2847
|
);
|
|
2538
2848
|
if (pushResult.nodeIds?.[0]) {
|
|
2849
|
+
this._lastMatchedSelector = selector;
|
|
2539
2850
|
return {
|
|
2540
2851
|
nodeId: pushResult.nodeIds[0],
|
|
2541
2852
|
backendNodeId,
|
|
@@ -2569,6 +2880,7 @@ var Page = class {
|
|
|
2569
2880
|
"DOM.describeNode",
|
|
2570
2881
|
{ nodeId: queryResult.nodeId }
|
|
2571
2882
|
);
|
|
2883
|
+
this._lastMatchedSelector = result.selector;
|
|
2572
2884
|
return {
|
|
2573
2885
|
nodeId: queryResult.nodeId,
|
|
2574
2886
|
backendNodeId: describeResult2.node.backendNodeId,
|
|
@@ -2596,6 +2908,7 @@ var Page = class {
|
|
|
2596
2908
|
"DOM.describeNode",
|
|
2597
2909
|
{ nodeId: nodeResult.nodeId }
|
|
2598
2910
|
);
|
|
2911
|
+
this._lastMatchedSelector = result.selector;
|
|
2599
2912
|
return {
|
|
2600
2913
|
nodeId: nodeResult.nodeId,
|
|
2601
2914
|
backendNodeId: describeResult.node.backendNodeId,
|
|
@@ -2702,14 +3015,24 @@ var Browser = class _Browser {
|
|
|
2702
3015
|
* Get or create a page by name
|
|
2703
3016
|
* If no name is provided, returns the first available page or creates a new one
|
|
2704
3017
|
*/
|
|
2705
|
-
async page(name) {
|
|
3018
|
+
async page(name, options) {
|
|
2706
3019
|
const pageName = name ?? "default";
|
|
2707
3020
|
const cached = this.pages.get(pageName);
|
|
2708
3021
|
if (cached) return cached;
|
|
2709
3022
|
const targets = await this.cdp.send("Target.getTargets");
|
|
2710
3023
|
const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
2711
3024
|
let targetId;
|
|
2712
|
-
if (
|
|
3025
|
+
if (options?.targetId) {
|
|
3026
|
+
const targetExists = pageTargets.some((t) => t.targetId === options.targetId);
|
|
3027
|
+
if (targetExists) {
|
|
3028
|
+
targetId = options.targetId;
|
|
3029
|
+
} else {
|
|
3030
|
+
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
3031
|
+
targetId = pageTargets.length > 0 ? pageTargets[0].targetId : (await this.cdp.send("Target.createTarget", {
|
|
3032
|
+
url: "about:blank"
|
|
3033
|
+
})).targetId;
|
|
3034
|
+
}
|
|
3035
|
+
} else if (pageTargets.length > 0) {
|
|
2713
3036
|
targetId = pageTargets[0].targetId;
|
|
2714
3037
|
} else {
|
|
2715
3038
|
const result = await this.cdp.send("Target.createTarget", {
|
|
@@ -2718,7 +3041,7 @@ var Browser = class _Browser {
|
|
|
2718
3041
|
targetId = result.targetId;
|
|
2719
3042
|
}
|
|
2720
3043
|
await this.cdp.attachToTarget(targetId);
|
|
2721
|
-
const page = new Page(this.cdp);
|
|
3044
|
+
const page = new Page(this.cdp, targetId);
|
|
2722
3045
|
await page.init();
|
|
2723
3046
|
this.pages.set(pageName, page);
|
|
2724
3047
|
return page;
|
|
@@ -2731,7 +3054,7 @@ var Browser = class _Browser {
|
|
|
2731
3054
|
url
|
|
2732
3055
|
});
|
|
2733
3056
|
await this.cdp.attachToTarget(result.targetId);
|
|
2734
|
-
const page = new Page(this.cdp);
|
|
3057
|
+
const page = new Page(this.cdp, result.targetId);
|
|
2735
3058
|
await page.init();
|
|
2736
3059
|
const name = `page-${this.pages.size + 1}`;
|
|
2737
3060
|
this.pages.set(name, page);
|