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
package/dist/browser.cjs
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,6 +30,206 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/browser/combobox.ts
|
|
34
|
+
var combobox_exports = {};
|
|
35
|
+
__export(combobox_exports, {
|
|
36
|
+
chooseOption: () => chooseOption
|
|
37
|
+
});
|
|
38
|
+
async function chooseOption(page, config) {
|
|
39
|
+
const {
|
|
40
|
+
trigger,
|
|
41
|
+
listbox,
|
|
42
|
+
optionSelector,
|
|
43
|
+
searchText,
|
|
44
|
+
value,
|
|
45
|
+
match = "contains",
|
|
46
|
+
timeout = 1e4
|
|
47
|
+
} = config;
|
|
48
|
+
try {
|
|
49
|
+
await page.click(trigger, { timeout });
|
|
50
|
+
const listboxSelectors = listbox ? Array.isArray(listbox) ? listbox : [listbox] : DEFAULT_LISTBOX_SELECTORS;
|
|
51
|
+
const listboxFound = await page.waitFor(listboxSelectors, {
|
|
52
|
+
timeout: Math.min(timeout, 3e3),
|
|
53
|
+
optional: true,
|
|
54
|
+
state: "visible"
|
|
55
|
+
});
|
|
56
|
+
if (!listboxFound) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
failedAt: "open",
|
|
60
|
+
error: "Listbox did not appear after clicking trigger"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (searchText) {
|
|
64
|
+
try {
|
|
65
|
+
const triggerSel = Array.isArray(trigger) ? trigger[0] : trigger;
|
|
66
|
+
await page.type(triggerSel, searchText, {
|
|
67
|
+
delay: 30,
|
|
68
|
+
timeout: Math.min(timeout, 3e3)
|
|
69
|
+
});
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
71
|
+
} catch {
|
|
72
|
+
return { success: false, failedAt: "search", error: "Failed to type search text" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const optionSelectors = optionSelector ? [optionSelector] : DEFAULT_OPTION_SELECTORS;
|
|
76
|
+
const matchFn = match === "exact" ? "exact" : match === "startsWith" ? "startsWith" : "contains";
|
|
77
|
+
const clickedOption = await page.evaluate(`(() => {
|
|
78
|
+
const selectors = ${JSON.stringify(optionSelectors)};
|
|
79
|
+
const targetValue = ${JSON.stringify(value)};
|
|
80
|
+
const matchMode = ${JSON.stringify(matchFn)};
|
|
81
|
+
|
|
82
|
+
for (const sel of selectors) {
|
|
83
|
+
const options = document.querySelectorAll(sel);
|
|
84
|
+
for (const opt of options) {
|
|
85
|
+
const text = (opt.textContent || '').trim();
|
|
86
|
+
let matches = false;
|
|
87
|
+
if (matchMode === 'exact') matches = text === targetValue;
|
|
88
|
+
else if (matchMode === 'startsWith') matches = text.startsWith(targetValue);
|
|
89
|
+
else matches = text.includes(targetValue);
|
|
90
|
+
|
|
91
|
+
if (matches) {
|
|
92
|
+
opt.click();
|
|
93
|
+
return text;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
})()`);
|
|
99
|
+
if (!clickedOption) {
|
|
100
|
+
return { success: false, failedAt: "select", error: `No option matching "${value}" found` };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
selectedText: String(clickedOption)
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: error instanceof Error ? error.message : String(error)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
var DEFAULT_LISTBOX_SELECTORS, DEFAULT_OPTION_SELECTORS;
|
|
114
|
+
var init_combobox = __esm({
|
|
115
|
+
"src/browser/combobox.ts"() {
|
|
116
|
+
"use strict";
|
|
117
|
+
DEFAULT_LISTBOX_SELECTORS = [
|
|
118
|
+
'[role="listbox"]',
|
|
119
|
+
'[role="menu"]',
|
|
120
|
+
'[role="tree"]',
|
|
121
|
+
'ul[class*="dropdown"]',
|
|
122
|
+
'ul[class*="option"]',
|
|
123
|
+
'ul[class*="list"]',
|
|
124
|
+
'div[class*="dropdown"]',
|
|
125
|
+
'div[class*="menu"]'
|
|
126
|
+
];
|
|
127
|
+
DEFAULT_OPTION_SELECTORS = [
|
|
128
|
+
'[role="option"]',
|
|
129
|
+
'[role="menuitem"]',
|
|
130
|
+
'[role="treeitem"]',
|
|
131
|
+
"li"
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// src/browser/upload.ts
|
|
137
|
+
var upload_exports = {};
|
|
138
|
+
__export(upload_exports, {
|
|
139
|
+
uploadFiles: () => uploadFiles
|
|
140
|
+
});
|
|
141
|
+
async function uploadFiles(page, config) {
|
|
142
|
+
const { selector, files, timeout = 1e4 } = config;
|
|
143
|
+
const fileNames = files.map((f) => f.split("/").pop() ?? f);
|
|
144
|
+
try {
|
|
145
|
+
const selectors = Array.isArray(selector) ? selector : [selector];
|
|
146
|
+
let nodeId;
|
|
147
|
+
for (const sel of selectors) {
|
|
148
|
+
try {
|
|
149
|
+
const found = await page.waitFor(sel, {
|
|
150
|
+
timeout: Math.min(timeout, 5e3),
|
|
151
|
+
optional: true,
|
|
152
|
+
state: "attached"
|
|
153
|
+
});
|
|
154
|
+
if (found) {
|
|
155
|
+
const result = await page.evaluate(`(() => {
|
|
156
|
+
const el = document.querySelector(${JSON.stringify(sel)});
|
|
157
|
+
if (!el) return null;
|
|
158
|
+
return el.tagName.toLowerCase() === 'input' && el.type === 'file' ? 'file-input' : 'not-file-input';
|
|
159
|
+
})()`);
|
|
160
|
+
if (result === "file-input") {
|
|
161
|
+
const doc = await page.cdpClient.send("DOM.getDocument");
|
|
162
|
+
const queryResult = await page.cdpClient.send("DOM.querySelector", {
|
|
163
|
+
nodeId: doc.root.nodeId,
|
|
164
|
+
selector: sel
|
|
165
|
+
});
|
|
166
|
+
nodeId = queryResult.nodeId;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!nodeId) {
|
|
174
|
+
return {
|
|
175
|
+
accepted: false,
|
|
176
|
+
fileCount: 0,
|
|
177
|
+
fileNames,
|
|
178
|
+
error: "No file input element found"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
await page.cdpClient.send("DOM.setFileInputFiles", {
|
|
182
|
+
files,
|
|
183
|
+
nodeId
|
|
184
|
+
});
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
186
|
+
let validationError;
|
|
187
|
+
try {
|
|
188
|
+
const errorText = await page.evaluate(`(() => {
|
|
189
|
+
const errorSelectors = ['.error', '.validation-error', '[class*="error"]', '[role="alert"]'];
|
|
190
|
+
for (const sel of errorSelectors) {
|
|
191
|
+
const el = document.querySelector(sel);
|
|
192
|
+
if (el && el.offsetParent !== null && el.textContent.trim()) {
|
|
193
|
+
return el.textContent.trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
})()`);
|
|
198
|
+
if (errorText) validationError = String(errorText);
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
let visibleInUI;
|
|
202
|
+
try {
|
|
203
|
+
const visible = await page.evaluate(`(() => {
|
|
204
|
+
const text = document.body.innerText;
|
|
205
|
+
const fileNames = ${JSON.stringify(fileNames)};
|
|
206
|
+
return fileNames.some(name => text.includes(name));
|
|
207
|
+
})()`);
|
|
208
|
+
visibleInUI = visible === true;
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
accepted: true,
|
|
213
|
+
fileCount: files.length,
|
|
214
|
+
fileNames,
|
|
215
|
+
visibleInUI,
|
|
216
|
+
validationError
|
|
217
|
+
};
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
accepted: false,
|
|
221
|
+
fileCount: 0,
|
|
222
|
+
fileNames,
|
|
223
|
+
error: error instanceof Error ? error.message : String(error)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
var init_upload = __esm({
|
|
228
|
+
"src/browser/upload.ts"() {
|
|
229
|
+
"use strict";
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
30
233
|
// src/browser/index.ts
|
|
31
234
|
var browser_exports = {};
|
|
32
235
|
__export(browser_exports, {
|
|
@@ -35,7 +238,21 @@ __export(browser_exports, {
|
|
|
35
238
|
NavigationError: () => NavigationError,
|
|
36
239
|
Page: () => Page,
|
|
37
240
|
TimeoutError: () => TimeoutError,
|
|
38
|
-
|
|
241
|
+
buildFingerprintMap: () => buildFingerprintMap,
|
|
242
|
+
chooseOption: () => chooseOption,
|
|
243
|
+
computeDelta: () => computeDelta,
|
|
244
|
+
connect: () => connect,
|
|
245
|
+
createFingerprint: () => createFingerprint,
|
|
246
|
+
createTargetFingerprint: () => createTargetFingerprint,
|
|
247
|
+
detectOverlay: () => detectOverlay,
|
|
248
|
+
extractPageState: () => extractPageState,
|
|
249
|
+
extractReview: () => extractReview,
|
|
250
|
+
fingerprintKey: () => fingerprintKey,
|
|
251
|
+
fingerprintSimilarity: () => fingerprintSimilarity,
|
|
252
|
+
recoverPinnedTarget: () => recoverPinnedTarget,
|
|
253
|
+
recoverStaleRef: () => recoverStaleRef,
|
|
254
|
+
submitAndVerify: () => submitAndVerify,
|
|
255
|
+
uploadFiles: () => uploadFiles
|
|
39
256
|
});
|
|
40
257
|
module.exports = __toCommonJS(browser_exports);
|
|
41
258
|
|
|
@@ -883,6 +1100,275 @@ function createProvider(options) {
|
|
|
883
1100
|
}
|
|
884
1101
|
}
|
|
885
1102
|
|
|
1103
|
+
// src/utils/strings.ts
|
|
1104
|
+
function readString(value) {
|
|
1105
|
+
return typeof value === "string" ? value : void 0;
|
|
1106
|
+
}
|
|
1107
|
+
function readStringOr(value, fallback = "") {
|
|
1108
|
+
return readString(value) ?? fallback;
|
|
1109
|
+
}
|
|
1110
|
+
function formatConsoleArg(entry) {
|
|
1111
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
1112
|
+
}
|
|
1113
|
+
function globToRegex(pattern) {
|
|
1114
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1115
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1116
|
+
return new RegExp(`^${withWildcards}$`);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/actions/conditions.ts
|
|
1120
|
+
var NetworkResponseTracker = class {
|
|
1121
|
+
responses = [];
|
|
1122
|
+
listening = false;
|
|
1123
|
+
handler = null;
|
|
1124
|
+
start(cdp) {
|
|
1125
|
+
if (this.listening) return;
|
|
1126
|
+
this.listening = true;
|
|
1127
|
+
this.handler = (params) => {
|
|
1128
|
+
const response = params["response"];
|
|
1129
|
+
if (response) {
|
|
1130
|
+
this.responses.push({ url: response.url, status: response.status });
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
cdp.on("Network.responseReceived", this.handler);
|
|
1134
|
+
}
|
|
1135
|
+
stop(cdp) {
|
|
1136
|
+
if (this.handler) {
|
|
1137
|
+
cdp.off("Network.responseReceived", this.handler);
|
|
1138
|
+
this.handler = null;
|
|
1139
|
+
}
|
|
1140
|
+
this.listening = false;
|
|
1141
|
+
}
|
|
1142
|
+
getResponses() {
|
|
1143
|
+
return this.responses;
|
|
1144
|
+
}
|
|
1145
|
+
reset() {
|
|
1146
|
+
this.responses = [];
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
async function captureStateSignature(page) {
|
|
1150
|
+
try {
|
|
1151
|
+
const url = await page.url();
|
|
1152
|
+
const text = await page.text();
|
|
1153
|
+
const truncated = text.slice(0, 2e3);
|
|
1154
|
+
return `${url}|${simpleHash(truncated)}`;
|
|
1155
|
+
} catch {
|
|
1156
|
+
return "";
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function simpleHash(str) {
|
|
1160
|
+
let hash = 0;
|
|
1161
|
+
for (let i = 0; i < str.length; i++) {
|
|
1162
|
+
const char = str.charCodeAt(i);
|
|
1163
|
+
hash = (hash << 5) - hash + char | 0;
|
|
1164
|
+
}
|
|
1165
|
+
return hash.toString(36);
|
|
1166
|
+
}
|
|
1167
|
+
async function evaluateCondition(condition, page, context = {}) {
|
|
1168
|
+
switch (condition.kind) {
|
|
1169
|
+
case "urlMatches": {
|
|
1170
|
+
try {
|
|
1171
|
+
const currentUrl = await page.url();
|
|
1172
|
+
const regex = globToRegex(condition.pattern);
|
|
1173
|
+
const matched = regex.test(currentUrl);
|
|
1174
|
+
return {
|
|
1175
|
+
condition,
|
|
1176
|
+
matched,
|
|
1177
|
+
detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
|
|
1178
|
+
};
|
|
1179
|
+
} catch {
|
|
1180
|
+
return { condition, matched: false, detail: "Failed to get current URL" };
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
case "elementVisible": {
|
|
1184
|
+
try {
|
|
1185
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
1186
|
+
for (const sel of selectors) {
|
|
1187
|
+
const visible = await page.waitFor(sel, {
|
|
1188
|
+
timeout: 2e3,
|
|
1189
|
+
optional: true,
|
|
1190
|
+
state: "visible"
|
|
1191
|
+
});
|
|
1192
|
+
if (visible) {
|
|
1193
|
+
return { condition, matched: true, detail: `Element "${sel}" is visible` };
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return { condition, matched: false, detail: "No matching visible element found" };
|
|
1197
|
+
} catch {
|
|
1198
|
+
return { condition, matched: false, detail: "Visibility check failed" };
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
case "elementHidden": {
|
|
1202
|
+
try {
|
|
1203
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
1204
|
+
for (const sel of selectors) {
|
|
1205
|
+
const visible = await page.waitFor(sel, {
|
|
1206
|
+
timeout: 500,
|
|
1207
|
+
optional: true,
|
|
1208
|
+
state: "visible"
|
|
1209
|
+
});
|
|
1210
|
+
if (visible) {
|
|
1211
|
+
return { condition, matched: false, detail: `Element "${sel}" is still visible` };
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return { condition, matched: true, detail: "Element is hidden or not found" };
|
|
1215
|
+
} catch {
|
|
1216
|
+
return { condition, matched: true, detail: "Element is hidden (check threw)" };
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
case "textAppears": {
|
|
1220
|
+
try {
|
|
1221
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
1222
|
+
const text = await page.text(selector);
|
|
1223
|
+
const matched = text.includes(condition.text);
|
|
1224
|
+
return {
|
|
1225
|
+
condition,
|
|
1226
|
+
matched,
|
|
1227
|
+
detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
|
|
1228
|
+
};
|
|
1229
|
+
} catch {
|
|
1230
|
+
return { condition, matched: false, detail: "Failed to get page text" };
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
case "textChanges": {
|
|
1234
|
+
try {
|
|
1235
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
1236
|
+
const text = await page.text(selector);
|
|
1237
|
+
if (condition.to !== void 0) {
|
|
1238
|
+
const matched = text.includes(condition.to);
|
|
1239
|
+
return {
|
|
1240
|
+
condition,
|
|
1241
|
+
matched,
|
|
1242
|
+
detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
|
|
1246
|
+
} catch {
|
|
1247
|
+
return { condition, matched: false, detail: "Failed to get text for change detection" };
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
case "networkResponse": {
|
|
1251
|
+
const tracker = context.networkTracker;
|
|
1252
|
+
if (!tracker) {
|
|
1253
|
+
return { condition, matched: false, detail: "No network tracker active" };
|
|
1254
|
+
}
|
|
1255
|
+
const regex = globToRegex(condition.urlPattern);
|
|
1256
|
+
const responses = tracker.getResponses();
|
|
1257
|
+
for (const resp of responses) {
|
|
1258
|
+
if (regex.test(resp.url)) {
|
|
1259
|
+
if (condition.status !== void 0 && resp.status !== condition.status) {
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
condition,
|
|
1264
|
+
matched: true,
|
|
1265
|
+
detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return {
|
|
1270
|
+
condition,
|
|
1271
|
+
matched: false,
|
|
1272
|
+
detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
case "stateSignatureChanges": {
|
|
1276
|
+
if (!context.beforeSignature) {
|
|
1277
|
+
return { condition, matched: false, detail: "No before-signature captured" };
|
|
1278
|
+
}
|
|
1279
|
+
const afterSignature = await captureStateSignature(page);
|
|
1280
|
+
const matched = afterSignature !== context.beforeSignature;
|
|
1281
|
+
return {
|
|
1282
|
+
condition,
|
|
1283
|
+
matched,
|
|
1284
|
+
detail: matched ? "Page state changed" : "Page state unchanged"
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
default: {
|
|
1288
|
+
const _exhaustive = condition;
|
|
1289
|
+
return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async function evaluateOutcome(page, options) {
|
|
1294
|
+
const {
|
|
1295
|
+
expectAny,
|
|
1296
|
+
expectAll,
|
|
1297
|
+
failIf,
|
|
1298
|
+
dangerous = false,
|
|
1299
|
+
networkTracker,
|
|
1300
|
+
beforeSignature
|
|
1301
|
+
} = options;
|
|
1302
|
+
const allMatched = [];
|
|
1303
|
+
const context = { networkTracker, beforeSignature };
|
|
1304
|
+
if (failIf && failIf.length > 0) {
|
|
1305
|
+
for (const condition of failIf) {
|
|
1306
|
+
const result = await evaluateCondition(condition, page, context);
|
|
1307
|
+
allMatched.push(result);
|
|
1308
|
+
if (result.matched) {
|
|
1309
|
+
return {
|
|
1310
|
+
outcomeStatus: "failed",
|
|
1311
|
+
matchedConditions: allMatched,
|
|
1312
|
+
retrySafe: !dangerous
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (expectAll && expectAll.length > 0) {
|
|
1318
|
+
let allPassed = true;
|
|
1319
|
+
for (const condition of expectAll) {
|
|
1320
|
+
const result = await evaluateCondition(condition, page, context);
|
|
1321
|
+
allMatched.push(result);
|
|
1322
|
+
if (!result.matched) {
|
|
1323
|
+
allPassed = false;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (!allPassed) {
|
|
1327
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
1328
|
+
return {
|
|
1329
|
+
outcomeStatus: status,
|
|
1330
|
+
matchedConditions: allMatched,
|
|
1331
|
+
retrySafe: !dangerous
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
if (!expectAny || expectAny.length === 0) {
|
|
1335
|
+
return {
|
|
1336
|
+
outcomeStatus: "success",
|
|
1337
|
+
matchedConditions: allMatched,
|
|
1338
|
+
retrySafe: true
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (expectAny && expectAny.length > 0) {
|
|
1343
|
+
let anyPassed = false;
|
|
1344
|
+
for (const condition of expectAny) {
|
|
1345
|
+
const result = await evaluateCondition(condition, page, context);
|
|
1346
|
+
allMatched.push(result);
|
|
1347
|
+
if (result.matched) {
|
|
1348
|
+
anyPassed = true;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (anyPassed) {
|
|
1352
|
+
return {
|
|
1353
|
+
outcomeStatus: "success",
|
|
1354
|
+
matchedConditions: allMatched,
|
|
1355
|
+
retrySafe: true
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
1359
|
+
return {
|
|
1360
|
+
outcomeStatus: status,
|
|
1361
|
+
matchedConditions: allMatched,
|
|
1362
|
+
retrySafe: !dangerous
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return {
|
|
1366
|
+
outcomeStatus: "success",
|
|
1367
|
+
matchedConditions: allMatched,
|
|
1368
|
+
retrySafe: true
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
|
|
886
1372
|
// src/actions/executor.ts
|
|
887
1373
|
var fs = __toESM(require("fs"), 1);
|
|
888
1374
|
var import_node_path = require("path");
|
|
@@ -2330,13 +2816,6 @@ var TRACE_SCRIPT = `
|
|
|
2330
2816
|
})();
|
|
2331
2817
|
`;
|
|
2332
2818
|
|
|
2333
|
-
// src/trace/live.ts
|
|
2334
|
-
function globToRegex(pattern) {
|
|
2335
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2336
|
-
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
2337
|
-
return new RegExp(`^${withWildcards}$`);
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
2819
|
// src/actions/executor.ts
|
|
2341
2820
|
var DEFAULT_TIMEOUT = 3e4;
|
|
2342
2821
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -2346,15 +2825,6 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
2346
2825
|
"text",
|
|
2347
2826
|
"screenshot"
|
|
2348
2827
|
];
|
|
2349
|
-
function readString(value) {
|
|
2350
|
-
return typeof value === "string" ? value : void 0;
|
|
2351
|
-
}
|
|
2352
|
-
function readStringOr(value, fallback = "") {
|
|
2353
|
-
return readString(value) ?? fallback;
|
|
2354
|
-
}
|
|
2355
|
-
function formatConsoleArg(entry) {
|
|
2356
|
-
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
2357
|
-
}
|
|
2358
2828
|
function loadExistingRecording(manifestPath) {
|
|
2359
2829
|
try {
|
|
2360
2830
|
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
@@ -2468,6 +2938,25 @@ function getSuggestion(reason) {
|
|
|
2468
2938
|
}
|
|
2469
2939
|
}
|
|
2470
2940
|
}
|
|
2941
|
+
function hasOutcomeConditions(step) {
|
|
2942
|
+
return step.expectAny !== void 0 && step.expectAny.length > 0 || step.expectAll !== void 0 && step.expectAll.length > 0 || step.failIf !== void 0 && step.failIf.length > 0;
|
|
2943
|
+
}
|
|
2944
|
+
function needsNetworkTracking(step) {
|
|
2945
|
+
const allConditions = [
|
|
2946
|
+
...step.expectAny ?? [],
|
|
2947
|
+
...step.expectAll ?? [],
|
|
2948
|
+
...step.failIf ?? []
|
|
2949
|
+
];
|
|
2950
|
+
return allConditions.some((c) => c.kind === "networkResponse");
|
|
2951
|
+
}
|
|
2952
|
+
function needsStateSignature(step) {
|
|
2953
|
+
const allConditions = [
|
|
2954
|
+
...step.expectAny ?? [],
|
|
2955
|
+
...step.expectAll ?? [],
|
|
2956
|
+
...step.failIf ?? []
|
|
2957
|
+
];
|
|
2958
|
+
return allConditions.some((c) => c.kind === "stateSignatureChanges");
|
|
2959
|
+
}
|
|
2471
2960
|
var BatchExecutor = class {
|
|
2472
2961
|
page;
|
|
2473
2962
|
constructor(page) {
|
|
@@ -2513,9 +3002,25 @@ var BatchExecutor = class {
|
|
|
2513
3002
|
})
|
|
2514
3003
|
);
|
|
2515
3004
|
}
|
|
3005
|
+
const hasOutcome = hasOutcomeConditions(step);
|
|
3006
|
+
let networkTracker;
|
|
3007
|
+
let beforeSignature;
|
|
3008
|
+
if (hasOutcome) {
|
|
3009
|
+
if (needsNetworkTracking(step)) {
|
|
3010
|
+
networkTracker = new NetworkResponseTracker();
|
|
3011
|
+
networkTracker.start(this.page.cdpClient);
|
|
3012
|
+
}
|
|
3013
|
+
if (needsStateSignature(step)) {
|
|
3014
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
2516
3017
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2517
3018
|
if (attempt > 0) {
|
|
2518
3019
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
3020
|
+
if (networkTracker) networkTracker.reset();
|
|
3021
|
+
if (hasOutcome && needsStateSignature(step)) {
|
|
3022
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
3023
|
+
}
|
|
2519
3024
|
}
|
|
2520
3025
|
try {
|
|
2521
3026
|
this.page.resetLastActionPosition();
|
|
@@ -2533,6 +3038,28 @@ var BatchExecutor = class {
|
|
|
2533
3038
|
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
2534
3039
|
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
2535
3040
|
};
|
|
3041
|
+
if (hasOutcome) {
|
|
3042
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
3043
|
+
const outcome = await evaluateOutcome(this.page, {
|
|
3044
|
+
expectAny: step.expectAny,
|
|
3045
|
+
expectAll: step.expectAll,
|
|
3046
|
+
failIf: step.failIf,
|
|
3047
|
+
dangerous: step.dangerous,
|
|
3048
|
+
networkTracker,
|
|
3049
|
+
beforeSignature
|
|
3050
|
+
});
|
|
3051
|
+
stepResult.outcomeStatus = outcome.outcomeStatus;
|
|
3052
|
+
stepResult.matchedConditions = outcome.matchedConditions;
|
|
3053
|
+
stepResult.retrySafe = outcome.retrySafe;
|
|
3054
|
+
if (outcome.outcomeStatus !== "success") {
|
|
3055
|
+
stepResult.success = false;
|
|
3056
|
+
stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
|
|
3057
|
+
const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
|
|
3058
|
+
if (failedDetails.length > 0) {
|
|
3059
|
+
stepResult.suggestion = failedDetails.join("; ");
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
2536
3063
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
2537
3064
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
2538
3065
|
}
|
|
@@ -2542,13 +3069,14 @@ var BatchExecutor = class {
|
|
|
2542
3069
|
traceId: createTraceId("action"),
|
|
2543
3070
|
elapsedMs: Date.now() - startTime,
|
|
2544
3071
|
channel: "action",
|
|
2545
|
-
event: "action.succeeded",
|
|
2546
|
-
summary: `${step.action} succeeded`,
|
|
3072
|
+
event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
|
|
3073
|
+
summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
|
|
2547
3074
|
data: {
|
|
2548
3075
|
action: step.action,
|
|
2549
3076
|
selector: step.selector ?? null,
|
|
2550
3077
|
selectorUsed: result.selectorUsed ?? null,
|
|
2551
|
-
durationMs: Date.now() - stepStart
|
|
3078
|
+
durationMs: Date.now() - stepStart,
|
|
3079
|
+
outcomeStatus: stepResult.outcomeStatus ?? null
|
|
2552
3080
|
},
|
|
2553
3081
|
actionId: `action-${i + 1}`,
|
|
2554
3082
|
stepIndex: i,
|
|
@@ -2558,6 +3086,18 @@ var BatchExecutor = class {
|
|
|
2558
3086
|
})
|
|
2559
3087
|
);
|
|
2560
3088
|
}
|
|
3089
|
+
if (hasOutcome && !stepResult.success) {
|
|
3090
|
+
if (step.dangerous) {
|
|
3091
|
+
results.push(stepResult);
|
|
3092
|
+
break;
|
|
3093
|
+
}
|
|
3094
|
+
if (attempt < maxAttempts - 1) {
|
|
3095
|
+
lastError = new Error(stepResult.error ?? "Outcome failed");
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
3098
|
+
results.push(stepResult);
|
|
3099
|
+
break;
|
|
3100
|
+
}
|
|
2561
3101
|
results.push(stepResult);
|
|
2562
3102
|
succeeded = true;
|
|
2563
3103
|
break;
|
|
@@ -2565,59 +3105,63 @@ var BatchExecutor = class {
|
|
|
2565
3105
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2566
3106
|
}
|
|
2567
3107
|
}
|
|
3108
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
2568
3109
|
if (!succeeded) {
|
|
2569
|
-
const
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
3110
|
+
const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
|
|
3111
|
+
if (!resultAlreadyPushed) {
|
|
3112
|
+
const errorMessage = lastError?.message ?? "Unknown error";
|
|
3113
|
+
let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
|
|
3114
|
+
const { reason, coveringElement } = classifyFailure(lastError);
|
|
3115
|
+
if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
|
|
3116
|
+
try {
|
|
3117
|
+
const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
|
|
3118
|
+
const autoHints = await generateHints(this.page, selectors, step.action, 3);
|
|
3119
|
+
if (autoHints.length > 0) {
|
|
3120
|
+
hints = autoHints;
|
|
3121
|
+
}
|
|
3122
|
+
} catch {
|
|
2578
3123
|
}
|
|
2579
|
-
} catch {
|
|
2580
3124
|
}
|
|
3125
|
+
const failedResult = {
|
|
3126
|
+
index: i,
|
|
3127
|
+
action: step.action,
|
|
3128
|
+
selector: step.selector,
|
|
3129
|
+
success: false,
|
|
3130
|
+
durationMs: Date.now() - stepStart,
|
|
3131
|
+
error: errorMessage,
|
|
3132
|
+
hints,
|
|
3133
|
+
failureReason: reason,
|
|
3134
|
+
coveringElement,
|
|
3135
|
+
suggestion: getSuggestion(reason),
|
|
3136
|
+
timestamp: Date.now()
|
|
3137
|
+
};
|
|
3138
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
3139
|
+
await this.captureRecordingFrame(step, failedResult, recording);
|
|
3140
|
+
}
|
|
3141
|
+
if (recording) {
|
|
3142
|
+
recording.traceEvents.push(
|
|
3143
|
+
normalizeTraceEvent({
|
|
3144
|
+
traceId: createTraceId("action"),
|
|
3145
|
+
elapsedMs: Date.now() - startTime,
|
|
3146
|
+
channel: "action",
|
|
3147
|
+
event: "action.failed",
|
|
3148
|
+
severity: "error",
|
|
3149
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
3150
|
+
data: {
|
|
3151
|
+
action: step.action,
|
|
3152
|
+
selector: step.selector ?? null,
|
|
3153
|
+
error: errorMessage,
|
|
3154
|
+
reason
|
|
3155
|
+
},
|
|
3156
|
+
actionId: `action-${i + 1}`,
|
|
3157
|
+
stepIndex: i,
|
|
3158
|
+
selector: step.selector,
|
|
3159
|
+
url: step.url
|
|
3160
|
+
})
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
results.push(failedResult);
|
|
2581
3164
|
}
|
|
2582
|
-
const failedResult = {
|
|
2583
|
-
index: i,
|
|
2584
|
-
action: step.action,
|
|
2585
|
-
selector: step.selector,
|
|
2586
|
-
success: false,
|
|
2587
|
-
durationMs: Date.now() - stepStart,
|
|
2588
|
-
error: errorMessage,
|
|
2589
|
-
hints,
|
|
2590
|
-
failureReason: reason,
|
|
2591
|
-
coveringElement,
|
|
2592
|
-
suggestion: getSuggestion(reason),
|
|
2593
|
-
timestamp: Date.now()
|
|
2594
|
-
};
|
|
2595
|
-
if (recording && !recording.skipActions.has(step.action)) {
|
|
2596
|
-
await this.captureRecordingFrame(step, failedResult, recording);
|
|
2597
|
-
}
|
|
2598
|
-
if (recording) {
|
|
2599
|
-
recording.traceEvents.push(
|
|
2600
|
-
normalizeTraceEvent({
|
|
2601
|
-
traceId: createTraceId("action"),
|
|
2602
|
-
elapsedMs: Date.now() - startTime,
|
|
2603
|
-
channel: "action",
|
|
2604
|
-
event: "action.failed",
|
|
2605
|
-
severity: "error",
|
|
2606
|
-
summary: `${step.action} failed: ${errorMessage}`,
|
|
2607
|
-
data: {
|
|
2608
|
-
action: step.action,
|
|
2609
|
-
selector: step.selector ?? null,
|
|
2610
|
-
error: errorMessage,
|
|
2611
|
-
reason
|
|
2612
|
-
},
|
|
2613
|
-
actionId: `action-${i + 1}`,
|
|
2614
|
-
stepIndex: i,
|
|
2615
|
-
selector: step.selector,
|
|
2616
|
-
url: step.url
|
|
2617
|
-
})
|
|
2618
|
-
);
|
|
2619
|
-
}
|
|
2620
|
-
results.push(failedResult);
|
|
2621
3165
|
if (onFail === "stop" && !step.optional) {
|
|
2622
3166
|
stoppedAtIndex = i;
|
|
2623
3167
|
break;
|
|
@@ -2928,6 +3472,14 @@ var BatchExecutor = class {
|
|
|
2928
3472
|
case "forms": {
|
|
2929
3473
|
return { value: await this.page.forms() };
|
|
2930
3474
|
}
|
|
3475
|
+
case "delta": {
|
|
3476
|
+
const review = await this.page.review();
|
|
3477
|
+
return { value: review };
|
|
3478
|
+
}
|
|
3479
|
+
case "review": {
|
|
3480
|
+
const review = await this.page.review();
|
|
3481
|
+
return { value: review };
|
|
3482
|
+
}
|
|
2931
3483
|
case "screenshot": {
|
|
2932
3484
|
const data = await this.page.screenshot({
|
|
2933
3485
|
format: step.format,
|
|
@@ -3078,6 +3630,35 @@ var BatchExecutor = class {
|
|
|
3078
3630
|
const media = await this.assertMediaTrackLive(step.kind);
|
|
3079
3631
|
return { value: media };
|
|
3080
3632
|
}
|
|
3633
|
+
case "chooseOption": {
|
|
3634
|
+
const { chooseOption: chooseOption2 } = await Promise.resolve().then(() => (init_combobox(), combobox_exports));
|
|
3635
|
+
if (!step.value) throw new Error("chooseOption requires value");
|
|
3636
|
+
const result = await chooseOption2(this.page, {
|
|
3637
|
+
trigger: step.trigger ?? step.selector ?? "",
|
|
3638
|
+
listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
|
|
3639
|
+
value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
|
|
3640
|
+
match: step.match,
|
|
3641
|
+
timeout: step.timeout ?? timeout
|
|
3642
|
+
});
|
|
3643
|
+
if (!result.success) {
|
|
3644
|
+
throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
|
|
3645
|
+
}
|
|
3646
|
+
return { value: result };
|
|
3647
|
+
}
|
|
3648
|
+
case "upload": {
|
|
3649
|
+
const { uploadFiles: uploadFiles2 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
|
|
3650
|
+
if (!step.selector) throw new Error("upload requires selector");
|
|
3651
|
+
if (!step.files || step.files.length === 0) throw new Error("upload requires files");
|
|
3652
|
+
const result = await uploadFiles2(this.page, {
|
|
3653
|
+
selector: step.selector,
|
|
3654
|
+
files: step.files,
|
|
3655
|
+
timeout: step.timeout ?? timeout
|
|
3656
|
+
});
|
|
3657
|
+
if (!result.accepted) {
|
|
3658
|
+
throw new Error(result.error ?? "Upload was not accepted");
|
|
3659
|
+
}
|
|
3660
|
+
return { value: result };
|
|
3661
|
+
}
|
|
3081
3662
|
default: {
|
|
3082
3663
|
const action = step.action;
|
|
3083
3664
|
const aliases = {
|
|
@@ -5323,6 +5904,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
5323
5904
|
});
|
|
5324
5905
|
}
|
|
5325
5906
|
|
|
5907
|
+
// src/browser/delta.ts
|
|
5908
|
+
function extractPageState(url, title, snapshot, forms, pageText) {
|
|
5909
|
+
const headings = [];
|
|
5910
|
+
const buttons = [];
|
|
5911
|
+
const alerts = [];
|
|
5912
|
+
function walkNodes(nodes) {
|
|
5913
|
+
for (const node of nodes) {
|
|
5914
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
5915
|
+
if (role === "heading" && node.name) {
|
|
5916
|
+
headings.push(node.name);
|
|
5917
|
+
}
|
|
5918
|
+
if ((role === "button" || role === "link") && node.name) {
|
|
5919
|
+
const disabled = node.disabled ?? false;
|
|
5920
|
+
buttons.push({ text: node.name, disabled, ref: node.ref });
|
|
5921
|
+
}
|
|
5922
|
+
if (role === "alert" && node.name) {
|
|
5923
|
+
alerts.push(node.name);
|
|
5924
|
+
}
|
|
5925
|
+
if (node.children) {
|
|
5926
|
+
walkNodes(node.children);
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
walkNodes(snapshot.accessibilityTree);
|
|
5931
|
+
const formFields = forms.map((f) => ({
|
|
5932
|
+
label: f.label,
|
|
5933
|
+
name: f.name,
|
|
5934
|
+
id: f.id,
|
|
5935
|
+
value: f.value,
|
|
5936
|
+
type: f.type
|
|
5937
|
+
}));
|
|
5938
|
+
return {
|
|
5939
|
+
url,
|
|
5940
|
+
title,
|
|
5941
|
+
headings,
|
|
5942
|
+
formFields,
|
|
5943
|
+
buttons,
|
|
5944
|
+
alerts,
|
|
5945
|
+
visibleText: pageText.slice(0, 3e3)
|
|
5946
|
+
};
|
|
5947
|
+
}
|
|
5948
|
+
function computeDelta(before, after) {
|
|
5949
|
+
const changes = [];
|
|
5950
|
+
if (before.url !== after.url) {
|
|
5951
|
+
changes.push({ kind: "url", before: before.url, after: after.url });
|
|
5952
|
+
}
|
|
5953
|
+
if (before.title !== after.title) {
|
|
5954
|
+
changes.push({ kind: "title", before: before.title, after: after.title });
|
|
5955
|
+
}
|
|
5956
|
+
const beforeHeadings = new Set(before.headings);
|
|
5957
|
+
const afterHeadings = new Set(after.headings);
|
|
5958
|
+
for (const h of after.headings) {
|
|
5959
|
+
if (!beforeHeadings.has(h)) {
|
|
5960
|
+
changes.push({ kind: "heading_added", after: h });
|
|
5961
|
+
}
|
|
5962
|
+
}
|
|
5963
|
+
for (const h of before.headings) {
|
|
5964
|
+
if (!afterHeadings.has(h)) {
|
|
5965
|
+
changes.push({ kind: "heading_removed", before: h });
|
|
5966
|
+
}
|
|
5967
|
+
}
|
|
5968
|
+
const beforeFieldMap = new Map(
|
|
5969
|
+
before.formFields.map((f) => [f.id ?? f.name ?? f.label ?? "", f])
|
|
5970
|
+
);
|
|
5971
|
+
for (const af of after.formFields) {
|
|
5972
|
+
const key = af.id ?? af.name ?? af.label ?? "";
|
|
5973
|
+
const bf = beforeFieldMap.get(key);
|
|
5974
|
+
if (bf && JSON.stringify(bf.value) !== JSON.stringify(af.value)) {
|
|
5975
|
+
changes.push({
|
|
5976
|
+
kind: "field_changed",
|
|
5977
|
+
before: String(bf.value ?? ""),
|
|
5978
|
+
after: String(af.value ?? ""),
|
|
5979
|
+
detail: af.label ?? af.name ?? af.id ?? key
|
|
5980
|
+
});
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
const beforeBtnMap = new Map(before.buttons.map((b) => [b.text, b]));
|
|
5984
|
+
for (const ab of after.buttons) {
|
|
5985
|
+
const bb = beforeBtnMap.get(ab.text);
|
|
5986
|
+
if (bb && bb.disabled !== ab.disabled) {
|
|
5987
|
+
changes.push({
|
|
5988
|
+
kind: "button_changed",
|
|
5989
|
+
detail: ab.text,
|
|
5990
|
+
before: bb.disabled ? "disabled" : "enabled",
|
|
5991
|
+
after: ab.disabled ? "disabled" : "enabled"
|
|
5992
|
+
});
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
const beforeAlerts = new Set(before.alerts);
|
|
5996
|
+
const afterAlerts = new Set(after.alerts);
|
|
5997
|
+
for (const a of after.alerts) {
|
|
5998
|
+
if (!beforeAlerts.has(a)) {
|
|
5999
|
+
changes.push({ kind: "alert_added", after: a });
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
for (const a of before.alerts) {
|
|
6003
|
+
if (!afterAlerts.has(a)) {
|
|
6004
|
+
changes.push({ kind: "alert_removed", before: a });
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
return {
|
|
6008
|
+
changes,
|
|
6009
|
+
before,
|
|
6010
|
+
after,
|
|
6011
|
+
hasChanges: changes.length > 0
|
|
6012
|
+
};
|
|
6013
|
+
}
|
|
6014
|
+
|
|
5326
6015
|
// src/browser/keyboard.ts
|
|
5327
6016
|
var US_KEYBOARD = {
|
|
5328
6017
|
// Letters (lowercase)
|
|
@@ -5482,8 +6171,118 @@ function parseShortcut(combo) {
|
|
|
5482
6171
|
return { modifiers, key };
|
|
5483
6172
|
}
|
|
5484
6173
|
|
|
6174
|
+
// src/browser/review.ts
|
|
6175
|
+
function extractReview(url, title, snapshot, forms, pageText) {
|
|
6176
|
+
const headings = [];
|
|
6177
|
+
const alerts = [];
|
|
6178
|
+
const statusLabels = [];
|
|
6179
|
+
const keyValues = [];
|
|
6180
|
+
const tables = [];
|
|
6181
|
+
const summaryCards = [];
|
|
6182
|
+
function walkNodes(nodes, parentHeading) {
|
|
6183
|
+
let currentHeading = parentHeading;
|
|
6184
|
+
for (const node of nodes) {
|
|
6185
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
6186
|
+
if (role === "heading" && node.name) {
|
|
6187
|
+
headings.push(node.name);
|
|
6188
|
+
currentHeading = node.name;
|
|
6189
|
+
}
|
|
6190
|
+
if (role === "alert" && node.name) {
|
|
6191
|
+
alerts.push(node.name);
|
|
6192
|
+
}
|
|
6193
|
+
if (role === "status" && node.name) {
|
|
6194
|
+
statusLabels.push(node.name);
|
|
6195
|
+
}
|
|
6196
|
+
if (role === "table" || role === "grid") {
|
|
6197
|
+
const table = extractTableFromNode(node);
|
|
6198
|
+
if (table) tables.push(table);
|
|
6199
|
+
}
|
|
6200
|
+
if ((role === "definition" || role === "term") && node.name) {
|
|
6201
|
+
if (role === "term") {
|
|
6202
|
+
keyValues.push({ key: node.name, value: "" });
|
|
6203
|
+
} else if (role === "definition" && keyValues.length > 0) {
|
|
6204
|
+
const last = keyValues[keyValues.length - 1];
|
|
6205
|
+
if (!last.value) last.value = node.name;
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
if (node.children) {
|
|
6209
|
+
walkNodes(node.children, currentHeading);
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
walkNodes(snapshot.accessibilityTree);
|
|
6214
|
+
const textKvPairs = extractKeyValueFromText(pageText);
|
|
6215
|
+
keyValues.push(...textKvPairs);
|
|
6216
|
+
const formEntries = forms.map((f) => ({
|
|
6217
|
+
label: f.label,
|
|
6218
|
+
value: f.value,
|
|
6219
|
+
type: f.type,
|
|
6220
|
+
disabled: f.disabled
|
|
6221
|
+
}));
|
|
6222
|
+
return {
|
|
6223
|
+
url,
|
|
6224
|
+
title,
|
|
6225
|
+
headings,
|
|
6226
|
+
forms: formEntries,
|
|
6227
|
+
alerts,
|
|
6228
|
+
summaryCards,
|
|
6229
|
+
tables,
|
|
6230
|
+
keyValues,
|
|
6231
|
+
statusLabels
|
|
6232
|
+
};
|
|
6233
|
+
}
|
|
6234
|
+
function extractTableFromNode(node) {
|
|
6235
|
+
const headers = [];
|
|
6236
|
+
const rows = [];
|
|
6237
|
+
function findRows(n) {
|
|
6238
|
+
const role = n.role?.toLowerCase() ?? "";
|
|
6239
|
+
if (role === "columnheader" && n.name) {
|
|
6240
|
+
headers.push(n.name);
|
|
6241
|
+
}
|
|
6242
|
+
if (role === "row") {
|
|
6243
|
+
const cells = [];
|
|
6244
|
+
if (n.children) {
|
|
6245
|
+
for (const child of n.children) {
|
|
6246
|
+
const childRole = child.role?.toLowerCase() ?? "";
|
|
6247
|
+
if ((childRole === "cell" || childRole === "gridcell") && child.name) {
|
|
6248
|
+
cells.push(child.name);
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
if (cells.length > 0) rows.push(cells);
|
|
6253
|
+
}
|
|
6254
|
+
if (n.children) {
|
|
6255
|
+
for (const child of n.children) findRows(child);
|
|
6256
|
+
}
|
|
6257
|
+
}
|
|
6258
|
+
findRows(node);
|
|
6259
|
+
if (rows.length === 0) return null;
|
|
6260
|
+
return { headers, rows };
|
|
6261
|
+
}
|
|
6262
|
+
function extractKeyValueFromText(text) {
|
|
6263
|
+
const pairs = [];
|
|
6264
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
6265
|
+
for (const line of lines) {
|
|
6266
|
+
const match = line.match(/^([A-Z][A-Za-z0-9 ]{1,30})[:—]\s+(.+)$/);
|
|
6267
|
+
if (match) {
|
|
6268
|
+
pairs.push({ key: match[1].trim(), value: match[2].trim() });
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
return pairs.slice(0, 20);
|
|
6272
|
+
}
|
|
6273
|
+
|
|
5485
6274
|
// src/browser/page.ts
|
|
5486
6275
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
6276
|
+
function normalizeAXCheckedValue(value) {
|
|
6277
|
+
if (typeof value === "boolean") {
|
|
6278
|
+
return value;
|
|
6279
|
+
}
|
|
6280
|
+
if (typeof value === "string") {
|
|
6281
|
+
if (value === "true") return true;
|
|
6282
|
+
if (value === "false") return false;
|
|
6283
|
+
}
|
|
6284
|
+
return void 0;
|
|
6285
|
+
}
|
|
5487
6286
|
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
5488
6287
|
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
5489
6288
|
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
@@ -7269,7 +8068,9 @@ var Page = class {
|
|
|
7269
8068
|
}
|
|
7270
8069
|
}
|
|
7271
8070
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
7272
|
-
const checked =
|
|
8071
|
+
const checked = normalizeAXCheckedValue(
|
|
8072
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
8073
|
+
);
|
|
7273
8074
|
return {
|
|
7274
8075
|
role,
|
|
7275
8076
|
name,
|
|
@@ -7325,7 +8126,9 @@ var Page = class {
|
|
|
7325
8126
|
const ref = nodeRefs.get(node.nodeId);
|
|
7326
8127
|
const name = node.name?.value ?? "";
|
|
7327
8128
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
7328
|
-
const checked =
|
|
8129
|
+
const checked = normalizeAXCheckedValue(
|
|
8130
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
8131
|
+
);
|
|
7329
8132
|
const value = node.value?.value;
|
|
7330
8133
|
const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
|
|
7331
8134
|
interactiveElements.push({
|
|
@@ -7392,6 +8195,45 @@ var Page = class {
|
|
|
7392
8195
|
}
|
|
7393
8196
|
}
|
|
7394
8197
|
}
|
|
8198
|
+
// ============ Delta & Review ============
|
|
8199
|
+
/**
|
|
8200
|
+
* Capture current page state for delta comparison.
|
|
8201
|
+
* Call before an action, then call delta() again after and use computeDelta().
|
|
8202
|
+
*/
|
|
8203
|
+
async captureState() {
|
|
8204
|
+
const [url, title, snapshot, forms, text] = await Promise.all([
|
|
8205
|
+
this.url(),
|
|
8206
|
+
this.title(),
|
|
8207
|
+
this.snapshot(),
|
|
8208
|
+
this.forms(),
|
|
8209
|
+
this.text()
|
|
8210
|
+
]);
|
|
8211
|
+
return extractPageState(url, title, snapshot, forms, text);
|
|
8212
|
+
}
|
|
8213
|
+
/**
|
|
8214
|
+
* Compute what changed between two page states.
|
|
8215
|
+
* If no arguments: captures current state and returns it (for use as "before").
|
|
8216
|
+
* If one argument (before state): captures current state and computes delta.
|
|
8217
|
+
*/
|
|
8218
|
+
async delta(before) {
|
|
8219
|
+
const currentState = await this.captureState();
|
|
8220
|
+
if (!before) return currentState;
|
|
8221
|
+
return computeDelta(before, currentState);
|
|
8222
|
+
}
|
|
8223
|
+
/**
|
|
8224
|
+
* Extract structured review surface from the current page.
|
|
8225
|
+
* Returns headings, form values, alerts, key-value pairs, tables, and status labels.
|
|
8226
|
+
*/
|
|
8227
|
+
async review() {
|
|
8228
|
+
const [url, title, snapshot, forms, text] = await Promise.all([
|
|
8229
|
+
this.url(),
|
|
8230
|
+
this.title(),
|
|
8231
|
+
this.snapshot(),
|
|
8232
|
+
this.forms(),
|
|
8233
|
+
this.text()
|
|
8234
|
+
]);
|
|
8235
|
+
return extractReview(url, title, snapshot, forms, text);
|
|
8236
|
+
}
|
|
7395
8237
|
// ============ Batch Execution ============
|
|
7396
8238
|
/**
|
|
7397
8239
|
* Execute a batch of steps
|
|
@@ -8816,6 +9658,316 @@ var Browser = class _Browser {
|
|
|
8816
9658
|
function connect(options) {
|
|
8817
9659
|
return Browser.connect(options);
|
|
8818
9660
|
}
|
|
9661
|
+
|
|
9662
|
+
// src/browser/index.ts
|
|
9663
|
+
init_combobox();
|
|
9664
|
+
|
|
9665
|
+
// src/browser/fingerprint.ts
|
|
9666
|
+
function createFingerprint(node, context) {
|
|
9667
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
9668
|
+
const name = node.name ?? "";
|
|
9669
|
+
let valueShape = "";
|
|
9670
|
+
if (node.value !== void 0) {
|
|
9671
|
+
valueShape = typeof node.value === "string" ? "text" : typeof node.value === "number" ? "number" : typeof node.value === "boolean" ? "boolean" : "other";
|
|
9672
|
+
}
|
|
9673
|
+
const stableAttrs = {};
|
|
9674
|
+
if (node.properties) {
|
|
9675
|
+
for (const key of ["id", "name", "type", "aria-label"]) {
|
|
9676
|
+
const val = node.properties[key];
|
|
9677
|
+
if (val !== void 0 && val !== null) {
|
|
9678
|
+
stableAttrs[key] = String(val);
|
|
9679
|
+
}
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
return {
|
|
9683
|
+
role,
|
|
9684
|
+
name,
|
|
9685
|
+
valueShape,
|
|
9686
|
+
label: name,
|
|
9687
|
+
// label is typically the accessible name
|
|
9688
|
+
stableAttrs,
|
|
9689
|
+
nearestHeading: context.nearestHeading,
|
|
9690
|
+
siblingIndex: context.siblingIndex,
|
|
9691
|
+
sectionPath: [...context.headingTrail]
|
|
9692
|
+
};
|
|
9693
|
+
}
|
|
9694
|
+
function fingerprintKey(fp) {
|
|
9695
|
+
const parts = [fp.role, fp.name, fp.sectionPath.join(">")];
|
|
9696
|
+
if (fp.stableAttrs["id"]) parts.push(`id=${fp.stableAttrs["id"]}`);
|
|
9697
|
+
if (fp.stableAttrs["name"]) parts.push(`name=${fp.stableAttrs["name"]}`);
|
|
9698
|
+
return parts.join("|");
|
|
9699
|
+
}
|
|
9700
|
+
function fingerprintSimilarity(a, b) {
|
|
9701
|
+
let score = 0;
|
|
9702
|
+
let weight = 0;
|
|
9703
|
+
weight += 3;
|
|
9704
|
+
if (a.role === b.role) score += 3;
|
|
9705
|
+
else return 0;
|
|
9706
|
+
weight += 5;
|
|
9707
|
+
if (a.name && b.name && a.name === b.name) score += 5;
|
|
9708
|
+
else if (a.name && b.name && a.name.toLowerCase() === b.name.toLowerCase()) score += 4;
|
|
9709
|
+
weight += 3;
|
|
9710
|
+
const pathA = a.sectionPath.join(">");
|
|
9711
|
+
const pathB = b.sectionPath.join(">");
|
|
9712
|
+
if (pathA === pathB) score += 3;
|
|
9713
|
+
else if (pathA && pathB && (pathA.includes(pathB) || pathB.includes(pathA))) score += 1;
|
|
9714
|
+
const attrKeys = /* @__PURE__ */ new Set([...Object.keys(a.stableAttrs), ...Object.keys(b.stableAttrs)]);
|
|
9715
|
+
for (const key of attrKeys) {
|
|
9716
|
+
weight += 2;
|
|
9717
|
+
if (a.stableAttrs[key] && b.stableAttrs[key] && a.stableAttrs[key] === b.stableAttrs[key]) {
|
|
9718
|
+
score += 2;
|
|
9719
|
+
}
|
|
9720
|
+
}
|
|
9721
|
+
weight += 1;
|
|
9722
|
+
if (a.siblingIndex === b.siblingIndex) score += 1;
|
|
9723
|
+
return score / weight;
|
|
9724
|
+
}
|
|
9725
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
9726
|
+
"button",
|
|
9727
|
+
"link",
|
|
9728
|
+
"textbox",
|
|
9729
|
+
"checkbox",
|
|
9730
|
+
"radio",
|
|
9731
|
+
"combobox",
|
|
9732
|
+
"listbox",
|
|
9733
|
+
"menuitem",
|
|
9734
|
+
"tab",
|
|
9735
|
+
"switch",
|
|
9736
|
+
"searchbox",
|
|
9737
|
+
"spinbutton",
|
|
9738
|
+
"slider"
|
|
9739
|
+
]);
|
|
9740
|
+
function buildFingerprintMap(nodes) {
|
|
9741
|
+
const map = /* @__PURE__ */ new Map();
|
|
9742
|
+
function walk(nodeList, headingTrail, nearestHeading) {
|
|
9743
|
+
const roleCounts = /* @__PURE__ */ new Map();
|
|
9744
|
+
for (const node of nodeList) {
|
|
9745
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
9746
|
+
let currentHeadingTrail = headingTrail;
|
|
9747
|
+
let currentNearestHeading = nearestHeading;
|
|
9748
|
+
if (role === "heading" && node.name) {
|
|
9749
|
+
currentHeadingTrail = [...headingTrail, node.name];
|
|
9750
|
+
currentNearestHeading = node.name;
|
|
9751
|
+
}
|
|
9752
|
+
if (INTERACTIVE_ROLES.has(role) && node.ref) {
|
|
9753
|
+
const siblingCount = roleCounts.get(role) ?? 0;
|
|
9754
|
+
roleCounts.set(role, siblingCount + 1);
|
|
9755
|
+
const fp = createFingerprint(node, {
|
|
9756
|
+
headingTrail: currentHeadingTrail,
|
|
9757
|
+
siblingIndex: siblingCount,
|
|
9758
|
+
nearestHeading: currentNearestHeading
|
|
9759
|
+
});
|
|
9760
|
+
map.set(node.ref, fp);
|
|
9761
|
+
}
|
|
9762
|
+
if (node.children) {
|
|
9763
|
+
walk(node.children, currentHeadingTrail, currentNearestHeading);
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9766
|
+
}
|
|
9767
|
+
walk(nodes, [], "");
|
|
9768
|
+
return map;
|
|
9769
|
+
}
|
|
9770
|
+
function recoverStaleRef(staleFingerprint, currentFingerprints, threshold = 0.7) {
|
|
9771
|
+
let bestRef = null;
|
|
9772
|
+
let bestScore = 0;
|
|
9773
|
+
let secondBestScore = 0;
|
|
9774
|
+
for (const [ref, fp] of currentFingerprints) {
|
|
9775
|
+
const similarity = fingerprintSimilarity(staleFingerprint, fp);
|
|
9776
|
+
if (similarity > bestScore) {
|
|
9777
|
+
secondBestScore = bestScore;
|
|
9778
|
+
bestScore = similarity;
|
|
9779
|
+
bestRef = ref;
|
|
9780
|
+
} else if (similarity > secondBestScore) {
|
|
9781
|
+
secondBestScore = similarity;
|
|
9782
|
+
}
|
|
9783
|
+
}
|
|
9784
|
+
if (!bestRef || bestScore < threshold) return null;
|
|
9785
|
+
if (secondBestScore > 0 && bestScore - secondBestScore < 0.15) return null;
|
|
9786
|
+
return { ref: bestRef, confidence: bestScore };
|
|
9787
|
+
}
|
|
9788
|
+
|
|
9789
|
+
// src/browser/overlay-detect.ts
|
|
9790
|
+
async function detectOverlay(page) {
|
|
9791
|
+
const result = await page.evaluate(`(() => {
|
|
9792
|
+
// Check for role="dialog" or role="alertdialog"
|
|
9793
|
+
const dialogs = document.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog[open]');
|
|
9794
|
+
for (const d of dialogs) {
|
|
9795
|
+
if (d.offsetParent !== null || getComputedStyle(d).display !== 'none') {
|
|
9796
|
+
return {
|
|
9797
|
+
hasOverlay: true,
|
|
9798
|
+
overlaySelector: d.id ? '#' + d.id : (d.getAttribute('role') ? '[role="' + d.getAttribute('role') + '"]' : 'dialog'),
|
|
9799
|
+
overlayText: (d.textContent || '').trim().slice(0, 200),
|
|
9800
|
+
};
|
|
9801
|
+
}
|
|
9802
|
+
}
|
|
9803
|
+
|
|
9804
|
+
// Check for fixed/absolute positioned elements with high z-index that look like modals
|
|
9805
|
+
const allElements = document.querySelectorAll('*');
|
|
9806
|
+
for (const el of allElements) {
|
|
9807
|
+
const style = getComputedStyle(el);
|
|
9808
|
+
if (
|
|
9809
|
+
(style.position === 'fixed' || style.position === 'absolute') &&
|
|
9810
|
+
parseInt(style.zIndex || '0', 10) > 999 &&
|
|
9811
|
+
el.offsetWidth > 100 &&
|
|
9812
|
+
el.offsetHeight > 100 &&
|
|
9813
|
+
style.display !== 'none' &&
|
|
9814
|
+
style.visibility !== 'hidden'
|
|
9815
|
+
) {
|
|
9816
|
+
const text = (el.textContent || '').trim();
|
|
9817
|
+
if (text.length > 10) {
|
|
9818
|
+
return {
|
|
9819
|
+
hasOverlay: true,
|
|
9820
|
+
overlaySelector: el.id ? '#' + el.id : null,
|
|
9821
|
+
overlayText: text.slice(0, 200),
|
|
9822
|
+
};
|
|
9823
|
+
}
|
|
9824
|
+
}
|
|
9825
|
+
}
|
|
9826
|
+
|
|
9827
|
+
return { hasOverlay: false };
|
|
9828
|
+
})()`);
|
|
9829
|
+
return result ?? { hasOverlay: false };
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
// src/browser/safe-submit.ts
|
|
9833
|
+
async function submitAndVerify(page, options) {
|
|
9834
|
+
const {
|
|
9835
|
+
selector,
|
|
9836
|
+
method = "enter+click",
|
|
9837
|
+
expectAny,
|
|
9838
|
+
expectAll,
|
|
9839
|
+
failIf,
|
|
9840
|
+
dangerous = false,
|
|
9841
|
+
timeout = 3e4,
|
|
9842
|
+
waitForNavigation: waitForNavigation2 = "auto"
|
|
9843
|
+
} = options;
|
|
9844
|
+
const startTime = Date.now();
|
|
9845
|
+
const allConditions = [...expectAny ?? [], ...expectAll ?? [], ...failIf ?? []];
|
|
9846
|
+
const needsNetwork = allConditions.some((c) => c.kind === "networkResponse");
|
|
9847
|
+
const needsSignature = allConditions.some((c) => c.kind === "stateSignatureChanges");
|
|
9848
|
+
let networkTracker;
|
|
9849
|
+
let beforeSignature;
|
|
9850
|
+
if (needsNetwork) {
|
|
9851
|
+
networkTracker = new NetworkResponseTracker();
|
|
9852
|
+
networkTracker.start(page.cdpClient);
|
|
9853
|
+
}
|
|
9854
|
+
if (needsSignature) {
|
|
9855
|
+
beforeSignature = await captureStateSignature(page);
|
|
9856
|
+
}
|
|
9857
|
+
try {
|
|
9858
|
+
await page.submit(selector, {
|
|
9859
|
+
timeout,
|
|
9860
|
+
method,
|
|
9861
|
+
waitForNavigation: waitForNavigation2
|
|
9862
|
+
});
|
|
9863
|
+
if (networkTracker) networkTracker.stop(page.cdpClient);
|
|
9864
|
+
if (allConditions.length === 0) {
|
|
9865
|
+
return {
|
|
9866
|
+
submitted: true,
|
|
9867
|
+
outcomeStatus: "success",
|
|
9868
|
+
matchedConditions: [],
|
|
9869
|
+
retrySafe: !dangerous,
|
|
9870
|
+
durationMs: Date.now() - startTime
|
|
9871
|
+
};
|
|
9872
|
+
}
|
|
9873
|
+
const outcome = await evaluateOutcome(page, {
|
|
9874
|
+
expectAny,
|
|
9875
|
+
expectAll,
|
|
9876
|
+
failIf,
|
|
9877
|
+
dangerous,
|
|
9878
|
+
networkTracker,
|
|
9879
|
+
beforeSignature
|
|
9880
|
+
});
|
|
9881
|
+
return {
|
|
9882
|
+
submitted: true,
|
|
9883
|
+
outcomeStatus: outcome.outcomeStatus,
|
|
9884
|
+
matchedConditions: outcome.matchedConditions,
|
|
9885
|
+
retrySafe: outcome.retrySafe,
|
|
9886
|
+
durationMs: Date.now() - startTime
|
|
9887
|
+
};
|
|
9888
|
+
} catch (error) {
|
|
9889
|
+
if (networkTracker) networkTracker.stop(page.cdpClient);
|
|
9890
|
+
return {
|
|
9891
|
+
submitted: false,
|
|
9892
|
+
outcomeStatus: "failed",
|
|
9893
|
+
matchedConditions: [],
|
|
9894
|
+
retrySafe: !dangerous,
|
|
9895
|
+
durationMs: Date.now() - startTime,
|
|
9896
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9897
|
+
};
|
|
9898
|
+
}
|
|
9899
|
+
}
|
|
9900
|
+
|
|
9901
|
+
// src/runtime/clock.ts
|
|
9902
|
+
function now() {
|
|
9903
|
+
return Date.now();
|
|
9904
|
+
}
|
|
9905
|
+
|
|
9906
|
+
// src/browser/target-pin.ts
|
|
9907
|
+
function createTargetFingerprint(targetId, url, title) {
|
|
9908
|
+
return {
|
|
9909
|
+
url,
|
|
9910
|
+
title,
|
|
9911
|
+
originalTargetId: targetId,
|
|
9912
|
+
pinnedAt: now()
|
|
9913
|
+
};
|
|
9914
|
+
}
|
|
9915
|
+
function scoreCandidate(candidate, pin) {
|
|
9916
|
+
if (candidate.targetId === pin.originalTargetId) return 1;
|
|
9917
|
+
let score = 0;
|
|
9918
|
+
if (candidate.url && pin.url) {
|
|
9919
|
+
if (candidate.url === pin.url) {
|
|
9920
|
+
score += 0.6;
|
|
9921
|
+
} else {
|
|
9922
|
+
try {
|
|
9923
|
+
const candidateOrigin = new URL(candidate.url).origin;
|
|
9924
|
+
const pinOrigin = new URL(pin.url).origin;
|
|
9925
|
+
if (candidateOrigin === pinOrigin) score += 0.3;
|
|
9926
|
+
} catch {
|
|
9927
|
+
}
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
if (candidate.title && pin.title) {
|
|
9931
|
+
if (candidate.title === pin.title) {
|
|
9932
|
+
score += 0.3;
|
|
9933
|
+
} else if (candidate.title.includes(pin.title) || pin.title.includes(candidate.title)) {
|
|
9934
|
+
score += 0.15;
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
if (candidate.type !== "page") score *= 0.5;
|
|
9938
|
+
return Math.min(score, 0.95);
|
|
9939
|
+
}
|
|
9940
|
+
function recoverPinnedTarget(pin, targets, threshold = 0.4) {
|
|
9941
|
+
if (targets.length === 0) return null;
|
|
9942
|
+
let bestTarget = null;
|
|
9943
|
+
let bestScore = 0;
|
|
9944
|
+
for (const target of targets) {
|
|
9945
|
+
const score = scoreCandidate(target, pin);
|
|
9946
|
+
if (score > bestScore) {
|
|
9947
|
+
bestScore = score;
|
|
9948
|
+
bestTarget = target;
|
|
9949
|
+
}
|
|
9950
|
+
}
|
|
9951
|
+
if (!bestTarget || bestScore < threshold) return null;
|
|
9952
|
+
let method;
|
|
9953
|
+
if (bestTarget.targetId === pin.originalTargetId) {
|
|
9954
|
+
method = "exact";
|
|
9955
|
+
} else if (bestTarget.url === pin.url) {
|
|
9956
|
+
method = "url_match";
|
|
9957
|
+
} else if (bestTarget.title === pin.title) {
|
|
9958
|
+
method = "title_match";
|
|
9959
|
+
} else {
|
|
9960
|
+
method = "best_guess";
|
|
9961
|
+
}
|
|
9962
|
+
return {
|
|
9963
|
+
targetId: bestTarget.targetId,
|
|
9964
|
+
method,
|
|
9965
|
+
confidence: bestScore
|
|
9966
|
+
};
|
|
9967
|
+
}
|
|
9968
|
+
|
|
9969
|
+
// src/browser/index.ts
|
|
9970
|
+
init_upload();
|
|
8819
9971
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8820
9972
|
0 && (module.exports = {
|
|
8821
9973
|
Browser,
|
|
@@ -8823,5 +9975,19 @@ function connect(options) {
|
|
|
8823
9975
|
NavigationError,
|
|
8824
9976
|
Page,
|
|
8825
9977
|
TimeoutError,
|
|
8826
|
-
|
|
9978
|
+
buildFingerprintMap,
|
|
9979
|
+
chooseOption,
|
|
9980
|
+
computeDelta,
|
|
9981
|
+
connect,
|
|
9982
|
+
createFingerprint,
|
|
9983
|
+
createTargetFingerprint,
|
|
9984
|
+
detectOverlay,
|
|
9985
|
+
extractPageState,
|
|
9986
|
+
extractReview,
|
|
9987
|
+
fingerprintKey,
|
|
9988
|
+
fingerprintSimilarity,
|
|
9989
|
+
recoverPinnedTarget,
|
|
9990
|
+
recoverStaleRef,
|
|
9991
|
+
submitAndVerify,
|
|
9992
|
+
uploadFiles
|
|
8827
9993
|
});
|