browser-pilot 0.0.16 → 0.0.17
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 +22 -0
- package/dist/actions.cjs +797 -69
- package/dist/actions.d.cts +101 -4
- package/dist/actions.d.ts +101 -4
- package/dist/actions.mjs +17 -1
- package/dist/{browser-ZCR6AA4D.mjs → browser-4ZHNAQR5.mjs} +2 -2
- package/dist/browser.cjs +1238 -72
- package/dist/browser.d.cts +229 -5
- package/dist/browser.d.ts +229 -5
- package/dist/browser.mjs +36 -4
- package/dist/{chunk-NNEHWWHL.mjs → chunk-FEEGNSHB.mjs} +584 -4
- package/dist/{chunk-TJ5B56NV.mjs → chunk-IRLHCVNH.mjs} +1 -1
- package/dist/chunk-MIJ7UIKB.mjs +96 -0
- package/dist/{chunk-6GBYX7C2.mjs → chunk-MRY3HRFJ.mjs} +799 -353
- package/dist/chunk-OIHU7OFY.mjs +91 -0
- package/dist/{chunk-V3VLBQAM.mjs → chunk-ZDODXEBD.mjs} +586 -69
- package/dist/cli.mjs +756 -174
- package/dist/combobox-RAKBA2BW.mjs +6 -0
- package/dist/index.cjs +1539 -71
- package/dist/index.d.cts +56 -5
- package/dist/index.d.ts +56 -5
- package/dist/index.mjs +189 -2
- package/dist/{page-IUUTJ3SW.mjs → page-SD64DY3F.mjs} +1 -1
- package/dist/{types-BzM-IfsL.d.ts → types-B_v62K7C.d.ts} +146 -2
- package/dist/{types-BflRmiDz.d.cts → types-Yuybzq53.d.cts} +146 -2
- package/dist/upload-E6MCC2OF.mjs +6 -0
- package/package.json +10 -3
|
@@ -10,10 +10,13 @@ import {
|
|
|
10
10
|
ActionabilityError,
|
|
11
11
|
BatchExecutor,
|
|
12
12
|
ElementNotFoundError,
|
|
13
|
+
NetworkResponseTracker,
|
|
13
14
|
TimeoutError,
|
|
15
|
+
captureStateSignature,
|
|
14
16
|
ensureActionable,
|
|
17
|
+
evaluateOutcome,
|
|
15
18
|
generateHints
|
|
16
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-ZDODXEBD.mjs";
|
|
17
20
|
|
|
18
21
|
// src/audio/encoding.ts
|
|
19
22
|
function bufferToBase64(data) {
|
|
@@ -2093,6 +2096,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
2093
2096
|
});
|
|
2094
2097
|
}
|
|
2095
2098
|
|
|
2099
|
+
// src/browser/delta.ts
|
|
2100
|
+
function extractPageState(url, title, snapshot, forms, pageText) {
|
|
2101
|
+
const headings = [];
|
|
2102
|
+
const buttons = [];
|
|
2103
|
+
const alerts = [];
|
|
2104
|
+
function walkNodes(nodes) {
|
|
2105
|
+
for (const node of nodes) {
|
|
2106
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
2107
|
+
if (role === "heading" && node.name) {
|
|
2108
|
+
headings.push(node.name);
|
|
2109
|
+
}
|
|
2110
|
+
if ((role === "button" || role === "link") && node.name) {
|
|
2111
|
+
const disabled = node.disabled ?? false;
|
|
2112
|
+
buttons.push({ text: node.name, disabled, ref: node.ref });
|
|
2113
|
+
}
|
|
2114
|
+
if (role === "alert" && node.name) {
|
|
2115
|
+
alerts.push(node.name);
|
|
2116
|
+
}
|
|
2117
|
+
if (node.children) {
|
|
2118
|
+
walkNodes(node.children);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
walkNodes(snapshot.accessibilityTree);
|
|
2123
|
+
const formFields = forms.map((f) => ({
|
|
2124
|
+
label: f.label,
|
|
2125
|
+
name: f.name,
|
|
2126
|
+
id: f.id,
|
|
2127
|
+
value: f.value,
|
|
2128
|
+
type: f.type
|
|
2129
|
+
}));
|
|
2130
|
+
return {
|
|
2131
|
+
url,
|
|
2132
|
+
title,
|
|
2133
|
+
headings,
|
|
2134
|
+
formFields,
|
|
2135
|
+
buttons,
|
|
2136
|
+
alerts,
|
|
2137
|
+
visibleText: pageText.slice(0, 3e3)
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
function computeDelta(before, after) {
|
|
2141
|
+
const changes = [];
|
|
2142
|
+
if (before.url !== after.url) {
|
|
2143
|
+
changes.push({ kind: "url", before: before.url, after: after.url });
|
|
2144
|
+
}
|
|
2145
|
+
if (before.title !== after.title) {
|
|
2146
|
+
changes.push({ kind: "title", before: before.title, after: after.title });
|
|
2147
|
+
}
|
|
2148
|
+
const beforeHeadings = new Set(before.headings);
|
|
2149
|
+
const afterHeadings = new Set(after.headings);
|
|
2150
|
+
for (const h of after.headings) {
|
|
2151
|
+
if (!beforeHeadings.has(h)) {
|
|
2152
|
+
changes.push({ kind: "heading_added", after: h });
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
for (const h of before.headings) {
|
|
2156
|
+
if (!afterHeadings.has(h)) {
|
|
2157
|
+
changes.push({ kind: "heading_removed", before: h });
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const beforeFieldMap = new Map(
|
|
2161
|
+
before.formFields.map((f) => [f.id ?? f.name ?? f.label ?? "", f])
|
|
2162
|
+
);
|
|
2163
|
+
for (const af of after.formFields) {
|
|
2164
|
+
const key = af.id ?? af.name ?? af.label ?? "";
|
|
2165
|
+
const bf = beforeFieldMap.get(key);
|
|
2166
|
+
if (bf && JSON.stringify(bf.value) !== JSON.stringify(af.value)) {
|
|
2167
|
+
changes.push({
|
|
2168
|
+
kind: "field_changed",
|
|
2169
|
+
before: String(bf.value ?? ""),
|
|
2170
|
+
after: String(af.value ?? ""),
|
|
2171
|
+
detail: af.label ?? af.name ?? af.id ?? key
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const beforeBtnMap = new Map(before.buttons.map((b) => [b.text, b]));
|
|
2176
|
+
for (const ab of after.buttons) {
|
|
2177
|
+
const bb = beforeBtnMap.get(ab.text);
|
|
2178
|
+
if (bb && bb.disabled !== ab.disabled) {
|
|
2179
|
+
changes.push({
|
|
2180
|
+
kind: "button_changed",
|
|
2181
|
+
detail: ab.text,
|
|
2182
|
+
before: bb.disabled ? "disabled" : "enabled",
|
|
2183
|
+
after: ab.disabled ? "disabled" : "enabled"
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
const beforeAlerts = new Set(before.alerts);
|
|
2188
|
+
const afterAlerts = new Set(after.alerts);
|
|
2189
|
+
for (const a of after.alerts) {
|
|
2190
|
+
if (!beforeAlerts.has(a)) {
|
|
2191
|
+
changes.push({ kind: "alert_added", after: a });
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
for (const a of before.alerts) {
|
|
2195
|
+
if (!afterAlerts.has(a)) {
|
|
2196
|
+
changes.push({ kind: "alert_removed", before: a });
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return {
|
|
2200
|
+
changes,
|
|
2201
|
+
before,
|
|
2202
|
+
after,
|
|
2203
|
+
hasChanges: changes.length > 0
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2096
2207
|
// src/browser/keyboard.ts
|
|
2097
2208
|
var US_KEYBOARD = {
|
|
2098
2209
|
// Letters (lowercase)
|
|
@@ -2252,8 +2363,118 @@ function parseShortcut(combo) {
|
|
|
2252
2363
|
return { modifiers, key };
|
|
2253
2364
|
}
|
|
2254
2365
|
|
|
2366
|
+
// src/browser/review.ts
|
|
2367
|
+
function extractReview(url, title, snapshot, forms, pageText) {
|
|
2368
|
+
const headings = [];
|
|
2369
|
+
const alerts = [];
|
|
2370
|
+
const statusLabels = [];
|
|
2371
|
+
const keyValues = [];
|
|
2372
|
+
const tables = [];
|
|
2373
|
+
const summaryCards = [];
|
|
2374
|
+
function walkNodes(nodes, parentHeading) {
|
|
2375
|
+
let currentHeading = parentHeading;
|
|
2376
|
+
for (const node of nodes) {
|
|
2377
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
2378
|
+
if (role === "heading" && node.name) {
|
|
2379
|
+
headings.push(node.name);
|
|
2380
|
+
currentHeading = node.name;
|
|
2381
|
+
}
|
|
2382
|
+
if (role === "alert" && node.name) {
|
|
2383
|
+
alerts.push(node.name);
|
|
2384
|
+
}
|
|
2385
|
+
if (role === "status" && node.name) {
|
|
2386
|
+
statusLabels.push(node.name);
|
|
2387
|
+
}
|
|
2388
|
+
if (role === "table" || role === "grid") {
|
|
2389
|
+
const table = extractTableFromNode(node);
|
|
2390
|
+
if (table) tables.push(table);
|
|
2391
|
+
}
|
|
2392
|
+
if ((role === "definition" || role === "term") && node.name) {
|
|
2393
|
+
if (role === "term") {
|
|
2394
|
+
keyValues.push({ key: node.name, value: "" });
|
|
2395
|
+
} else if (role === "definition" && keyValues.length > 0) {
|
|
2396
|
+
const last = keyValues[keyValues.length - 1];
|
|
2397
|
+
if (!last.value) last.value = node.name;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
if (node.children) {
|
|
2401
|
+
walkNodes(node.children, currentHeading);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
walkNodes(snapshot.accessibilityTree);
|
|
2406
|
+
const textKvPairs = extractKeyValueFromText(pageText);
|
|
2407
|
+
keyValues.push(...textKvPairs);
|
|
2408
|
+
const formEntries = forms.map((f) => ({
|
|
2409
|
+
label: f.label,
|
|
2410
|
+
value: f.value,
|
|
2411
|
+
type: f.type,
|
|
2412
|
+
disabled: f.disabled
|
|
2413
|
+
}));
|
|
2414
|
+
return {
|
|
2415
|
+
url,
|
|
2416
|
+
title,
|
|
2417
|
+
headings,
|
|
2418
|
+
forms: formEntries,
|
|
2419
|
+
alerts,
|
|
2420
|
+
summaryCards,
|
|
2421
|
+
tables,
|
|
2422
|
+
keyValues,
|
|
2423
|
+
statusLabels
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
function extractTableFromNode(node) {
|
|
2427
|
+
const headers = [];
|
|
2428
|
+
const rows = [];
|
|
2429
|
+
function findRows(n) {
|
|
2430
|
+
const role = n.role?.toLowerCase() ?? "";
|
|
2431
|
+
if (role === "columnheader" && n.name) {
|
|
2432
|
+
headers.push(n.name);
|
|
2433
|
+
}
|
|
2434
|
+
if (role === "row") {
|
|
2435
|
+
const cells = [];
|
|
2436
|
+
if (n.children) {
|
|
2437
|
+
for (const child of n.children) {
|
|
2438
|
+
const childRole = child.role?.toLowerCase() ?? "";
|
|
2439
|
+
if ((childRole === "cell" || childRole === "gridcell") && child.name) {
|
|
2440
|
+
cells.push(child.name);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
if (cells.length > 0) rows.push(cells);
|
|
2445
|
+
}
|
|
2446
|
+
if (n.children) {
|
|
2447
|
+
for (const child of n.children) findRows(child);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
findRows(node);
|
|
2451
|
+
if (rows.length === 0) return null;
|
|
2452
|
+
return { headers, rows };
|
|
2453
|
+
}
|
|
2454
|
+
function extractKeyValueFromText(text) {
|
|
2455
|
+
const pairs = [];
|
|
2456
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2457
|
+
for (const line of lines) {
|
|
2458
|
+
const match = line.match(/^([A-Z][A-Za-z0-9 ]{1,30})[:—]\s+(.+)$/);
|
|
2459
|
+
if (match) {
|
|
2460
|
+
pairs.push({ key: match[1].trim(), value: match[2].trim() });
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return pairs.slice(0, 20);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2255
2466
|
// src/browser/page.ts
|
|
2256
2467
|
var DEFAULT_TIMEOUT = 3e4;
|
|
2468
|
+
function normalizeAXCheckedValue(value) {
|
|
2469
|
+
if (typeof value === "boolean") {
|
|
2470
|
+
return value;
|
|
2471
|
+
}
|
|
2472
|
+
if (typeof value === "string") {
|
|
2473
|
+
if (value === "true") return true;
|
|
2474
|
+
if (value === "false") return false;
|
|
2475
|
+
}
|
|
2476
|
+
return void 0;
|
|
2477
|
+
}
|
|
2257
2478
|
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
2258
2479
|
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
2259
2480
|
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
@@ -4039,7 +4260,9 @@ var Page = class {
|
|
|
4039
4260
|
}
|
|
4040
4261
|
}
|
|
4041
4262
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
4042
|
-
const checked =
|
|
4263
|
+
const checked = normalizeAXCheckedValue(
|
|
4264
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
4265
|
+
);
|
|
4043
4266
|
return {
|
|
4044
4267
|
role,
|
|
4045
4268
|
name,
|
|
@@ -4095,7 +4318,9 @@ var Page = class {
|
|
|
4095
4318
|
const ref = nodeRefs.get(node.nodeId);
|
|
4096
4319
|
const name = node.name?.value ?? "";
|
|
4097
4320
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
4098
|
-
const checked =
|
|
4321
|
+
const checked = normalizeAXCheckedValue(
|
|
4322
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
4323
|
+
);
|
|
4099
4324
|
const value = node.value?.value;
|
|
4100
4325
|
const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
|
|
4101
4326
|
interactiveElements.push({
|
|
@@ -4162,6 +4387,45 @@ var Page = class {
|
|
|
4162
4387
|
}
|
|
4163
4388
|
}
|
|
4164
4389
|
}
|
|
4390
|
+
// ============ Delta & Review ============
|
|
4391
|
+
/**
|
|
4392
|
+
* Capture current page state for delta comparison.
|
|
4393
|
+
* Call before an action, then call delta() again after and use computeDelta().
|
|
4394
|
+
*/
|
|
4395
|
+
async captureState() {
|
|
4396
|
+
const [url, title, snapshot, forms, text] = await Promise.all([
|
|
4397
|
+
this.url(),
|
|
4398
|
+
this.title(),
|
|
4399
|
+
this.snapshot(),
|
|
4400
|
+
this.forms(),
|
|
4401
|
+
this.text()
|
|
4402
|
+
]);
|
|
4403
|
+
return extractPageState(url, title, snapshot, forms, text);
|
|
4404
|
+
}
|
|
4405
|
+
/**
|
|
4406
|
+
* Compute what changed between two page states.
|
|
4407
|
+
* If no arguments: captures current state and returns it (for use as "before").
|
|
4408
|
+
* If one argument (before state): captures current state and computes delta.
|
|
4409
|
+
*/
|
|
4410
|
+
async delta(before) {
|
|
4411
|
+
const currentState = await this.captureState();
|
|
4412
|
+
if (!before) return currentState;
|
|
4413
|
+
return computeDelta(before, currentState);
|
|
4414
|
+
}
|
|
4415
|
+
/**
|
|
4416
|
+
* Extract structured review surface from the current page.
|
|
4417
|
+
* Returns headings, form values, alerts, key-value pairs, tables, and status labels.
|
|
4418
|
+
*/
|
|
4419
|
+
async review() {
|
|
4420
|
+
const [url, title, snapshot, forms, text] = await Promise.all([
|
|
4421
|
+
this.url(),
|
|
4422
|
+
this.title(),
|
|
4423
|
+
this.snapshot(),
|
|
4424
|
+
this.forms(),
|
|
4425
|
+
this.text()
|
|
4426
|
+
]);
|
|
4427
|
+
return extractReview(url, title, snapshot, forms, text);
|
|
4428
|
+
}
|
|
4165
4429
|
// ============ Batch Execution ============
|
|
4166
4430
|
/**
|
|
4167
4431
|
* Execute a batch of steps
|
|
@@ -5587,6 +5851,310 @@ function connect(options) {
|
|
|
5587
5851
|
return Browser.connect(options);
|
|
5588
5852
|
}
|
|
5589
5853
|
|
|
5854
|
+
// src/browser/fingerprint.ts
|
|
5855
|
+
function createFingerprint(node, context) {
|
|
5856
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
5857
|
+
const name = node.name ?? "";
|
|
5858
|
+
let valueShape = "";
|
|
5859
|
+
if (node.value !== void 0) {
|
|
5860
|
+
valueShape = typeof node.value === "string" ? "text" : typeof node.value === "number" ? "number" : typeof node.value === "boolean" ? "boolean" : "other";
|
|
5861
|
+
}
|
|
5862
|
+
const stableAttrs = {};
|
|
5863
|
+
if (node.properties) {
|
|
5864
|
+
for (const key of ["id", "name", "type", "aria-label"]) {
|
|
5865
|
+
const val = node.properties[key];
|
|
5866
|
+
if (val !== void 0 && val !== null) {
|
|
5867
|
+
stableAttrs[key] = String(val);
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
return {
|
|
5872
|
+
role,
|
|
5873
|
+
name,
|
|
5874
|
+
valueShape,
|
|
5875
|
+
label: name,
|
|
5876
|
+
// label is typically the accessible name
|
|
5877
|
+
stableAttrs,
|
|
5878
|
+
nearestHeading: context.nearestHeading,
|
|
5879
|
+
siblingIndex: context.siblingIndex,
|
|
5880
|
+
sectionPath: [...context.headingTrail]
|
|
5881
|
+
};
|
|
5882
|
+
}
|
|
5883
|
+
function fingerprintKey(fp) {
|
|
5884
|
+
const parts = [fp.role, fp.name, fp.sectionPath.join(">")];
|
|
5885
|
+
if (fp.stableAttrs["id"]) parts.push(`id=${fp.stableAttrs["id"]}`);
|
|
5886
|
+
if (fp.stableAttrs["name"]) parts.push(`name=${fp.stableAttrs["name"]}`);
|
|
5887
|
+
return parts.join("|");
|
|
5888
|
+
}
|
|
5889
|
+
function fingerprintSimilarity(a, b) {
|
|
5890
|
+
let score = 0;
|
|
5891
|
+
let weight = 0;
|
|
5892
|
+
weight += 3;
|
|
5893
|
+
if (a.role === b.role) score += 3;
|
|
5894
|
+
else return 0;
|
|
5895
|
+
weight += 5;
|
|
5896
|
+
if (a.name && b.name && a.name === b.name) score += 5;
|
|
5897
|
+
else if (a.name && b.name && a.name.toLowerCase() === b.name.toLowerCase()) score += 4;
|
|
5898
|
+
weight += 3;
|
|
5899
|
+
const pathA = a.sectionPath.join(">");
|
|
5900
|
+
const pathB = b.sectionPath.join(">");
|
|
5901
|
+
if (pathA === pathB) score += 3;
|
|
5902
|
+
else if (pathA && pathB && (pathA.includes(pathB) || pathB.includes(pathA))) score += 1;
|
|
5903
|
+
const attrKeys = /* @__PURE__ */ new Set([...Object.keys(a.stableAttrs), ...Object.keys(b.stableAttrs)]);
|
|
5904
|
+
for (const key of attrKeys) {
|
|
5905
|
+
weight += 2;
|
|
5906
|
+
if (a.stableAttrs[key] && b.stableAttrs[key] && a.stableAttrs[key] === b.stableAttrs[key]) {
|
|
5907
|
+
score += 2;
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
weight += 1;
|
|
5911
|
+
if (a.siblingIndex === b.siblingIndex) score += 1;
|
|
5912
|
+
return score / weight;
|
|
5913
|
+
}
|
|
5914
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
5915
|
+
"button",
|
|
5916
|
+
"link",
|
|
5917
|
+
"textbox",
|
|
5918
|
+
"checkbox",
|
|
5919
|
+
"radio",
|
|
5920
|
+
"combobox",
|
|
5921
|
+
"listbox",
|
|
5922
|
+
"menuitem",
|
|
5923
|
+
"tab",
|
|
5924
|
+
"switch",
|
|
5925
|
+
"searchbox",
|
|
5926
|
+
"spinbutton",
|
|
5927
|
+
"slider"
|
|
5928
|
+
]);
|
|
5929
|
+
function buildFingerprintMap(nodes) {
|
|
5930
|
+
const map = /* @__PURE__ */ new Map();
|
|
5931
|
+
function walk(nodeList, headingTrail, nearestHeading) {
|
|
5932
|
+
const roleCounts = /* @__PURE__ */ new Map();
|
|
5933
|
+
for (const node of nodeList) {
|
|
5934
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
5935
|
+
let currentHeadingTrail = headingTrail;
|
|
5936
|
+
let currentNearestHeading = nearestHeading;
|
|
5937
|
+
if (role === "heading" && node.name) {
|
|
5938
|
+
currentHeadingTrail = [...headingTrail, node.name];
|
|
5939
|
+
currentNearestHeading = node.name;
|
|
5940
|
+
}
|
|
5941
|
+
if (INTERACTIVE_ROLES.has(role) && node.ref) {
|
|
5942
|
+
const siblingCount = roleCounts.get(role) ?? 0;
|
|
5943
|
+
roleCounts.set(role, siblingCount + 1);
|
|
5944
|
+
const fp = createFingerprint(node, {
|
|
5945
|
+
headingTrail: currentHeadingTrail,
|
|
5946
|
+
siblingIndex: siblingCount,
|
|
5947
|
+
nearestHeading: currentNearestHeading
|
|
5948
|
+
});
|
|
5949
|
+
map.set(node.ref, fp);
|
|
5950
|
+
}
|
|
5951
|
+
if (node.children) {
|
|
5952
|
+
walk(node.children, currentHeadingTrail, currentNearestHeading);
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
}
|
|
5956
|
+
walk(nodes, [], "");
|
|
5957
|
+
return map;
|
|
5958
|
+
}
|
|
5959
|
+
function recoverStaleRef(staleFingerprint, currentFingerprints, threshold = 0.7) {
|
|
5960
|
+
let bestRef = null;
|
|
5961
|
+
let bestScore = 0;
|
|
5962
|
+
let secondBestScore = 0;
|
|
5963
|
+
for (const [ref, fp] of currentFingerprints) {
|
|
5964
|
+
const similarity = fingerprintSimilarity(staleFingerprint, fp);
|
|
5965
|
+
if (similarity > bestScore) {
|
|
5966
|
+
secondBestScore = bestScore;
|
|
5967
|
+
bestScore = similarity;
|
|
5968
|
+
bestRef = ref;
|
|
5969
|
+
} else if (similarity > secondBestScore) {
|
|
5970
|
+
secondBestScore = similarity;
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5973
|
+
if (!bestRef || bestScore < threshold) return null;
|
|
5974
|
+
if (secondBestScore > 0 && bestScore - secondBestScore < 0.15) return null;
|
|
5975
|
+
return { ref: bestRef, confidence: bestScore };
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
// src/browser/overlay-detect.ts
|
|
5979
|
+
async function detectOverlay(page) {
|
|
5980
|
+
const result = await page.evaluate(`(() => {
|
|
5981
|
+
// Check for role="dialog" or role="alertdialog"
|
|
5982
|
+
const dialogs = document.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog[open]');
|
|
5983
|
+
for (const d of dialogs) {
|
|
5984
|
+
if (d.offsetParent !== null || getComputedStyle(d).display !== 'none') {
|
|
5985
|
+
return {
|
|
5986
|
+
hasOverlay: true,
|
|
5987
|
+
overlaySelector: d.id ? '#' + d.id : (d.getAttribute('role') ? '[role="' + d.getAttribute('role') + '"]' : 'dialog'),
|
|
5988
|
+
overlayText: (d.textContent || '').trim().slice(0, 200),
|
|
5989
|
+
};
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
// Check for fixed/absolute positioned elements with high z-index that look like modals
|
|
5994
|
+
const allElements = document.querySelectorAll('*');
|
|
5995
|
+
for (const el of allElements) {
|
|
5996
|
+
const style = getComputedStyle(el);
|
|
5997
|
+
if (
|
|
5998
|
+
(style.position === 'fixed' || style.position === 'absolute') &&
|
|
5999
|
+
parseInt(style.zIndex || '0', 10) > 999 &&
|
|
6000
|
+
el.offsetWidth > 100 &&
|
|
6001
|
+
el.offsetHeight > 100 &&
|
|
6002
|
+
style.display !== 'none' &&
|
|
6003
|
+
style.visibility !== 'hidden'
|
|
6004
|
+
) {
|
|
6005
|
+
const text = (el.textContent || '').trim();
|
|
6006
|
+
if (text.length > 10) {
|
|
6007
|
+
return {
|
|
6008
|
+
hasOverlay: true,
|
|
6009
|
+
overlaySelector: el.id ? '#' + el.id : null,
|
|
6010
|
+
overlayText: text.slice(0, 200),
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
|
|
6016
|
+
return { hasOverlay: false };
|
|
6017
|
+
})()`);
|
|
6018
|
+
return result ?? { hasOverlay: false };
|
|
6019
|
+
}
|
|
6020
|
+
|
|
6021
|
+
// src/browser/safe-submit.ts
|
|
6022
|
+
async function submitAndVerify(page, options) {
|
|
6023
|
+
const {
|
|
6024
|
+
selector,
|
|
6025
|
+
method = "enter+click",
|
|
6026
|
+
expectAny,
|
|
6027
|
+
expectAll,
|
|
6028
|
+
failIf,
|
|
6029
|
+
dangerous = false,
|
|
6030
|
+
timeout = 3e4,
|
|
6031
|
+
waitForNavigation: waitForNavigation2 = "auto"
|
|
6032
|
+
} = options;
|
|
6033
|
+
const startTime = Date.now();
|
|
6034
|
+
const allConditions = [...expectAny ?? [], ...expectAll ?? [], ...failIf ?? []];
|
|
6035
|
+
const needsNetwork = allConditions.some((c) => c.kind === "networkResponse");
|
|
6036
|
+
const needsSignature = allConditions.some((c) => c.kind === "stateSignatureChanges");
|
|
6037
|
+
let networkTracker;
|
|
6038
|
+
let beforeSignature;
|
|
6039
|
+
if (needsNetwork) {
|
|
6040
|
+
networkTracker = new NetworkResponseTracker();
|
|
6041
|
+
networkTracker.start(page.cdpClient);
|
|
6042
|
+
}
|
|
6043
|
+
if (needsSignature) {
|
|
6044
|
+
beforeSignature = await captureStateSignature(page);
|
|
6045
|
+
}
|
|
6046
|
+
try {
|
|
6047
|
+
await page.submit(selector, {
|
|
6048
|
+
timeout,
|
|
6049
|
+
method,
|
|
6050
|
+
waitForNavigation: waitForNavigation2
|
|
6051
|
+
});
|
|
6052
|
+
if (networkTracker) networkTracker.stop(page.cdpClient);
|
|
6053
|
+
if (allConditions.length === 0) {
|
|
6054
|
+
return {
|
|
6055
|
+
submitted: true,
|
|
6056
|
+
outcomeStatus: "success",
|
|
6057
|
+
matchedConditions: [],
|
|
6058
|
+
retrySafe: !dangerous,
|
|
6059
|
+
durationMs: Date.now() - startTime
|
|
6060
|
+
};
|
|
6061
|
+
}
|
|
6062
|
+
const outcome = await evaluateOutcome(page, {
|
|
6063
|
+
expectAny,
|
|
6064
|
+
expectAll,
|
|
6065
|
+
failIf,
|
|
6066
|
+
dangerous,
|
|
6067
|
+
networkTracker,
|
|
6068
|
+
beforeSignature
|
|
6069
|
+
});
|
|
6070
|
+
return {
|
|
6071
|
+
submitted: true,
|
|
6072
|
+
outcomeStatus: outcome.outcomeStatus,
|
|
6073
|
+
matchedConditions: outcome.matchedConditions,
|
|
6074
|
+
retrySafe: outcome.retrySafe,
|
|
6075
|
+
durationMs: Date.now() - startTime
|
|
6076
|
+
};
|
|
6077
|
+
} catch (error) {
|
|
6078
|
+
if (networkTracker) networkTracker.stop(page.cdpClient);
|
|
6079
|
+
return {
|
|
6080
|
+
submitted: false,
|
|
6081
|
+
outcomeStatus: "failed",
|
|
6082
|
+
matchedConditions: [],
|
|
6083
|
+
retrySafe: !dangerous,
|
|
6084
|
+
durationMs: Date.now() - startTime,
|
|
6085
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6086
|
+
};
|
|
6087
|
+
}
|
|
6088
|
+
}
|
|
6089
|
+
|
|
6090
|
+
// src/runtime/clock.ts
|
|
6091
|
+
function now() {
|
|
6092
|
+
return Date.now();
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
// src/browser/target-pin.ts
|
|
6096
|
+
function createTargetFingerprint(targetId, url, title) {
|
|
6097
|
+
return {
|
|
6098
|
+
url,
|
|
6099
|
+
title,
|
|
6100
|
+
originalTargetId: targetId,
|
|
6101
|
+
pinnedAt: now()
|
|
6102
|
+
};
|
|
6103
|
+
}
|
|
6104
|
+
function scoreCandidate(candidate, pin) {
|
|
6105
|
+
if (candidate.targetId === pin.originalTargetId) return 1;
|
|
6106
|
+
let score = 0;
|
|
6107
|
+
if (candidate.url && pin.url) {
|
|
6108
|
+
if (candidate.url === pin.url) {
|
|
6109
|
+
score += 0.6;
|
|
6110
|
+
} else {
|
|
6111
|
+
try {
|
|
6112
|
+
const candidateOrigin = new URL(candidate.url).origin;
|
|
6113
|
+
const pinOrigin = new URL(pin.url).origin;
|
|
6114
|
+
if (candidateOrigin === pinOrigin) score += 0.3;
|
|
6115
|
+
} catch {
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
if (candidate.title && pin.title) {
|
|
6120
|
+
if (candidate.title === pin.title) {
|
|
6121
|
+
score += 0.3;
|
|
6122
|
+
} else if (candidate.title.includes(pin.title) || pin.title.includes(candidate.title)) {
|
|
6123
|
+
score += 0.15;
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
if (candidate.type !== "page") score *= 0.5;
|
|
6127
|
+
return Math.min(score, 0.95);
|
|
6128
|
+
}
|
|
6129
|
+
function recoverPinnedTarget(pin, targets, threshold = 0.4) {
|
|
6130
|
+
if (targets.length === 0) return null;
|
|
6131
|
+
let bestTarget = null;
|
|
6132
|
+
let bestScore = 0;
|
|
6133
|
+
for (const target of targets) {
|
|
6134
|
+
const score = scoreCandidate(target, pin);
|
|
6135
|
+
if (score > bestScore) {
|
|
6136
|
+
bestScore = score;
|
|
6137
|
+
bestTarget = target;
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
if (!bestTarget || bestScore < threshold) return null;
|
|
6141
|
+
let method;
|
|
6142
|
+
if (bestTarget.targetId === pin.originalTargetId) {
|
|
6143
|
+
method = "exact";
|
|
6144
|
+
} else if (bestTarget.url === pin.url) {
|
|
6145
|
+
method = "url_match";
|
|
6146
|
+
} else if (bestTarget.title === pin.title) {
|
|
6147
|
+
method = "title_match";
|
|
6148
|
+
} else {
|
|
6149
|
+
method = "best_guess";
|
|
6150
|
+
}
|
|
6151
|
+
return {
|
|
6152
|
+
targetId: bestTarget.targetId,
|
|
6153
|
+
method,
|
|
6154
|
+
confidence: bestScore
|
|
6155
|
+
};
|
|
6156
|
+
}
|
|
6157
|
+
|
|
5590
6158
|
export {
|
|
5591
6159
|
bufferToBase64,
|
|
5592
6160
|
calculateRMS,
|
|
@@ -5602,7 +6170,19 @@ export {
|
|
|
5602
6170
|
waitForAnyElement,
|
|
5603
6171
|
waitForNavigation,
|
|
5604
6172
|
waitForNetworkIdle,
|
|
6173
|
+
extractPageState,
|
|
6174
|
+
computeDelta,
|
|
6175
|
+
extractReview,
|
|
5605
6176
|
Page,
|
|
5606
6177
|
Browser,
|
|
5607
|
-
connect
|
|
6178
|
+
connect,
|
|
6179
|
+
createFingerprint,
|
|
6180
|
+
fingerprintKey,
|
|
6181
|
+
fingerprintSimilarity,
|
|
6182
|
+
buildFingerprintMap,
|
|
6183
|
+
recoverStaleRef,
|
|
6184
|
+
detectOverlay,
|
|
6185
|
+
submitAndVerify,
|
|
6186
|
+
createTargetFingerprint,
|
|
6187
|
+
recoverPinnedTarget
|
|
5608
6188
|
};
|