browser-pilot 0.0.15 → 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 +38 -3
- package/dist/actions.cjs +848 -105
- package/dist/actions.d.cts +101 -4
- package/dist/actions.d.ts +101 -4
- package/dist/actions.mjs +17 -1
- package/dist/{browser-MEWT75IB.mjs → browser-4ZHNAQR5.mjs} +2 -2
- package/dist/browser.cjs +1684 -130
- package/dist/browser.d.cts +230 -6
- package/dist/browser.d.ts +230 -6
- package/dist/browser.mjs +37 -5
- package/dist/chunk-EZNZ72VA.mjs +563 -0
- package/dist/{chunk-ZAXQ5OTV.mjs → chunk-FEEGNSHB.mjs} +606 -12
- package/dist/{chunk-WPNW23CE.mjs → chunk-IRLHCVNH.mjs} +345 -7
- package/dist/chunk-MIJ7UIKB.mjs +96 -0
- package/dist/{chunk-USYSHCI3.mjs → chunk-MRY3HRFJ.mjs} +841 -370
- package/dist/chunk-OIHU7OFY.mjs +91 -0
- package/dist/{chunk-7YVCOL2W.mjs → chunk-ZDODXEBD.mjs} +637 -105
- package/dist/cli.mjs +1280 -549
- package/dist/combobox-RAKBA2BW.mjs +6 -0
- package/dist/index.cjs +1976 -144
- package/dist/index.d.cts +57 -6
- package/dist/index.d.ts +57 -6
- package/dist/index.mjs +206 -7
- package/dist/{page-XPS6IC6V.mjs → page-SD64DY3F.mjs} +1 -1
- package/dist/providers.cjs +637 -2
- package/dist/providers.d.cts +2 -2
- package/dist/providers.d.ts +2 -2
- package/dist/providers.mjs +17 -3
- package/dist/{types-Cvvf0oGu.d.ts → types-B_v62K7C.d.ts} +147 -3
- package/dist/types-DeVSWhXj.d.cts +142 -0
- package/dist/types-DeVSWhXj.d.ts +142 -0
- package/dist/{types-C9ySEdOX.d.cts → types-Yuybzq53.d.cts} +147 -3
- package/dist/upload-E6MCC2OF.mjs +6 -0
- package/package.json +10 -3
- package/dist/chunk-BRAFQUMG.mjs +0 -229
- package/dist/types--wXNHUwt.d.cts +0 -56
- package/dist/types--wXNHUwt.d.ts +0 -56
|
@@ -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";
|
|
@@ -860,7 +1187,9 @@ function buildTraceSummaries(events) {
|
|
|
860
1187
|
};
|
|
861
1188
|
}
|
|
862
1189
|
function summarizeWs(events) {
|
|
863
|
-
const relevant = events.filter(
|
|
1190
|
+
const relevant = events.filter(
|
|
1191
|
+
(event) => event.channel === "ws" || event.event.startsWith("ws.")
|
|
1192
|
+
);
|
|
864
1193
|
const connections = /* @__PURE__ */ new Map();
|
|
865
1194
|
for (const event of relevant) {
|
|
866
1195
|
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
@@ -890,7 +1219,7 @@ function summarizeWs(events) {
|
|
|
890
1219
|
}
|
|
891
1220
|
const values = [...connections.values()];
|
|
892
1221
|
const reconnects = values.reduce((count, connection) => {
|
|
893
|
-
return connection.closedAt && !connection.createdAt ? count : count;
|
|
1222
|
+
return connection.closedAt && !connection.createdAt ? count + 1 : count;
|
|
894
1223
|
}, 0);
|
|
895
1224
|
return {
|
|
896
1225
|
view: "ws",
|
|
@@ -1143,6 +1472,31 @@ function frameToStep(frame) {
|
|
|
1143
1472
|
}
|
|
1144
1473
|
}
|
|
1145
1474
|
|
|
1475
|
+
// src/trace/model.ts
|
|
1476
|
+
function createTraceId(prefix = "evt") {
|
|
1477
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1478
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1479
|
+
}
|
|
1480
|
+
function normalizeTraceEvent(event) {
|
|
1481
|
+
return {
|
|
1482
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1483
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1484
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
1485
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
1486
|
+
data: event.data ?? {},
|
|
1487
|
+
...event
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function inferSeverity(eventName) {
|
|
1491
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1492
|
+
return "error";
|
|
1493
|
+
}
|
|
1494
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1495
|
+
return "warn";
|
|
1496
|
+
}
|
|
1497
|
+
return "info";
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1146
1500
|
// src/trace/script.ts
|
|
1147
1501
|
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
1148
1502
|
var TRACE_SCRIPT = `
|
|
@@ -1422,38 +1776,6 @@ var TRACE_SCRIPT = `
|
|
|
1422
1776
|
})();
|
|
1423
1777
|
`;
|
|
1424
1778
|
|
|
1425
|
-
// src/trace/model.ts
|
|
1426
|
-
function createTraceId(prefix = "evt") {
|
|
1427
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
1428
|
-
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1429
|
-
}
|
|
1430
|
-
function normalizeTraceEvent(event) {
|
|
1431
|
-
return {
|
|
1432
|
-
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1433
|
-
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1434
|
-
elapsedMs: event.elapsedMs ?? 0,
|
|
1435
|
-
severity: event.severity ?? inferSeverity(event.event),
|
|
1436
|
-
data: event.data ?? {},
|
|
1437
|
-
...event
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1440
|
-
function inferSeverity(eventName) {
|
|
1441
|
-
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1442
|
-
return "error";
|
|
1443
|
-
}
|
|
1444
|
-
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1445
|
-
return "warn";
|
|
1446
|
-
}
|
|
1447
|
-
return "info";
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
// src/trace/live.ts
|
|
1451
|
-
function globToRegex(pattern) {
|
|
1452
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1453
|
-
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1454
|
-
return new RegExp(`^${withWildcards}$`);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
1779
|
// src/actions/executor.ts
|
|
1458
1780
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1459
1781
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -1576,6 +1898,25 @@ function getSuggestion(reason) {
|
|
|
1576
1898
|
}
|
|
1577
1899
|
}
|
|
1578
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
|
+
}
|
|
1579
1920
|
var BatchExecutor = class {
|
|
1580
1921
|
page;
|
|
1581
1922
|
constructor(page) {
|
|
@@ -1621,9 +1962,25 @@ var BatchExecutor = class {
|
|
|
1621
1962
|
})
|
|
1622
1963
|
);
|
|
1623
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
|
+
}
|
|
1624
1977
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1625
1978
|
if (attempt > 0) {
|
|
1626
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
|
+
}
|
|
1627
1984
|
}
|
|
1628
1985
|
try {
|
|
1629
1986
|
this.page.resetLastActionPosition();
|
|
@@ -1641,6 +1998,28 @@ var BatchExecutor = class {
|
|
|
1641
1998
|
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
1642
1999
|
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
1643
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
|
+
}
|
|
1644
2023
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1645
2024
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1646
2025
|
}
|
|
@@ -1650,13 +2029,14 @@ var BatchExecutor = class {
|
|
|
1650
2029
|
traceId: createTraceId("action"),
|
|
1651
2030
|
elapsedMs: Date.now() - startTime,
|
|
1652
2031
|
channel: "action",
|
|
1653
|
-
event: "action.succeeded",
|
|
1654
|
-
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}`,
|
|
1655
2034
|
data: {
|
|
1656
2035
|
action: step.action,
|
|
1657
2036
|
selector: step.selector ?? null,
|
|
1658
2037
|
selectorUsed: result.selectorUsed ?? null,
|
|
1659
|
-
durationMs: Date.now() - stepStart
|
|
2038
|
+
durationMs: Date.now() - stepStart,
|
|
2039
|
+
outcomeStatus: stepResult.outcomeStatus ?? null
|
|
1660
2040
|
},
|
|
1661
2041
|
actionId: `action-${i + 1}`,
|
|
1662
2042
|
stepIndex: i,
|
|
@@ -1666,6 +2046,18 @@ var BatchExecutor = class {
|
|
|
1666
2046
|
})
|
|
1667
2047
|
);
|
|
1668
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
|
+
}
|
|
1669
2061
|
results.push(stepResult);
|
|
1670
2062
|
succeeded = true;
|
|
1671
2063
|
break;
|
|
@@ -1673,59 +2065,63 @@ var BatchExecutor = class {
|
|
|
1673
2065
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1674
2066
|
}
|
|
1675
2067
|
}
|
|
2068
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1676
2069
|
if (!succeeded) {
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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 {
|
|
1686
2083
|
}
|
|
1687
|
-
} catch {
|
|
1688
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);
|
|
1689
2124
|
}
|
|
1690
|
-
const failedResult = {
|
|
1691
|
-
index: i,
|
|
1692
|
-
action: step.action,
|
|
1693
|
-
selector: step.selector,
|
|
1694
|
-
success: false,
|
|
1695
|
-
durationMs: Date.now() - stepStart,
|
|
1696
|
-
error: errorMessage,
|
|
1697
|
-
hints,
|
|
1698
|
-
failureReason: reason,
|
|
1699
|
-
coveringElement,
|
|
1700
|
-
suggestion: getSuggestion(reason),
|
|
1701
|
-
timestamp: Date.now()
|
|
1702
|
-
};
|
|
1703
|
-
if (recording && !recording.skipActions.has(step.action)) {
|
|
1704
|
-
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1705
|
-
}
|
|
1706
|
-
if (recording) {
|
|
1707
|
-
recording.traceEvents.push(
|
|
1708
|
-
normalizeTraceEvent({
|
|
1709
|
-
traceId: createTraceId("action"),
|
|
1710
|
-
elapsedMs: Date.now() - startTime,
|
|
1711
|
-
channel: "action",
|
|
1712
|
-
event: "action.failed",
|
|
1713
|
-
severity: "error",
|
|
1714
|
-
summary: `${step.action} failed: ${errorMessage}`,
|
|
1715
|
-
data: {
|
|
1716
|
-
action: step.action,
|
|
1717
|
-
selector: step.selector ?? null,
|
|
1718
|
-
error: errorMessage,
|
|
1719
|
-
reason
|
|
1720
|
-
},
|
|
1721
|
-
actionId: `action-${i + 1}`,
|
|
1722
|
-
stepIndex: i,
|
|
1723
|
-
selector: step.selector,
|
|
1724
|
-
url: step.url
|
|
1725
|
-
})
|
|
1726
|
-
);
|
|
1727
|
-
}
|
|
1728
|
-
results.push(failedResult);
|
|
1729
2125
|
if (onFail === "stop" && !step.optional) {
|
|
1730
2126
|
stoppedAtIndex = i;
|
|
1731
2127
|
break;
|
|
@@ -2036,6 +2432,14 @@ var BatchExecutor = class {
|
|
|
2036
2432
|
case "forms": {
|
|
2037
2433
|
return { value: await this.page.forms() };
|
|
2038
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
|
+
}
|
|
2039
2443
|
case "screenshot": {
|
|
2040
2444
|
const data = await this.page.screenshot({
|
|
2041
2445
|
format: step.format,
|
|
@@ -2186,6 +2590,35 @@ var BatchExecutor = class {
|
|
|
2186
2590
|
const media = await this.assertMediaTrackLive(step.kind);
|
|
2187
2591
|
return { value: media };
|
|
2188
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
|
+
}
|
|
2189
2622
|
default: {
|
|
2190
2623
|
const action = step.action;
|
|
2191
2624
|
const aliases = {
|
|
@@ -2263,8 +2696,13 @@ Valid actions: ${valid}`);
|
|
|
2263
2696
|
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
2264
2697
|
} catch {
|
|
2265
2698
|
}
|
|
2266
|
-
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2267
|
-
|
|
2699
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2700
|
+
source: TRACE_SCRIPT
|
|
2701
|
+
});
|
|
2702
|
+
await this.page.cdpClient.send("Runtime.evaluate", {
|
|
2703
|
+
expression: TRACE_SCRIPT,
|
|
2704
|
+
awaitPromise: false
|
|
2705
|
+
});
|
|
2268
2706
|
}
|
|
2269
2707
|
async waitForWsMessage(match, where, timeout) {
|
|
2270
2708
|
await this.ensureTraceHooks();
|
|
@@ -2282,12 +2720,12 @@ Valid actions: ${valid}`);
|
|
|
2282
2720
|
clearTimeout(timer);
|
|
2283
2721
|
};
|
|
2284
2722
|
const onCreated = (params) => {
|
|
2285
|
-
wsUrls.set(
|
|
2723
|
+
wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
|
|
2286
2724
|
};
|
|
2287
2725
|
const onFrame = (params) => {
|
|
2288
|
-
const requestId =
|
|
2726
|
+
const requestId = readStringOr(params["requestId"]);
|
|
2289
2727
|
const response = params["response"] ?? {};
|
|
2290
|
-
const payload =
|
|
2728
|
+
const payload = response.payloadData ?? "";
|
|
2291
2729
|
const url = wsUrls.get(requestId) ?? "";
|
|
2292
2730
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2293
2731
|
return;
|
|
@@ -2303,13 +2741,13 @@ Valid actions: ${valid}`);
|
|
|
2303
2741
|
return;
|
|
2304
2742
|
}
|
|
2305
2743
|
try {
|
|
2306
|
-
const parsed = JSON.parse(
|
|
2744
|
+
const parsed = JSON.parse(readStringOr(params["payload"]));
|
|
2307
2745
|
if (parsed.event !== "ws.frame.received") {
|
|
2308
2746
|
return;
|
|
2309
2747
|
}
|
|
2310
2748
|
const data = parsed.data ?? {};
|
|
2311
|
-
const payload =
|
|
2312
|
-
const url =
|
|
2749
|
+
const payload = readStringOr(data["payload"]);
|
|
2750
|
+
const url = readStringOr(data["url"]);
|
|
2313
2751
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2314
2752
|
return;
|
|
2315
2753
|
}
|
|
@@ -2318,7 +2756,7 @@ Valid actions: ${valid}`);
|
|
|
2318
2756
|
}
|
|
2319
2757
|
cleanup();
|
|
2320
2758
|
resolve({
|
|
2321
|
-
requestId:
|
|
2759
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2322
2760
|
url,
|
|
2323
2761
|
payload
|
|
2324
2762
|
});
|
|
@@ -2362,13 +2800,14 @@ Valid actions: ${valid}`);
|
|
|
2362
2800
|
if (!entry || typeof entry !== "object") {
|
|
2363
2801
|
continue;
|
|
2364
2802
|
}
|
|
2365
|
-
const
|
|
2803
|
+
const record = entry;
|
|
2804
|
+
const event = readStringOr(record["event"]);
|
|
2366
2805
|
if (event !== "ws.frame.received") {
|
|
2367
2806
|
continue;
|
|
2368
2807
|
}
|
|
2369
|
-
const data =
|
|
2370
|
-
const payload =
|
|
2371
|
-
const url =
|
|
2808
|
+
const data = record["data"] ?? {};
|
|
2809
|
+
const payload = readStringOr(data["payload"]);
|
|
2810
|
+
const url = readStringOr(data["url"]);
|
|
2372
2811
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2373
2812
|
continue;
|
|
2374
2813
|
}
|
|
@@ -2376,7 +2815,7 @@ Valid actions: ${valid}`);
|
|
|
2376
2815
|
continue;
|
|
2377
2816
|
}
|
|
2378
2817
|
return {
|
|
2379
|
-
requestId:
|
|
2818
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2380
2819
|
url,
|
|
2381
2820
|
payload
|
|
2382
2821
|
};
|
|
@@ -2397,13 +2836,11 @@ Valid actions: ${valid}`);
|
|
|
2397
2836
|
return;
|
|
2398
2837
|
}
|
|
2399
2838
|
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2400
|
-
errors.push(
|
|
2401
|
-
args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
|
|
2402
|
-
);
|
|
2839
|
+
errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
|
|
2403
2840
|
};
|
|
2404
2841
|
const onException = (params) => {
|
|
2405
2842
|
const details = params["exceptionDetails"] ?? {};
|
|
2406
|
-
errors.push(
|
|
2843
|
+
errors.push(readString(details["text"]) ?? "Runtime exception");
|
|
2407
2844
|
};
|
|
2408
2845
|
const timer = setTimeout(() => {
|
|
2409
2846
|
cleanup();
|
|
@@ -2775,6 +3212,30 @@ var ACTION_RULES = {
|
|
|
2775
3212
|
kind: { type: "string", enum: ["audio", "video"] }
|
|
2776
3213
|
},
|
|
2777
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: {}
|
|
2778
3239
|
}
|
|
2779
3240
|
};
|
|
2780
3241
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -2814,7 +3275,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2814
3275
|
"name",
|
|
2815
3276
|
"state",
|
|
2816
3277
|
"kind",
|
|
2817
|
-
"windowMs"
|
|
3278
|
+
"windowMs",
|
|
3279
|
+
"expectAny",
|
|
3280
|
+
"expectAll",
|
|
3281
|
+
"failIf",
|
|
3282
|
+
"dangerous",
|
|
3283
|
+
"files"
|
|
2818
3284
|
]);
|
|
2819
3285
|
function resolveAction(name) {
|
|
2820
3286
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -3034,6 +3500,64 @@ function validateSteps(steps) {
|
|
|
3034
3500
|
});
|
|
3035
3501
|
}
|
|
3036
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
|
+
}
|
|
3037
3561
|
if (action === "assertText") {
|
|
3038
3562
|
if (!("expect" in obj) && !("value" in obj)) {
|
|
3039
3563
|
errors.push({
|
|
@@ -3110,6 +3634,14 @@ function validateSteps(steps) {
|
|
|
3110
3634
|
}
|
|
3111
3635
|
|
|
3112
3636
|
export {
|
|
3637
|
+
NetworkResponseTracker,
|
|
3638
|
+
captureStateSignature,
|
|
3639
|
+
evaluateCondition,
|
|
3640
|
+
evaluateOutcome,
|
|
3641
|
+
conditionAny,
|
|
3642
|
+
conditionAll,
|
|
3643
|
+
conditionNot,
|
|
3644
|
+
conditionRace,
|
|
3113
3645
|
ActionabilityError,
|
|
3114
3646
|
ensureActionable,
|
|
3115
3647
|
generateHints,
|