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
|
@@ -2,6 +2,333 @@ import {
|
|
|
2
2
|
CDPError
|
|
3
3
|
} from "./chunk-JXAUPHZM.mjs";
|
|
4
4
|
|
|
5
|
+
// src/utils/strings.ts
|
|
6
|
+
function readString(value) {
|
|
7
|
+
return typeof value === "string" ? value : void 0;
|
|
8
|
+
}
|
|
9
|
+
function readStringOr(value, fallback = "") {
|
|
10
|
+
return readString(value) ?? fallback;
|
|
11
|
+
}
|
|
12
|
+
function formatConsoleArg(entry) {
|
|
13
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
14
|
+
}
|
|
15
|
+
function globToRegex(pattern) {
|
|
16
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
17
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
18
|
+
return new RegExp(`^${withWildcards}$`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/actions/conditions.ts
|
|
22
|
+
var NetworkResponseTracker = class {
|
|
23
|
+
responses = [];
|
|
24
|
+
listening = false;
|
|
25
|
+
handler = null;
|
|
26
|
+
start(cdp) {
|
|
27
|
+
if (this.listening) return;
|
|
28
|
+
this.listening = true;
|
|
29
|
+
this.handler = (params) => {
|
|
30
|
+
const response = params["response"];
|
|
31
|
+
if (response) {
|
|
32
|
+
this.responses.push({ url: response.url, status: response.status });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
cdp.on("Network.responseReceived", this.handler);
|
|
36
|
+
}
|
|
37
|
+
stop(cdp) {
|
|
38
|
+
if (this.handler) {
|
|
39
|
+
cdp.off("Network.responseReceived", this.handler);
|
|
40
|
+
this.handler = null;
|
|
41
|
+
}
|
|
42
|
+
this.listening = false;
|
|
43
|
+
}
|
|
44
|
+
getResponses() {
|
|
45
|
+
return this.responses;
|
|
46
|
+
}
|
|
47
|
+
reset() {
|
|
48
|
+
this.responses = [];
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
async function captureStateSignature(page) {
|
|
52
|
+
try {
|
|
53
|
+
const url = await page.url();
|
|
54
|
+
const text = await page.text();
|
|
55
|
+
const truncated = text.slice(0, 2e3);
|
|
56
|
+
return `${url}|${simpleHash(truncated)}`;
|
|
57
|
+
} catch {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function simpleHash(str) {
|
|
62
|
+
let hash = 0;
|
|
63
|
+
for (let i = 0; i < str.length; i++) {
|
|
64
|
+
const char = str.charCodeAt(i);
|
|
65
|
+
hash = (hash << 5) - hash + char | 0;
|
|
66
|
+
}
|
|
67
|
+
return hash.toString(36);
|
|
68
|
+
}
|
|
69
|
+
async function evaluateCondition(condition, page, context = {}) {
|
|
70
|
+
switch (condition.kind) {
|
|
71
|
+
case "urlMatches": {
|
|
72
|
+
try {
|
|
73
|
+
const currentUrl = await page.url();
|
|
74
|
+
const regex = globToRegex(condition.pattern);
|
|
75
|
+
const matched = regex.test(currentUrl);
|
|
76
|
+
return {
|
|
77
|
+
condition,
|
|
78
|
+
matched,
|
|
79
|
+
detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
|
|
80
|
+
};
|
|
81
|
+
} catch {
|
|
82
|
+
return { condition, matched: false, detail: "Failed to get current URL" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
case "elementVisible": {
|
|
86
|
+
try {
|
|
87
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
88
|
+
for (const sel of selectors) {
|
|
89
|
+
const visible = await page.waitFor(sel, {
|
|
90
|
+
timeout: 2e3,
|
|
91
|
+
optional: true,
|
|
92
|
+
state: "visible"
|
|
93
|
+
});
|
|
94
|
+
if (visible) {
|
|
95
|
+
return { condition, matched: true, detail: `Element "${sel}" is visible` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { condition, matched: false, detail: "No matching visible element found" };
|
|
99
|
+
} catch {
|
|
100
|
+
return { condition, matched: false, detail: "Visibility check failed" };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
case "elementHidden": {
|
|
104
|
+
try {
|
|
105
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
106
|
+
for (const sel of selectors) {
|
|
107
|
+
const visible = await page.waitFor(sel, {
|
|
108
|
+
timeout: 500,
|
|
109
|
+
optional: true,
|
|
110
|
+
state: "visible"
|
|
111
|
+
});
|
|
112
|
+
if (visible) {
|
|
113
|
+
return { condition, matched: false, detail: `Element "${sel}" is still visible` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { condition, matched: true, detail: "Element is hidden or not found" };
|
|
117
|
+
} catch {
|
|
118
|
+
return { condition, matched: true, detail: "Element is hidden (check threw)" };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
case "textAppears": {
|
|
122
|
+
try {
|
|
123
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
124
|
+
const text = await page.text(selector);
|
|
125
|
+
const matched = text.includes(condition.text);
|
|
126
|
+
return {
|
|
127
|
+
condition,
|
|
128
|
+
matched,
|
|
129
|
+
detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
|
|
130
|
+
};
|
|
131
|
+
} catch {
|
|
132
|
+
return { condition, matched: false, detail: "Failed to get page text" };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
case "textChanges": {
|
|
136
|
+
try {
|
|
137
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
138
|
+
const text = await page.text(selector);
|
|
139
|
+
if (condition.to !== void 0) {
|
|
140
|
+
const matched = text.includes(condition.to);
|
|
141
|
+
return {
|
|
142
|
+
condition,
|
|
143
|
+
matched,
|
|
144
|
+
detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
|
|
148
|
+
} catch {
|
|
149
|
+
return { condition, matched: false, detail: "Failed to get text for change detection" };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
case "networkResponse": {
|
|
153
|
+
const tracker = context.networkTracker;
|
|
154
|
+
if (!tracker) {
|
|
155
|
+
return { condition, matched: false, detail: "No network tracker active" };
|
|
156
|
+
}
|
|
157
|
+
const regex = globToRegex(condition.urlPattern);
|
|
158
|
+
const responses = tracker.getResponses();
|
|
159
|
+
for (const resp of responses) {
|
|
160
|
+
if (regex.test(resp.url)) {
|
|
161
|
+
if (condition.status !== void 0 && resp.status !== condition.status) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
condition,
|
|
166
|
+
matched: true,
|
|
167
|
+
detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
condition,
|
|
173
|
+
matched: false,
|
|
174
|
+
detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
case "stateSignatureChanges": {
|
|
178
|
+
if (!context.beforeSignature) {
|
|
179
|
+
return { condition, matched: false, detail: "No before-signature captured" };
|
|
180
|
+
}
|
|
181
|
+
const afterSignature = await captureStateSignature(page);
|
|
182
|
+
const matched = afterSignature !== context.beforeSignature;
|
|
183
|
+
return {
|
|
184
|
+
condition,
|
|
185
|
+
matched,
|
|
186
|
+
detail: matched ? "Page state changed" : "Page state unchanged"
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
default: {
|
|
190
|
+
const _exhaustive = condition;
|
|
191
|
+
return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function evaluateOutcome(page, options) {
|
|
196
|
+
const {
|
|
197
|
+
expectAny,
|
|
198
|
+
expectAll,
|
|
199
|
+
failIf,
|
|
200
|
+
dangerous = false,
|
|
201
|
+
networkTracker,
|
|
202
|
+
beforeSignature
|
|
203
|
+
} = options;
|
|
204
|
+
const allMatched = [];
|
|
205
|
+
const context = { networkTracker, beforeSignature };
|
|
206
|
+
if (failIf && failIf.length > 0) {
|
|
207
|
+
for (const condition of failIf) {
|
|
208
|
+
const result = await evaluateCondition(condition, page, context);
|
|
209
|
+
allMatched.push(result);
|
|
210
|
+
if (result.matched) {
|
|
211
|
+
return {
|
|
212
|
+
outcomeStatus: "failed",
|
|
213
|
+
matchedConditions: allMatched,
|
|
214
|
+
retrySafe: !dangerous
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (expectAll && expectAll.length > 0) {
|
|
220
|
+
let allPassed = true;
|
|
221
|
+
for (const condition of expectAll) {
|
|
222
|
+
const result = await evaluateCondition(condition, page, context);
|
|
223
|
+
allMatched.push(result);
|
|
224
|
+
if (!result.matched) {
|
|
225
|
+
allPassed = false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!allPassed) {
|
|
229
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
230
|
+
return {
|
|
231
|
+
outcomeStatus: status,
|
|
232
|
+
matchedConditions: allMatched,
|
|
233
|
+
retrySafe: !dangerous
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (!expectAny || expectAny.length === 0) {
|
|
237
|
+
return {
|
|
238
|
+
outcomeStatus: "success",
|
|
239
|
+
matchedConditions: allMatched,
|
|
240
|
+
retrySafe: true
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (expectAny && expectAny.length > 0) {
|
|
245
|
+
let anyPassed = false;
|
|
246
|
+
for (const condition of expectAny) {
|
|
247
|
+
const result = await evaluateCondition(condition, page, context);
|
|
248
|
+
allMatched.push(result);
|
|
249
|
+
if (result.matched) {
|
|
250
|
+
anyPassed = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (anyPassed) {
|
|
254
|
+
return {
|
|
255
|
+
outcomeStatus: "success",
|
|
256
|
+
matchedConditions: allMatched,
|
|
257
|
+
retrySafe: true
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
261
|
+
return {
|
|
262
|
+
outcomeStatus: status,
|
|
263
|
+
matchedConditions: allMatched,
|
|
264
|
+
retrySafe: !dangerous
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
outcomeStatus: "success",
|
|
269
|
+
matchedConditions: allMatched,
|
|
270
|
+
retrySafe: true
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/actions/combinators.ts
|
|
275
|
+
async function conditionAny(conditions, page, context) {
|
|
276
|
+
const results = [];
|
|
277
|
+
let winnerIndex;
|
|
278
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
279
|
+
const result = await evaluateCondition(conditions[i], page, context);
|
|
280
|
+
results.push(result);
|
|
281
|
+
if (result.matched && winnerIndex === void 0) {
|
|
282
|
+
winnerIndex = i;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
matched: winnerIndex !== void 0,
|
|
287
|
+
matchedConditions: results,
|
|
288
|
+
winnerIndex
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async function conditionAll(conditions, page, context) {
|
|
292
|
+
const results = [];
|
|
293
|
+
let allMatched = true;
|
|
294
|
+
for (const condition of conditions) {
|
|
295
|
+
const result = await evaluateCondition(condition, page, context);
|
|
296
|
+
results.push(result);
|
|
297
|
+
if (!result.matched) allMatched = false;
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
matched: allMatched,
|
|
301
|
+
matchedConditions: results
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function conditionNot(condition, page, context) {
|
|
305
|
+
const result = await evaluateCondition(condition, page, context);
|
|
306
|
+
return {
|
|
307
|
+
matched: !result.matched,
|
|
308
|
+
matchedConditions: [
|
|
309
|
+
{
|
|
310
|
+
condition: result.condition,
|
|
311
|
+
matched: !result.matched,
|
|
312
|
+
detail: result.matched ? `NOT: condition was true (inverted to false): ${result.detail}` : `NOT: condition was false (inverted to true): ${result.detail}`
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function conditionRace(conditions, page, options = {}) {
|
|
318
|
+
const { timeout = 1e4, pollInterval = 200, networkTracker, beforeSignature } = options;
|
|
319
|
+
const context = { networkTracker, beforeSignature };
|
|
320
|
+
const startTime = Date.now();
|
|
321
|
+
const deadline = startTime + timeout;
|
|
322
|
+
const immediate = await conditionAny(conditions, page, context);
|
|
323
|
+
if (immediate.matched) return immediate;
|
|
324
|
+
while (Date.now() < deadline) {
|
|
325
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
326
|
+
const result = await conditionAny(conditions, page, context);
|
|
327
|
+
if (result.matched) return result;
|
|
328
|
+
}
|
|
329
|
+
return await conditionAny(conditions, page, context);
|
|
330
|
+
}
|
|
331
|
+
|
|
5
332
|
// src/actions/executor.ts
|
|
6
333
|
import * as fs from "fs";
|
|
7
334
|
import { join } from "path";
|
|
@@ -1449,13 +1776,6 @@ var TRACE_SCRIPT = `
|
|
|
1449
1776
|
})();
|
|
1450
1777
|
`;
|
|
1451
1778
|
|
|
1452
|
-
// src/trace/live.ts
|
|
1453
|
-
function globToRegex(pattern) {
|
|
1454
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1455
|
-
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1456
|
-
return new RegExp(`^${withWildcards}$`);
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
1779
|
// src/actions/executor.ts
|
|
1460
1780
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1461
1781
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -1465,15 +1785,6 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
1465
1785
|
"text",
|
|
1466
1786
|
"screenshot"
|
|
1467
1787
|
];
|
|
1468
|
-
function readString(value) {
|
|
1469
|
-
return typeof value === "string" ? value : void 0;
|
|
1470
|
-
}
|
|
1471
|
-
function readStringOr(value, fallback = "") {
|
|
1472
|
-
return readString(value) ?? fallback;
|
|
1473
|
-
}
|
|
1474
|
-
function formatConsoleArg(entry) {
|
|
1475
|
-
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
1476
|
-
}
|
|
1477
1788
|
function loadExistingRecording(manifestPath) {
|
|
1478
1789
|
try {
|
|
1479
1790
|
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
@@ -1587,6 +1898,25 @@ function getSuggestion(reason) {
|
|
|
1587
1898
|
}
|
|
1588
1899
|
}
|
|
1589
1900
|
}
|
|
1901
|
+
function hasOutcomeConditions(step) {
|
|
1902
|
+
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;
|
|
1903
|
+
}
|
|
1904
|
+
function needsNetworkTracking(step) {
|
|
1905
|
+
const allConditions = [
|
|
1906
|
+
...step.expectAny ?? [],
|
|
1907
|
+
...step.expectAll ?? [],
|
|
1908
|
+
...step.failIf ?? []
|
|
1909
|
+
];
|
|
1910
|
+
return allConditions.some((c) => c.kind === "networkResponse");
|
|
1911
|
+
}
|
|
1912
|
+
function needsStateSignature(step) {
|
|
1913
|
+
const allConditions = [
|
|
1914
|
+
...step.expectAny ?? [],
|
|
1915
|
+
...step.expectAll ?? [],
|
|
1916
|
+
...step.failIf ?? []
|
|
1917
|
+
];
|
|
1918
|
+
return allConditions.some((c) => c.kind === "stateSignatureChanges");
|
|
1919
|
+
}
|
|
1590
1920
|
var BatchExecutor = class {
|
|
1591
1921
|
page;
|
|
1592
1922
|
constructor(page) {
|
|
@@ -1632,9 +1962,25 @@ var BatchExecutor = class {
|
|
|
1632
1962
|
})
|
|
1633
1963
|
);
|
|
1634
1964
|
}
|
|
1965
|
+
const hasOutcome = hasOutcomeConditions(step);
|
|
1966
|
+
let networkTracker;
|
|
1967
|
+
let beforeSignature;
|
|
1968
|
+
if (hasOutcome) {
|
|
1969
|
+
if (needsNetworkTracking(step)) {
|
|
1970
|
+
networkTracker = new NetworkResponseTracker();
|
|
1971
|
+
networkTracker.start(this.page.cdpClient);
|
|
1972
|
+
}
|
|
1973
|
+
if (needsStateSignature(step)) {
|
|
1974
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1635
1977
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1636
1978
|
if (attempt > 0) {
|
|
1637
1979
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1980
|
+
if (networkTracker) networkTracker.reset();
|
|
1981
|
+
if (hasOutcome && needsStateSignature(step)) {
|
|
1982
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
1983
|
+
}
|
|
1638
1984
|
}
|
|
1639
1985
|
try {
|
|
1640
1986
|
this.page.resetLastActionPosition();
|
|
@@ -1652,6 +1998,28 @@ var BatchExecutor = class {
|
|
|
1652
1998
|
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
1653
1999
|
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
1654
2000
|
};
|
|
2001
|
+
if (hasOutcome) {
|
|
2002
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
2003
|
+
const outcome = await evaluateOutcome(this.page, {
|
|
2004
|
+
expectAny: step.expectAny,
|
|
2005
|
+
expectAll: step.expectAll,
|
|
2006
|
+
failIf: step.failIf,
|
|
2007
|
+
dangerous: step.dangerous,
|
|
2008
|
+
networkTracker,
|
|
2009
|
+
beforeSignature
|
|
2010
|
+
});
|
|
2011
|
+
stepResult.outcomeStatus = outcome.outcomeStatus;
|
|
2012
|
+
stepResult.matchedConditions = outcome.matchedConditions;
|
|
2013
|
+
stepResult.retrySafe = outcome.retrySafe;
|
|
2014
|
+
if (outcome.outcomeStatus !== "success") {
|
|
2015
|
+
stepResult.success = false;
|
|
2016
|
+
stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
|
|
2017
|
+
const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
|
|
2018
|
+
if (failedDetails.length > 0) {
|
|
2019
|
+
stepResult.suggestion = failedDetails.join("; ");
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
1655
2023
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1656
2024
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1657
2025
|
}
|
|
@@ -1661,13 +2029,14 @@ var BatchExecutor = class {
|
|
|
1661
2029
|
traceId: createTraceId("action"),
|
|
1662
2030
|
elapsedMs: Date.now() - startTime,
|
|
1663
2031
|
channel: "action",
|
|
1664
|
-
event: "action.succeeded",
|
|
1665
|
-
summary: `${step.action} succeeded`,
|
|
2032
|
+
event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
|
|
2033
|
+
summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
|
|
1666
2034
|
data: {
|
|
1667
2035
|
action: step.action,
|
|
1668
2036
|
selector: step.selector ?? null,
|
|
1669
2037
|
selectorUsed: result.selectorUsed ?? null,
|
|
1670
|
-
durationMs: Date.now() - stepStart
|
|
2038
|
+
durationMs: Date.now() - stepStart,
|
|
2039
|
+
outcomeStatus: stepResult.outcomeStatus ?? null
|
|
1671
2040
|
},
|
|
1672
2041
|
actionId: `action-${i + 1}`,
|
|
1673
2042
|
stepIndex: i,
|
|
@@ -1677,6 +2046,18 @@ var BatchExecutor = class {
|
|
|
1677
2046
|
})
|
|
1678
2047
|
);
|
|
1679
2048
|
}
|
|
2049
|
+
if (hasOutcome && !stepResult.success) {
|
|
2050
|
+
if (step.dangerous) {
|
|
2051
|
+
results.push(stepResult);
|
|
2052
|
+
break;
|
|
2053
|
+
}
|
|
2054
|
+
if (attempt < maxAttempts - 1) {
|
|
2055
|
+
lastError = new Error(stepResult.error ?? "Outcome failed");
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
results.push(stepResult);
|
|
2059
|
+
break;
|
|
2060
|
+
}
|
|
1680
2061
|
results.push(stepResult);
|
|
1681
2062
|
succeeded = true;
|
|
1682
2063
|
break;
|
|
@@ -1684,59 +2065,63 @@ var BatchExecutor = class {
|
|
|
1684
2065
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1685
2066
|
}
|
|
1686
2067
|
}
|
|
2068
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1687
2069
|
if (!succeeded) {
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2070
|
+
const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
|
|
2071
|
+
if (!resultAlreadyPushed) {
|
|
2072
|
+
const errorMessage = lastError?.message ?? "Unknown error";
|
|
2073
|
+
let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
|
|
2074
|
+
const { reason, coveringElement } = classifyFailure(lastError);
|
|
2075
|
+
if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
|
|
2076
|
+
try {
|
|
2077
|
+
const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
|
|
2078
|
+
const autoHints = await generateHints(this.page, selectors, step.action, 3);
|
|
2079
|
+
if (autoHints.length > 0) {
|
|
2080
|
+
hints = autoHints;
|
|
2081
|
+
}
|
|
2082
|
+
} catch {
|
|
1697
2083
|
}
|
|
1698
|
-
} catch {
|
|
1699
2084
|
}
|
|
2085
|
+
const failedResult = {
|
|
2086
|
+
index: i,
|
|
2087
|
+
action: step.action,
|
|
2088
|
+
selector: step.selector,
|
|
2089
|
+
success: false,
|
|
2090
|
+
durationMs: Date.now() - stepStart,
|
|
2091
|
+
error: errorMessage,
|
|
2092
|
+
hints,
|
|
2093
|
+
failureReason: reason,
|
|
2094
|
+
coveringElement,
|
|
2095
|
+
suggestion: getSuggestion(reason),
|
|
2096
|
+
timestamp: Date.now()
|
|
2097
|
+
};
|
|
2098
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
2099
|
+
await this.captureRecordingFrame(step, failedResult, recording);
|
|
2100
|
+
}
|
|
2101
|
+
if (recording) {
|
|
2102
|
+
recording.traceEvents.push(
|
|
2103
|
+
normalizeTraceEvent({
|
|
2104
|
+
traceId: createTraceId("action"),
|
|
2105
|
+
elapsedMs: Date.now() - startTime,
|
|
2106
|
+
channel: "action",
|
|
2107
|
+
event: "action.failed",
|
|
2108
|
+
severity: "error",
|
|
2109
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
2110
|
+
data: {
|
|
2111
|
+
action: step.action,
|
|
2112
|
+
selector: step.selector ?? null,
|
|
2113
|
+
error: errorMessage,
|
|
2114
|
+
reason
|
|
2115
|
+
},
|
|
2116
|
+
actionId: `action-${i + 1}`,
|
|
2117
|
+
stepIndex: i,
|
|
2118
|
+
selector: step.selector,
|
|
2119
|
+
url: step.url
|
|
2120
|
+
})
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
results.push(failedResult);
|
|
1700
2124
|
}
|
|
1701
|
-
const failedResult = {
|
|
1702
|
-
index: i,
|
|
1703
|
-
action: step.action,
|
|
1704
|
-
selector: step.selector,
|
|
1705
|
-
success: false,
|
|
1706
|
-
durationMs: Date.now() - stepStart,
|
|
1707
|
-
error: errorMessage,
|
|
1708
|
-
hints,
|
|
1709
|
-
failureReason: reason,
|
|
1710
|
-
coveringElement,
|
|
1711
|
-
suggestion: getSuggestion(reason),
|
|
1712
|
-
timestamp: Date.now()
|
|
1713
|
-
};
|
|
1714
|
-
if (recording && !recording.skipActions.has(step.action)) {
|
|
1715
|
-
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1716
|
-
}
|
|
1717
|
-
if (recording) {
|
|
1718
|
-
recording.traceEvents.push(
|
|
1719
|
-
normalizeTraceEvent({
|
|
1720
|
-
traceId: createTraceId("action"),
|
|
1721
|
-
elapsedMs: Date.now() - startTime,
|
|
1722
|
-
channel: "action",
|
|
1723
|
-
event: "action.failed",
|
|
1724
|
-
severity: "error",
|
|
1725
|
-
summary: `${step.action} failed: ${errorMessage}`,
|
|
1726
|
-
data: {
|
|
1727
|
-
action: step.action,
|
|
1728
|
-
selector: step.selector ?? null,
|
|
1729
|
-
error: errorMessage,
|
|
1730
|
-
reason
|
|
1731
|
-
},
|
|
1732
|
-
actionId: `action-${i + 1}`,
|
|
1733
|
-
stepIndex: i,
|
|
1734
|
-
selector: step.selector,
|
|
1735
|
-
url: step.url
|
|
1736
|
-
})
|
|
1737
|
-
);
|
|
1738
|
-
}
|
|
1739
|
-
results.push(failedResult);
|
|
1740
2125
|
if (onFail === "stop" && !step.optional) {
|
|
1741
2126
|
stoppedAtIndex = i;
|
|
1742
2127
|
break;
|
|
@@ -2047,6 +2432,14 @@ var BatchExecutor = class {
|
|
|
2047
2432
|
case "forms": {
|
|
2048
2433
|
return { value: await this.page.forms() };
|
|
2049
2434
|
}
|
|
2435
|
+
case "delta": {
|
|
2436
|
+
const review = await this.page.review();
|
|
2437
|
+
return { value: review };
|
|
2438
|
+
}
|
|
2439
|
+
case "review": {
|
|
2440
|
+
const review = await this.page.review();
|
|
2441
|
+
return { value: review };
|
|
2442
|
+
}
|
|
2050
2443
|
case "screenshot": {
|
|
2051
2444
|
const data = await this.page.screenshot({
|
|
2052
2445
|
format: step.format,
|
|
@@ -2197,6 +2590,35 @@ var BatchExecutor = class {
|
|
|
2197
2590
|
const media = await this.assertMediaTrackLive(step.kind);
|
|
2198
2591
|
return { value: media };
|
|
2199
2592
|
}
|
|
2593
|
+
case "chooseOption": {
|
|
2594
|
+
const { chooseOption } = await import("./combobox-RAKBA2BW.mjs");
|
|
2595
|
+
if (!step.value) throw new Error("chooseOption requires value");
|
|
2596
|
+
const result = await chooseOption(this.page, {
|
|
2597
|
+
trigger: step.trigger ?? step.selector ?? "",
|
|
2598
|
+
listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
|
|
2599
|
+
value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
|
|
2600
|
+
match: step.match,
|
|
2601
|
+
timeout: step.timeout ?? timeout
|
|
2602
|
+
});
|
|
2603
|
+
if (!result.success) {
|
|
2604
|
+
throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
|
|
2605
|
+
}
|
|
2606
|
+
return { value: result };
|
|
2607
|
+
}
|
|
2608
|
+
case "upload": {
|
|
2609
|
+
const { uploadFiles } = await import("./upload-E6MCC2OF.mjs");
|
|
2610
|
+
if (!step.selector) throw new Error("upload requires selector");
|
|
2611
|
+
if (!step.files || step.files.length === 0) throw new Error("upload requires files");
|
|
2612
|
+
const result = await uploadFiles(this.page, {
|
|
2613
|
+
selector: step.selector,
|
|
2614
|
+
files: step.files,
|
|
2615
|
+
timeout: step.timeout ?? timeout
|
|
2616
|
+
});
|
|
2617
|
+
if (!result.accepted) {
|
|
2618
|
+
throw new Error(result.error ?? "Upload was not accepted");
|
|
2619
|
+
}
|
|
2620
|
+
return { value: result };
|
|
2621
|
+
}
|
|
2200
2622
|
default: {
|
|
2201
2623
|
const action = step.action;
|
|
2202
2624
|
const aliases = {
|
|
@@ -2790,6 +3212,30 @@ var ACTION_RULES = {
|
|
|
2790
3212
|
kind: { type: "string", enum: ["audio", "video"] }
|
|
2791
3213
|
},
|
|
2792
3214
|
optional: {}
|
|
3215
|
+
},
|
|
3216
|
+
delta: {
|
|
3217
|
+
required: {},
|
|
3218
|
+
optional: {}
|
|
3219
|
+
},
|
|
3220
|
+
review: {
|
|
3221
|
+
required: {},
|
|
3222
|
+
optional: {}
|
|
3223
|
+
},
|
|
3224
|
+
chooseOption: {
|
|
3225
|
+
required: { value: { type: "string|string[]" } },
|
|
3226
|
+
optional: {
|
|
3227
|
+
trigger: { type: "string|string[]" },
|
|
3228
|
+
selector: { type: "string|string[]" },
|
|
3229
|
+
option: { type: "string|string[]" },
|
|
3230
|
+
match: { type: "string", enum: ["exact", "contains", "startsWith"] }
|
|
3231
|
+
}
|
|
3232
|
+
},
|
|
3233
|
+
upload: {
|
|
3234
|
+
required: {
|
|
3235
|
+
selector: { type: "string|string[]" },
|
|
3236
|
+
files: { type: "string|string[]" }
|
|
3237
|
+
},
|
|
3238
|
+
optional: {}
|
|
2793
3239
|
}
|
|
2794
3240
|
};
|
|
2795
3241
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -2829,7 +3275,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2829
3275
|
"name",
|
|
2830
3276
|
"state",
|
|
2831
3277
|
"kind",
|
|
2832
|
-
"windowMs"
|
|
3278
|
+
"windowMs",
|
|
3279
|
+
"expectAny",
|
|
3280
|
+
"expectAll",
|
|
3281
|
+
"failIf",
|
|
3282
|
+
"dangerous",
|
|
3283
|
+
"files"
|
|
2833
3284
|
]);
|
|
2834
3285
|
function resolveAction(name) {
|
|
2835
3286
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -3049,6 +3500,64 @@ function validateSteps(steps) {
|
|
|
3049
3500
|
});
|
|
3050
3501
|
}
|
|
3051
3502
|
}
|
|
3503
|
+
if ("dangerous" in obj && obj["dangerous"] !== void 0) {
|
|
3504
|
+
if (typeof obj["dangerous"] !== "boolean") {
|
|
3505
|
+
errors.push({
|
|
3506
|
+
stepIndex: i,
|
|
3507
|
+
field: "dangerous",
|
|
3508
|
+
message: `"dangerous" expected boolean, got ${typeof obj["dangerous"]}.`
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
for (const condField of ["expectAny", "expectAll", "failIf"]) {
|
|
3513
|
+
if (condField in obj && obj[condField] !== void 0) {
|
|
3514
|
+
if (!Array.isArray(obj[condField])) {
|
|
3515
|
+
errors.push({
|
|
3516
|
+
stepIndex: i,
|
|
3517
|
+
field: condField,
|
|
3518
|
+
message: `"${condField}" expected array, got ${typeof obj[condField]}.`
|
|
3519
|
+
});
|
|
3520
|
+
} else {
|
|
3521
|
+
const conditions = obj[condField];
|
|
3522
|
+
for (let ci = 0; ci < conditions.length; ci++) {
|
|
3523
|
+
const cond = conditions[ci];
|
|
3524
|
+
if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
|
|
3525
|
+
errors.push({
|
|
3526
|
+
stepIndex: i,
|
|
3527
|
+
field: condField,
|
|
3528
|
+
message: `"${condField}[${ci}]" must be a condition object.`
|
|
3529
|
+
});
|
|
3530
|
+
continue;
|
|
3531
|
+
}
|
|
3532
|
+
const condObj = cond;
|
|
3533
|
+
if (!("kind" in condObj) || typeof condObj["kind"] !== "string") {
|
|
3534
|
+
errors.push({
|
|
3535
|
+
stepIndex: i,
|
|
3536
|
+
field: condField,
|
|
3537
|
+
message: `"${condField}[${ci}]" missing required "kind" field.`
|
|
3538
|
+
});
|
|
3539
|
+
} else {
|
|
3540
|
+
const validKinds = [
|
|
3541
|
+
"urlMatches",
|
|
3542
|
+
"elementVisible",
|
|
3543
|
+
"elementHidden",
|
|
3544
|
+
"textAppears",
|
|
3545
|
+
"textChanges",
|
|
3546
|
+
"networkResponse",
|
|
3547
|
+
"stateSignatureChanges"
|
|
3548
|
+
];
|
|
3549
|
+
if (!validKinds.includes(condObj["kind"])) {
|
|
3550
|
+
errors.push({
|
|
3551
|
+
stepIndex: i,
|
|
3552
|
+
field: condField,
|
|
3553
|
+
message: `"${condField}[${ci}].kind" must be one of: ${validKinds.join(", ")}. Got "${condObj["kind"]}".`
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3052
3561
|
if (action === "assertText") {
|
|
3053
3562
|
if (!("expect" in obj) && !("value" in obj)) {
|
|
3054
3563
|
errors.push({
|
|
@@ -3125,6 +3634,14 @@ function validateSteps(steps) {
|
|
|
3125
3634
|
}
|
|
3126
3635
|
|
|
3127
3636
|
export {
|
|
3637
|
+
NetworkResponseTracker,
|
|
3638
|
+
captureStateSignature,
|
|
3639
|
+
evaluateCondition,
|
|
3640
|
+
evaluateOutcome,
|
|
3641
|
+
conditionAny,
|
|
3642
|
+
conditionAll,
|
|
3643
|
+
conditionNot,
|
|
3644
|
+
conditionRace,
|
|
3128
3645
|
ActionabilityError,
|
|
3129
3646
|
ensureActionable,
|
|
3130
3647
|
generateHints,
|