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/browser.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { C as CDPClient } from './client-7Nqka5MV.cjs';
|
|
2
2
|
import { C as ConnectOptions } from './types-D_uDqh0Z.cjs';
|
|
3
|
-
import { P as Page } from './types-
|
|
4
|
-
export { d as ActionOptions, e as ActionResult, C as ConsoleHandler, f as ConsoleMessage, g as ConsoleMessageType, h as CustomSelectConfig, D as Dialog, i as DialogHandler, j as DialogType, k as Download, E as ElementInfo, l as ElementNotFoundError, m as EmulationState, n as ErrorHandler, F as FileInput, o as FillOptions, G as GeolocationOptions, I as InteractiveElement, N as NavigationError, p as NetworkIdleOptions, q as PageError, r as PageSnapshot, s as SnapshotNode, t as SubmitOptions, T as TimeoutError, u as TypeOptions, U as UserAgentMetadata, v as UserAgentOptions, V as ViewportOptions, W as WaitForOptions } from './types-
|
|
3
|
+
import { P as Page } from './types-DklIxnbO.cjs';
|
|
4
|
+
export { d as ActionOptions, e as ActionResult, C as ConsoleHandler, f as ConsoleMessage, g as ConsoleMessageType, h as CustomSelectConfig, D as Dialog, i as DialogHandler, j as DialogType, k as Download, E as ElementInfo, l as ElementNotFoundError, m as EmulationState, n as ErrorHandler, a5 as FailureHint, F as FileInput, o as FillOptions, G as GeolocationOptions, I as InteractiveElement, N as NavigationError, p as NetworkIdleOptions, q as PageError, r as PageSnapshot, s as SnapshotNode, t as SubmitOptions, T as TimeoutError, u as TypeOptions, U as UserAgentMetadata, v as UserAgentOptions, V as ViewportOptions, W as WaitForOptions } from './types-DklIxnbO.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Browser class - manages CDP connection and pages
|
|
@@ -11,6 +11,10 @@ interface BrowserOptions extends ConnectOptions {
|
|
|
11
11
|
/** Enable debug logging */
|
|
12
12
|
debug?: boolean;
|
|
13
13
|
}
|
|
14
|
+
interface PageOptions {
|
|
15
|
+
/** Specific target ID to attach to */
|
|
16
|
+
targetId?: string;
|
|
17
|
+
}
|
|
14
18
|
declare class Browser {
|
|
15
19
|
private cdp;
|
|
16
20
|
private providerSession;
|
|
@@ -24,7 +28,7 @@ declare class Browser {
|
|
|
24
28
|
* Get or create a page by name
|
|
25
29
|
* If no name is provided, returns the first available page or creates a new one
|
|
26
30
|
*/
|
|
27
|
-
page(name?: string): Promise<Page>;
|
|
31
|
+
page(name?: string, options?: PageOptions): Promise<Page>;
|
|
28
32
|
/**
|
|
29
33
|
* Create a new page (tab)
|
|
30
34
|
*/
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { C as CDPClient } from './client-7Nqka5MV.js';
|
|
2
2
|
import { C as ConnectOptions } from './types-D_uDqh0Z.js';
|
|
3
|
-
import { P as Page } from './types-
|
|
4
|
-
export { d as ActionOptions, e as ActionResult, C as ConsoleHandler, f as ConsoleMessage, g as ConsoleMessageType, h as CustomSelectConfig, D as Dialog, i as DialogHandler, j as DialogType, k as Download, E as ElementInfo, l as ElementNotFoundError, m as EmulationState, n as ErrorHandler, F as FileInput, o as FillOptions, G as GeolocationOptions, I as InteractiveElement, N as NavigationError, p as NetworkIdleOptions, q as PageError, r as PageSnapshot, s as SnapshotNode, t as SubmitOptions, T as TimeoutError, u as TypeOptions, U as UserAgentMetadata, v as UserAgentOptions, V as ViewportOptions, W as WaitForOptions } from './types-
|
|
3
|
+
import { P as Page } from './types-Pv8KzZ6l.js';
|
|
4
|
+
export { d as ActionOptions, e as ActionResult, C as ConsoleHandler, f as ConsoleMessage, g as ConsoleMessageType, h as CustomSelectConfig, D as Dialog, i as DialogHandler, j as DialogType, k as Download, E as ElementInfo, l as ElementNotFoundError, m as EmulationState, n as ErrorHandler, a5 as FailureHint, F as FileInput, o as FillOptions, G as GeolocationOptions, I as InteractiveElement, N as NavigationError, p as NetworkIdleOptions, q as PageError, r as PageSnapshot, s as SnapshotNode, t as SubmitOptions, T as TimeoutError, u as TypeOptions, U as UserAgentMetadata, v as UserAgentOptions, V as ViewportOptions, W as WaitForOptions } from './types-Pv8KzZ6l.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Browser class - manages CDP connection and pages
|
|
@@ -11,6 +11,10 @@ interface BrowserOptions extends ConnectOptions {
|
|
|
11
11
|
/** Enable debug logging */
|
|
12
12
|
debug?: boolean;
|
|
13
13
|
}
|
|
14
|
+
interface PageOptions {
|
|
15
|
+
/** Specific target ID to attach to */
|
|
16
|
+
targetId?: string;
|
|
17
|
+
}
|
|
14
18
|
declare class Browser {
|
|
15
19
|
private cdp;
|
|
16
20
|
private providerSession;
|
|
@@ -24,7 +28,7 @@ declare class Browser {
|
|
|
24
28
|
* Get or create a page by name
|
|
25
29
|
* If no name is provided, returns the first available page or creates a new one
|
|
26
30
|
*/
|
|
27
|
-
page(name?: string): Promise<Page>;
|
|
31
|
+
page(name?: string, options?: PageOptions): Promise<Page>;
|
|
28
32
|
/**
|
|
29
33
|
* Create a new page (tab)
|
|
30
34
|
*/
|
package/dist/browser.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Browser,
|
|
3
|
-
ElementNotFoundError,
|
|
4
|
-
NavigationError,
|
|
5
3
|
Page,
|
|
6
|
-
TimeoutError,
|
|
7
4
|
connect
|
|
8
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-JN44FHTK.mjs";
|
|
9
6
|
import "./chunk-BCOZUKWS.mjs";
|
|
10
7
|
import "./chunk-R3PS4PCM.mjs";
|
|
11
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
ElementNotFoundError,
|
|
10
|
+
NavigationError,
|
|
11
|
+
TimeoutError
|
|
12
|
+
} from "./chunk-ZTQ37YQT.mjs";
|
|
12
13
|
export {
|
|
13
14
|
Browser,
|
|
14
15
|
ElementNotFoundError,
|
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
createProvider
|
|
6
6
|
} from "./chunk-R3PS4PCM.mjs";
|
|
7
7
|
import {
|
|
8
|
-
BatchExecutor
|
|
9
|
-
|
|
8
|
+
BatchExecutor,
|
|
9
|
+
ElementNotFoundError,
|
|
10
|
+
TimeoutError
|
|
11
|
+
} from "./chunk-ZTQ37YQT.mjs";
|
|
10
12
|
|
|
11
13
|
// src/network/interceptor.ts
|
|
12
14
|
var RequestInterceptor = class {
|
|
@@ -422,33 +424,256 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
422
424
|
});
|
|
423
425
|
}
|
|
424
426
|
|
|
425
|
-
// src/browser/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
427
|
+
// src/browser/fuzzy-match.ts
|
|
428
|
+
function jaroWinkler(a, b) {
|
|
429
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
430
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
431
|
+
if (a === b) return 1;
|
|
432
|
+
const s1 = a.toLowerCase();
|
|
433
|
+
const s2 = b.toLowerCase();
|
|
434
|
+
const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
|
|
435
|
+
const s1Matches = new Array(s1.length).fill(false);
|
|
436
|
+
const s2Matches = new Array(s2.length).fill(false);
|
|
437
|
+
let matches = 0;
|
|
438
|
+
let transpositions = 0;
|
|
439
|
+
for (let i = 0; i < s1.length; i++) {
|
|
440
|
+
const start = Math.max(0, i - matchWindow);
|
|
441
|
+
const end = Math.min(i + matchWindow + 1, s2.length);
|
|
442
|
+
for (let j = start; j < end; j++) {
|
|
443
|
+
if (s2Matches[j] || s1[i] !== s2[j]) continue;
|
|
444
|
+
s1Matches[i] = true;
|
|
445
|
+
s2Matches[j] = true;
|
|
446
|
+
matches++;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (matches === 0) return 0;
|
|
451
|
+
let k = 0;
|
|
452
|
+
for (let i = 0; i < s1.length; i++) {
|
|
453
|
+
if (!s1Matches[i]) continue;
|
|
454
|
+
while (!s2Matches[k]) k++;
|
|
455
|
+
if (s1[i] !== s2[k]) transpositions++;
|
|
456
|
+
k++;
|
|
457
|
+
}
|
|
458
|
+
const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
|
459
|
+
let prefix = 0;
|
|
460
|
+
for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
|
|
461
|
+
if (s1[i] === s2[i]) {
|
|
462
|
+
prefix++;
|
|
463
|
+
} else {
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
433
466
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
467
|
+
const WINKLER_SCALING = 0.1;
|
|
468
|
+
return jaro + prefix * WINKLER_SCALING * (1 - jaro);
|
|
469
|
+
}
|
|
470
|
+
function stringSimilarity(a, b) {
|
|
471
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
472
|
+
const lowerA = a.toLowerCase();
|
|
473
|
+
const lowerB = b.toLowerCase();
|
|
474
|
+
if (lowerA === lowerB) return 1;
|
|
475
|
+
const jw = jaroWinkler(a, b);
|
|
476
|
+
let containsBonus = 0;
|
|
477
|
+
if (lowerB.includes(lowerA)) {
|
|
478
|
+
containsBonus = 0.2;
|
|
479
|
+
} else if (lowerA.includes(lowerB)) {
|
|
480
|
+
containsBonus = 0.1;
|
|
481
|
+
}
|
|
482
|
+
return Math.min(1, jw + containsBonus);
|
|
483
|
+
}
|
|
484
|
+
function scoreElement(query, element) {
|
|
485
|
+
const lowerQuery = query.toLowerCase();
|
|
486
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
487
|
+
let nameScore = 0;
|
|
488
|
+
if (element.name) {
|
|
489
|
+
const lowerName = element.name.toLowerCase();
|
|
490
|
+
if (lowerName === lowerQuery) {
|
|
491
|
+
nameScore = 1;
|
|
492
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
493
|
+
nameScore = 0.8;
|
|
494
|
+
} else if (words.length > 0) {
|
|
495
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
496
|
+
nameScore = matchedWords.length / words.length * 0.7;
|
|
497
|
+
} else {
|
|
498
|
+
nameScore = stringSimilarity(query, element.name) * 0.6;
|
|
499
|
+
}
|
|
439
500
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
501
|
+
let roleScore = 0;
|
|
502
|
+
const lowerRole = element.role.toLowerCase();
|
|
503
|
+
if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
|
|
504
|
+
roleScore = 0.3;
|
|
505
|
+
} else if (words.some((w) => lowerRole.includes(w))) {
|
|
506
|
+
roleScore = 0.2;
|
|
507
|
+
}
|
|
508
|
+
let selectorScore = 0;
|
|
509
|
+
const lowerSelector = element.selector.toLowerCase();
|
|
510
|
+
if (words.some((w) => lowerSelector.includes(w))) {
|
|
511
|
+
selectorScore = 0.2;
|
|
445
512
|
}
|
|
513
|
+
const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
|
|
514
|
+
return totalScore;
|
|
515
|
+
}
|
|
516
|
+
function explainMatch(query, element, score) {
|
|
517
|
+
const reasons = [];
|
|
518
|
+
const lowerQuery = query.toLowerCase();
|
|
519
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
520
|
+
if (element.name) {
|
|
521
|
+
const lowerName = element.name.toLowerCase();
|
|
522
|
+
if (lowerName === lowerQuery) {
|
|
523
|
+
reasons.push("exact name match");
|
|
524
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
525
|
+
reasons.push("name contains query");
|
|
526
|
+
} else if (words.some((w) => lowerName.includes(w))) {
|
|
527
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
528
|
+
reasons.push(`name contains: ${matchedWords.join(", ")}`);
|
|
529
|
+
} else if (stringSimilarity(query, element.name) > 0.5) {
|
|
530
|
+
reasons.push("similar name");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const lowerRole = element.role.toLowerCase();
|
|
534
|
+
if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
|
|
535
|
+
reasons.push(`role: ${element.role}`);
|
|
536
|
+
}
|
|
537
|
+
if (words.some((w) => element.selector.toLowerCase().includes(w))) {
|
|
538
|
+
reasons.push("selector match");
|
|
539
|
+
}
|
|
540
|
+
if (reasons.length === 0) {
|
|
541
|
+
reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
|
|
542
|
+
}
|
|
543
|
+
return reasons.join(", ");
|
|
544
|
+
}
|
|
545
|
+
function fuzzyMatchElements(query, elements, maxResults = 5) {
|
|
546
|
+
if (!query || query.length === 0) {
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
const THRESHOLD = 0.3;
|
|
550
|
+
const scored = elements.map((element) => ({
|
|
551
|
+
element,
|
|
552
|
+
score: scoreElement(query, element)
|
|
553
|
+
}));
|
|
554
|
+
return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
|
|
555
|
+
element: s.element,
|
|
556
|
+
score: s.score,
|
|
557
|
+
matchReason: explainMatch(query, s.element, s.score)
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/browser/hint-generator.ts
|
|
562
|
+
var ACTION_ROLE_MAP = {
|
|
563
|
+
click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
|
|
564
|
+
fill: ["textbox", "searchbox", "textarea"],
|
|
565
|
+
type: ["textbox", "searchbox", "textarea"],
|
|
566
|
+
submit: ["button", "form"],
|
|
567
|
+
select: ["combobox", "listbox", "option"],
|
|
568
|
+
check: ["checkbox", "radio", "switch"],
|
|
569
|
+
uncheck: ["checkbox", "switch"],
|
|
570
|
+
focus: [],
|
|
571
|
+
// Any focusable element
|
|
572
|
+
hover: [],
|
|
573
|
+
// Any element
|
|
574
|
+
clear: ["textbox", "searchbox", "textarea"]
|
|
446
575
|
};
|
|
576
|
+
function extractIntent(selectors) {
|
|
577
|
+
const patterns = [];
|
|
578
|
+
let text = "";
|
|
579
|
+
for (const selector of selectors) {
|
|
580
|
+
if (selector.startsWith("ref:")) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
|
|
584
|
+
if (idMatch) {
|
|
585
|
+
patterns.push(idMatch[1]);
|
|
586
|
+
}
|
|
587
|
+
const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
|
|
588
|
+
if (ariaMatch) {
|
|
589
|
+
patterns.push(ariaMatch[1]);
|
|
590
|
+
}
|
|
591
|
+
const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
|
|
592
|
+
if (testidMatch) {
|
|
593
|
+
patterns.push(testidMatch[1]);
|
|
594
|
+
}
|
|
595
|
+
const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
|
|
596
|
+
if (classMatch) {
|
|
597
|
+
patterns.push(classMatch[1]);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
patterns.sort((a, b) => b.length - a.length);
|
|
601
|
+
text = patterns[0] ?? selectors[0] ?? "";
|
|
602
|
+
return { text, patterns };
|
|
603
|
+
}
|
|
604
|
+
function getHintType(selector) {
|
|
605
|
+
if (selector.startsWith("ref:")) return "ref";
|
|
606
|
+
if (selector.includes("data-testid")) return "testid";
|
|
607
|
+
if (selector.includes("aria-label")) return "aria";
|
|
608
|
+
if (selector.startsWith("#")) return "id";
|
|
609
|
+
return "css";
|
|
610
|
+
}
|
|
611
|
+
function getConfidence(score) {
|
|
612
|
+
if (score >= 0.8) return "high";
|
|
613
|
+
if (score >= 0.5) return "medium";
|
|
614
|
+
return "low";
|
|
615
|
+
}
|
|
616
|
+
function diversifyHints(candidates, maxHints) {
|
|
617
|
+
const hints = [];
|
|
618
|
+
const usedTypes = /* @__PURE__ */ new Set();
|
|
619
|
+
for (const candidate of candidates) {
|
|
620
|
+
if (hints.length >= maxHints) break;
|
|
621
|
+
const refSelector = `ref:${candidate.element.ref}`;
|
|
622
|
+
const hintType = getHintType(refSelector);
|
|
623
|
+
if (!usedTypes.has(hintType)) {
|
|
624
|
+
hints.push({
|
|
625
|
+
selector: refSelector,
|
|
626
|
+
reason: candidate.matchReason,
|
|
627
|
+
confidence: getConfidence(candidate.score),
|
|
628
|
+
element: {
|
|
629
|
+
ref: candidate.element.ref,
|
|
630
|
+
role: candidate.element.role,
|
|
631
|
+
name: candidate.element.name,
|
|
632
|
+
disabled: candidate.element.disabled
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
usedTypes.add(hintType);
|
|
636
|
+
} else if (hints.length < maxHints) {
|
|
637
|
+
hints.push({
|
|
638
|
+
selector: refSelector,
|
|
639
|
+
reason: candidate.matchReason,
|
|
640
|
+
confidence: getConfidence(candidate.score),
|
|
641
|
+
element: {
|
|
642
|
+
ref: candidate.element.ref,
|
|
643
|
+
role: candidate.element.role,
|
|
644
|
+
name: candidate.element.name,
|
|
645
|
+
disabled: candidate.element.disabled
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return hints;
|
|
651
|
+
}
|
|
652
|
+
async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
|
|
653
|
+
let snapshot;
|
|
654
|
+
try {
|
|
655
|
+
snapshot = await page.snapshot();
|
|
656
|
+
} catch {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
const intent = extractIntent(failedSelectors);
|
|
660
|
+
const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
|
|
661
|
+
let candidates = snapshot.interactiveElements;
|
|
662
|
+
if (roleFilter.length > 0) {
|
|
663
|
+
candidates = candidates.filter((el) => roleFilter.includes(el.role));
|
|
664
|
+
}
|
|
665
|
+
const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
|
|
666
|
+
if (matches.length === 0) {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
return diversifyHints(matches, maxHints);
|
|
670
|
+
}
|
|
447
671
|
|
|
448
672
|
// src/browser/page.ts
|
|
449
673
|
var DEFAULT_TIMEOUT = 3e4;
|
|
450
674
|
var Page = class {
|
|
451
675
|
cdp;
|
|
676
|
+
_targetId;
|
|
452
677
|
rootNodeId = null;
|
|
453
678
|
batchExecutor;
|
|
454
679
|
emulationState = {};
|
|
@@ -467,10 +692,19 @@ var Page = class {
|
|
|
467
692
|
frameExecutionContexts = /* @__PURE__ */ new Map();
|
|
468
693
|
/** Current frame's execution context ID (null = main frame default) */
|
|
469
694
|
currentFrameContextId = null;
|
|
470
|
-
|
|
695
|
+
/** Last matched selector from findElement (for selectorUsed tracking) */
|
|
696
|
+
_lastMatchedSelector;
|
|
697
|
+
constructor(cdp, targetId) {
|
|
471
698
|
this.cdp = cdp;
|
|
699
|
+
this._targetId = targetId;
|
|
472
700
|
this.batchExecutor = new BatchExecutor(this);
|
|
473
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* Get the CDP target ID for this page
|
|
704
|
+
*/
|
|
705
|
+
get targetId() {
|
|
706
|
+
return this._targetId;
|
|
707
|
+
}
|
|
474
708
|
/**
|
|
475
709
|
* Get the underlying CDP client for advanced operations.
|
|
476
710
|
* Use with caution - prefer high-level Page methods when possible.
|
|
@@ -478,6 +712,13 @@ var Page = class {
|
|
|
478
712
|
get cdpClient() {
|
|
479
713
|
return this.cdp;
|
|
480
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* Get the last matched selector from findElement (for selectorUsed tracking).
|
|
717
|
+
* Returns undefined if no selector has been matched yet.
|
|
718
|
+
*/
|
|
719
|
+
getLastMatchedSelector() {
|
|
720
|
+
return this._lastMatchedSelector;
|
|
721
|
+
}
|
|
481
722
|
/**
|
|
482
723
|
* Initialize the page (enable required CDP domains)
|
|
483
724
|
*/
|
|
@@ -597,7 +838,9 @@ var Page = class {
|
|
|
597
838
|
const element = await this.findElement(selector, options);
|
|
598
839
|
if (!element) {
|
|
599
840
|
if (options.optional) return false;
|
|
600
|
-
|
|
841
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
842
|
+
const hints = await generateHints(this, selectorList, "click");
|
|
843
|
+
throw new ElementNotFoundError(selector, hints);
|
|
601
844
|
}
|
|
602
845
|
await this.scrollIntoView(element.nodeId);
|
|
603
846
|
const submitResult = await this.evaluateInFrame(
|
|
@@ -633,7 +876,9 @@ var Page = class {
|
|
|
633
876
|
const element = await this.findElement(selector, options);
|
|
634
877
|
if (!element) {
|
|
635
878
|
if (options.optional) return false;
|
|
636
|
-
|
|
879
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
880
|
+
const hints = await generateHints(this, selectorList, "fill");
|
|
881
|
+
throw new ElementNotFoundError(selector, hints);
|
|
637
882
|
}
|
|
638
883
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
639
884
|
if (clear) {
|
|
@@ -711,7 +956,9 @@ var Page = class {
|
|
|
711
956
|
const element = await this.findElement(selector, options);
|
|
712
957
|
if (!element) {
|
|
713
958
|
if (options.optional) return false;
|
|
714
|
-
|
|
959
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
960
|
+
const hints = await generateHints(this, selectorList, "select");
|
|
961
|
+
throw new ElementNotFoundError(selector, hints);
|
|
715
962
|
}
|
|
716
963
|
const values = Array.isArray(value) ? value : [value];
|
|
717
964
|
await this.cdp.send("Runtime.evaluate", {
|
|
@@ -772,7 +1019,9 @@ var Page = class {
|
|
|
772
1019
|
const element = await this.findElement(selector, options);
|
|
773
1020
|
if (!element) {
|
|
774
1021
|
if (options.optional) return false;
|
|
775
|
-
|
|
1022
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1023
|
+
const hints = await generateHints(this, selectorList, "check");
|
|
1024
|
+
throw new ElementNotFoundError(selector, hints);
|
|
776
1025
|
}
|
|
777
1026
|
const result = await this.cdp.send("Runtime.evaluate", {
|
|
778
1027
|
expression: `(() => {
|
|
@@ -792,7 +1041,9 @@ var Page = class {
|
|
|
792
1041
|
const element = await this.findElement(selector, options);
|
|
793
1042
|
if (!element) {
|
|
794
1043
|
if (options.optional) return false;
|
|
795
|
-
|
|
1044
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1045
|
+
const hints = await generateHints(this, selectorList, "uncheck");
|
|
1046
|
+
throw new ElementNotFoundError(selector, hints);
|
|
796
1047
|
}
|
|
797
1048
|
const result = await this.cdp.send("Runtime.evaluate", {
|
|
798
1049
|
expression: `(() => {
|
|
@@ -812,13 +1063,40 @@ var Page = class {
|
|
|
812
1063
|
* - 'auto' (default): Attempt to detect navigation for 1 second, then assume client-side handling
|
|
813
1064
|
* - true: Wait for full navigation (traditional forms)
|
|
814
1065
|
* - false: Return immediately (AJAX forms where you'll wait for something else)
|
|
1066
|
+
*
|
|
1067
|
+
* When targeting a <form> element directly, uses form.requestSubmit() which fires
|
|
1068
|
+
* the submit event and triggers HTML5 validation.
|
|
815
1069
|
*/
|
|
816
1070
|
async submit(selector, options = {}) {
|
|
817
1071
|
const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
|
|
818
1072
|
const element = await this.findElement(selector, options);
|
|
819
1073
|
if (!element) {
|
|
820
1074
|
if (options.optional) return false;
|
|
821
|
-
|
|
1075
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1076
|
+
const hints = await generateHints(this, selectorList, "submit");
|
|
1077
|
+
throw new ElementNotFoundError(selector, hints);
|
|
1078
|
+
}
|
|
1079
|
+
const isFormElement = await this.evaluateInFrame(
|
|
1080
|
+
`(() => {
|
|
1081
|
+
const el = document.querySelector(${JSON.stringify(element.selector)});
|
|
1082
|
+
return el instanceof HTMLFormElement;
|
|
1083
|
+
})()`
|
|
1084
|
+
);
|
|
1085
|
+
if (isFormElement.result.value) {
|
|
1086
|
+
await this.evaluateInFrame(
|
|
1087
|
+
`(() => {
|
|
1088
|
+
const form = document.querySelector(${JSON.stringify(element.selector)});
|
|
1089
|
+
if (form && form instanceof HTMLFormElement) {
|
|
1090
|
+
form.requestSubmit();
|
|
1091
|
+
}
|
|
1092
|
+
})()`
|
|
1093
|
+
);
|
|
1094
|
+
if (shouldWait === true) {
|
|
1095
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT });
|
|
1096
|
+
} else if (shouldWait === "auto") {
|
|
1097
|
+
await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep2(500)]);
|
|
1098
|
+
}
|
|
1099
|
+
return true;
|
|
822
1100
|
}
|
|
823
1101
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
824
1102
|
if (method.includes("enter")) {
|
|
@@ -889,7 +1167,9 @@ var Page = class {
|
|
|
889
1167
|
const element = await this.findElement(selector, options);
|
|
890
1168
|
if (!element) {
|
|
891
1169
|
if (options.optional) return false;
|
|
892
|
-
|
|
1170
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1171
|
+
const hints = await generateHints(this, selectorList, "focus");
|
|
1172
|
+
throw new ElementNotFoundError(selector, hints);
|
|
893
1173
|
}
|
|
894
1174
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
895
1175
|
return true;
|
|
@@ -902,7 +1182,9 @@ var Page = class {
|
|
|
902
1182
|
const element = await this.findElement(selector, options);
|
|
903
1183
|
if (!element) {
|
|
904
1184
|
if (options.optional) return false;
|
|
905
|
-
|
|
1185
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
1186
|
+
const hints = await generateHints(this, selectorList, "hover");
|
|
1187
|
+
throw new ElementNotFoundError(selector, hints);
|
|
906
1188
|
}
|
|
907
1189
|
await this.scrollIntoView(element.nodeId);
|
|
908
1190
|
const box = await this.getBoxModel(element.nodeId);
|
|
@@ -1857,6 +2139,7 @@ var Page = class {
|
|
|
1857
2139
|
async findElement(selectors, options = {}) {
|
|
1858
2140
|
const { timeout = DEFAULT_TIMEOUT } = options;
|
|
1859
2141
|
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
2142
|
+
this._lastMatchedSelector = void 0;
|
|
1860
2143
|
for (const selector of selectorList) {
|
|
1861
2144
|
if (selector.startsWith("ref:")) {
|
|
1862
2145
|
const ref = selector.slice(4);
|
|
@@ -1873,6 +2156,7 @@ var Page = class {
|
|
|
1873
2156
|
}
|
|
1874
2157
|
);
|
|
1875
2158
|
if (pushResult.nodeIds?.[0]) {
|
|
2159
|
+
this._lastMatchedSelector = selector;
|
|
1876
2160
|
return {
|
|
1877
2161
|
nodeId: pushResult.nodeIds[0],
|
|
1878
2162
|
backendNodeId,
|
|
@@ -1906,6 +2190,7 @@ var Page = class {
|
|
|
1906
2190
|
"DOM.describeNode",
|
|
1907
2191
|
{ nodeId: queryResult.nodeId }
|
|
1908
2192
|
);
|
|
2193
|
+
this._lastMatchedSelector = result.selector;
|
|
1909
2194
|
return {
|
|
1910
2195
|
nodeId: queryResult.nodeId,
|
|
1911
2196
|
backendNodeId: describeResult2.node.backendNodeId,
|
|
@@ -1933,6 +2218,7 @@ var Page = class {
|
|
|
1933
2218
|
"DOM.describeNode",
|
|
1934
2219
|
{ nodeId: nodeResult.nodeId }
|
|
1935
2220
|
);
|
|
2221
|
+
this._lastMatchedSelector = result.selector;
|
|
1936
2222
|
return {
|
|
1937
2223
|
nodeId: nodeResult.nodeId,
|
|
1938
2224
|
backendNodeId: describeResult.node.backendNodeId,
|
|
@@ -2039,14 +2325,24 @@ var Browser = class _Browser {
|
|
|
2039
2325
|
* Get or create a page by name
|
|
2040
2326
|
* If no name is provided, returns the first available page or creates a new one
|
|
2041
2327
|
*/
|
|
2042
|
-
async page(name) {
|
|
2328
|
+
async page(name, options) {
|
|
2043
2329
|
const pageName = name ?? "default";
|
|
2044
2330
|
const cached = this.pages.get(pageName);
|
|
2045
2331
|
if (cached) return cached;
|
|
2046
2332
|
const targets = await this.cdp.send("Target.getTargets");
|
|
2047
2333
|
const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
2048
2334
|
let targetId;
|
|
2049
|
-
if (
|
|
2335
|
+
if (options?.targetId) {
|
|
2336
|
+
const targetExists = pageTargets.some((t) => t.targetId === options.targetId);
|
|
2337
|
+
if (targetExists) {
|
|
2338
|
+
targetId = options.targetId;
|
|
2339
|
+
} else {
|
|
2340
|
+
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
2341
|
+
targetId = pageTargets.length > 0 ? pageTargets[0].targetId : (await this.cdp.send("Target.createTarget", {
|
|
2342
|
+
url: "about:blank"
|
|
2343
|
+
})).targetId;
|
|
2344
|
+
}
|
|
2345
|
+
} else if (pageTargets.length > 0) {
|
|
2050
2346
|
targetId = pageTargets[0].targetId;
|
|
2051
2347
|
} else {
|
|
2052
2348
|
const result = await this.cdp.send("Target.createTarget", {
|
|
@@ -2055,7 +2351,7 @@ var Browser = class _Browser {
|
|
|
2055
2351
|
targetId = result.targetId;
|
|
2056
2352
|
}
|
|
2057
2353
|
await this.cdp.attachToTarget(targetId);
|
|
2058
|
-
const page = new Page(this.cdp);
|
|
2354
|
+
const page = new Page(this.cdp, targetId);
|
|
2059
2355
|
await page.init();
|
|
2060
2356
|
this.pages.set(pageName, page);
|
|
2061
2357
|
return page;
|
|
@@ -2068,7 +2364,7 @@ var Browser = class _Browser {
|
|
|
2068
2364
|
url
|
|
2069
2365
|
});
|
|
2070
2366
|
await this.cdp.attachToTarget(result.targetId);
|
|
2071
|
-
const page = new Page(this.cdp);
|
|
2367
|
+
const page = new Page(this.cdp, result.targetId);
|
|
2072
2368
|
await page.init();
|
|
2073
2369
|
const name = `page-${this.pages.size + 1}`;
|
|
2074
2370
|
this.pages.set(name, page);
|
|
@@ -2141,13 +2437,12 @@ function connect(options) {
|
|
|
2141
2437
|
|
|
2142
2438
|
export {
|
|
2143
2439
|
RequestInterceptor,
|
|
2440
|
+
DEEP_QUERY_SCRIPT,
|
|
2144
2441
|
waitForElement,
|
|
2145
2442
|
waitForAnyElement,
|
|
2146
2443
|
waitForNavigation,
|
|
2147
2444
|
waitForNetworkIdle,
|
|
2148
|
-
|
|
2149
|
-
TimeoutError,
|
|
2150
|
-
NavigationError,
|
|
2445
|
+
fuzzyMatchElements,
|
|
2151
2446
|
Page,
|
|
2152
2447
|
Browser,
|
|
2153
2448
|
connect
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
// src/browser/types.ts
|
|
2
|
+
var ElementNotFoundError = class extends Error {
|
|
3
|
+
selectors;
|
|
4
|
+
hints;
|
|
5
|
+
constructor(selectors, hints) {
|
|
6
|
+
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
7
|
+
super(`Element not found: ${selectorList.join(", ")}`);
|
|
8
|
+
this.name = "ElementNotFoundError";
|
|
9
|
+
this.selectors = selectorList;
|
|
10
|
+
this.hints = hints;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var TimeoutError = class extends Error {
|
|
14
|
+
constructor(message = "Operation timed out") {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "TimeoutError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var NavigationError = class extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "NavigationError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
1
26
|
// src/actions/executor.ts
|
|
2
27
|
var DEFAULT_TIMEOUT = 3e4;
|
|
3
28
|
var BatchExecutor = class {
|
|
@@ -29,13 +54,15 @@ var BatchExecutor = class {
|
|
|
29
54
|
});
|
|
30
55
|
} catch (error) {
|
|
31
56
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
57
|
+
const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
|
|
32
58
|
results.push({
|
|
33
59
|
index: i,
|
|
34
60
|
action: step.action,
|
|
35
61
|
selector: step.selector,
|
|
36
62
|
success: false,
|
|
37
63
|
durationMs: Date.now() - stepStart,
|
|
38
|
-
error: errorMessage
|
|
64
|
+
error: errorMessage,
|
|
65
|
+
hints
|
|
39
66
|
});
|
|
40
67
|
if (onFail === "stop" && !step.optional) {
|
|
41
68
|
return {
|
|
@@ -231,10 +258,12 @@ var BatchExecutor = class {
|
|
|
231
258
|
}
|
|
232
259
|
}
|
|
233
260
|
/**
|
|
234
|
-
* Get the
|
|
235
|
-
*
|
|
261
|
+
* Get the actual selector that matched the element.
|
|
262
|
+
* Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
|
|
236
263
|
*/
|
|
237
264
|
getUsedSelector(selector) {
|
|
265
|
+
const matched = this.page.getLastMatchedSelector();
|
|
266
|
+
if (matched) return matched;
|
|
238
267
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
239
268
|
}
|
|
240
269
|
};
|
|
@@ -246,6 +275,9 @@ function addBatchToPage(page) {
|
|
|
246
275
|
}
|
|
247
276
|
|
|
248
277
|
export {
|
|
278
|
+
ElementNotFoundError,
|
|
279
|
+
TimeoutError,
|
|
280
|
+
NavigationError,
|
|
249
281
|
BatchExecutor,
|
|
250
282
|
addBatchToPage
|
|
251
283
|
};
|