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
|
@@ -3,6 +3,275 @@ import {
|
|
|
3
3
|
stringifyUnknown
|
|
4
4
|
} from "./chunk-DTVRFXKI.mjs";
|
|
5
5
|
|
|
6
|
+
// src/utils/strings.ts
|
|
7
|
+
function readString(value) {
|
|
8
|
+
return typeof value === "string" ? value : void 0;
|
|
9
|
+
}
|
|
10
|
+
function readStringOr(value, fallback = "") {
|
|
11
|
+
return readString(value) ?? fallback;
|
|
12
|
+
}
|
|
13
|
+
function formatConsoleArg(entry) {
|
|
14
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
15
|
+
}
|
|
16
|
+
function globToRegex(pattern) {
|
|
17
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
18
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
19
|
+
return new RegExp(`^${withWildcards}$`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/actions/conditions.ts
|
|
23
|
+
var NetworkResponseTracker = class {
|
|
24
|
+
responses = [];
|
|
25
|
+
listening = false;
|
|
26
|
+
handler = null;
|
|
27
|
+
start(cdp) {
|
|
28
|
+
if (this.listening) return;
|
|
29
|
+
this.listening = true;
|
|
30
|
+
this.handler = (params) => {
|
|
31
|
+
const response = params["response"];
|
|
32
|
+
if (response) {
|
|
33
|
+
this.responses.push({ url: response.url, status: response.status });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
cdp.on("Network.responseReceived", this.handler);
|
|
37
|
+
}
|
|
38
|
+
stop(cdp) {
|
|
39
|
+
if (this.handler) {
|
|
40
|
+
cdp.off("Network.responseReceived", this.handler);
|
|
41
|
+
this.handler = null;
|
|
42
|
+
}
|
|
43
|
+
this.listening = false;
|
|
44
|
+
}
|
|
45
|
+
getResponses() {
|
|
46
|
+
return this.responses;
|
|
47
|
+
}
|
|
48
|
+
reset() {
|
|
49
|
+
this.responses = [];
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
async function captureStateSignature(page) {
|
|
53
|
+
try {
|
|
54
|
+
const url = await page.url();
|
|
55
|
+
const text = await page.text();
|
|
56
|
+
const truncated = text.slice(0, 2e3);
|
|
57
|
+
return `${url}|${simpleHash(truncated)}`;
|
|
58
|
+
} catch {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function simpleHash(str) {
|
|
63
|
+
let hash = 0;
|
|
64
|
+
for (let i = 0; i < str.length; i++) {
|
|
65
|
+
const char = str.charCodeAt(i);
|
|
66
|
+
hash = (hash << 5) - hash + char | 0;
|
|
67
|
+
}
|
|
68
|
+
return hash.toString(36);
|
|
69
|
+
}
|
|
70
|
+
async function evaluateCondition(condition, page, context = {}) {
|
|
71
|
+
switch (condition.kind) {
|
|
72
|
+
case "urlMatches": {
|
|
73
|
+
try {
|
|
74
|
+
const currentUrl = await page.url();
|
|
75
|
+
const regex = globToRegex(condition.pattern);
|
|
76
|
+
const matched = regex.test(currentUrl);
|
|
77
|
+
return {
|
|
78
|
+
condition,
|
|
79
|
+
matched,
|
|
80
|
+
detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return { condition, matched: false, detail: "Failed to get current URL" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
case "elementVisible": {
|
|
87
|
+
try {
|
|
88
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
89
|
+
for (const sel of selectors) {
|
|
90
|
+
const visible = await page.waitFor(sel, {
|
|
91
|
+
timeout: 2e3,
|
|
92
|
+
optional: true,
|
|
93
|
+
state: "visible"
|
|
94
|
+
});
|
|
95
|
+
if (visible) {
|
|
96
|
+
return { condition, matched: true, detail: `Element "${sel}" is visible` };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { condition, matched: false, detail: "No matching visible element found" };
|
|
100
|
+
} catch {
|
|
101
|
+
return { condition, matched: false, detail: "Visibility check failed" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
case "elementHidden": {
|
|
105
|
+
try {
|
|
106
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
107
|
+
for (const sel of selectors) {
|
|
108
|
+
const visible = await page.waitFor(sel, {
|
|
109
|
+
timeout: 500,
|
|
110
|
+
optional: true,
|
|
111
|
+
state: "visible"
|
|
112
|
+
});
|
|
113
|
+
if (visible) {
|
|
114
|
+
return { condition, matched: false, detail: `Element "${sel}" is still visible` };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { condition, matched: true, detail: "Element is hidden or not found" };
|
|
118
|
+
} catch {
|
|
119
|
+
return { condition, matched: true, detail: "Element is hidden (check threw)" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
case "textAppears": {
|
|
123
|
+
try {
|
|
124
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
125
|
+
const text = await page.text(selector);
|
|
126
|
+
const matched = text.includes(condition.text);
|
|
127
|
+
return {
|
|
128
|
+
condition,
|
|
129
|
+
matched,
|
|
130
|
+
detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
|
|
131
|
+
};
|
|
132
|
+
} catch {
|
|
133
|
+
return { condition, matched: false, detail: "Failed to get page text" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
case "textChanges": {
|
|
137
|
+
try {
|
|
138
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
139
|
+
const text = await page.text(selector);
|
|
140
|
+
if (condition.to !== void 0) {
|
|
141
|
+
const matched = text.includes(condition.to);
|
|
142
|
+
return {
|
|
143
|
+
condition,
|
|
144
|
+
matched,
|
|
145
|
+
detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
|
|
149
|
+
} catch {
|
|
150
|
+
return { condition, matched: false, detail: "Failed to get text for change detection" };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
case "networkResponse": {
|
|
154
|
+
const tracker = context.networkTracker;
|
|
155
|
+
if (!tracker) {
|
|
156
|
+
return { condition, matched: false, detail: "No network tracker active" };
|
|
157
|
+
}
|
|
158
|
+
const regex = globToRegex(condition.urlPattern);
|
|
159
|
+
const responses = tracker.getResponses();
|
|
160
|
+
for (const resp of responses) {
|
|
161
|
+
if (regex.test(resp.url)) {
|
|
162
|
+
if (condition.status !== void 0 && resp.status !== condition.status) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
condition,
|
|
167
|
+
matched: true,
|
|
168
|
+
detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
condition,
|
|
174
|
+
matched: false,
|
|
175
|
+
detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
case "stateSignatureChanges": {
|
|
179
|
+
if (!context.beforeSignature) {
|
|
180
|
+
return { condition, matched: false, detail: "No before-signature captured" };
|
|
181
|
+
}
|
|
182
|
+
const afterSignature = await captureStateSignature(page);
|
|
183
|
+
const matched = afterSignature !== context.beforeSignature;
|
|
184
|
+
return {
|
|
185
|
+
condition,
|
|
186
|
+
matched,
|
|
187
|
+
detail: matched ? "Page state changed" : "Page state unchanged"
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
default: {
|
|
191
|
+
const _exhaustive = condition;
|
|
192
|
+
return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function evaluateOutcome(page, options) {
|
|
197
|
+
const {
|
|
198
|
+
expectAny,
|
|
199
|
+
expectAll,
|
|
200
|
+
failIf,
|
|
201
|
+
dangerous = false,
|
|
202
|
+
networkTracker,
|
|
203
|
+
beforeSignature
|
|
204
|
+
} = options;
|
|
205
|
+
const allMatched = [];
|
|
206
|
+
const context = { networkTracker, beforeSignature };
|
|
207
|
+
if (failIf && failIf.length > 0) {
|
|
208
|
+
for (const condition of failIf) {
|
|
209
|
+
const result = await evaluateCondition(condition, page, context);
|
|
210
|
+
allMatched.push(result);
|
|
211
|
+
if (result.matched) {
|
|
212
|
+
return {
|
|
213
|
+
outcomeStatus: "failed",
|
|
214
|
+
matchedConditions: allMatched,
|
|
215
|
+
retrySafe: !dangerous
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (expectAll && expectAll.length > 0) {
|
|
221
|
+
let allPassed = true;
|
|
222
|
+
for (const condition of expectAll) {
|
|
223
|
+
const result = await evaluateCondition(condition, page, context);
|
|
224
|
+
allMatched.push(result);
|
|
225
|
+
if (!result.matched) {
|
|
226
|
+
allPassed = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!allPassed) {
|
|
230
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
231
|
+
return {
|
|
232
|
+
outcomeStatus: status,
|
|
233
|
+
matchedConditions: allMatched,
|
|
234
|
+
retrySafe: !dangerous
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (!expectAny || expectAny.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
outcomeStatus: "success",
|
|
240
|
+
matchedConditions: allMatched,
|
|
241
|
+
retrySafe: true
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (expectAny && expectAny.length > 0) {
|
|
246
|
+
let anyPassed = false;
|
|
247
|
+
for (const condition of expectAny) {
|
|
248
|
+
const result = await evaluateCondition(condition, page, context);
|
|
249
|
+
allMatched.push(result);
|
|
250
|
+
if (result.matched) {
|
|
251
|
+
anyPassed = true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (anyPassed) {
|
|
255
|
+
return {
|
|
256
|
+
outcomeStatus: "success",
|
|
257
|
+
matchedConditions: allMatched,
|
|
258
|
+
retrySafe: true
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
262
|
+
return {
|
|
263
|
+
outcomeStatus: status,
|
|
264
|
+
matchedConditions: allMatched,
|
|
265
|
+
retrySafe: !dangerous
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
outcomeStatus: "success",
|
|
270
|
+
matchedConditions: allMatched,
|
|
271
|
+
retrySafe: true
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
6
275
|
// src/actions/executor.ts
|
|
7
276
|
import * as fs from "fs";
|
|
8
277
|
import { join } from "path";
|
|
@@ -1469,282 +1738,6 @@ var TRACE_SCRIPT = `
|
|
|
1469
1738
|
})();
|
|
1470
1739
|
`;
|
|
1471
1740
|
|
|
1472
|
-
// src/trace/live.ts
|
|
1473
|
-
function globToRegex(pattern) {
|
|
1474
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1475
|
-
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1476
|
-
return new RegExp(`^${withWildcards}$`);
|
|
1477
|
-
}
|
|
1478
|
-
function readString(value) {
|
|
1479
|
-
return typeof value === "string" ? value : void 0;
|
|
1480
|
-
}
|
|
1481
|
-
function readStringOr(value, fallback = "") {
|
|
1482
|
-
return readString(value) ?? fallback;
|
|
1483
|
-
}
|
|
1484
|
-
function formatConsoleArg(entry) {
|
|
1485
|
-
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
1486
|
-
}
|
|
1487
|
-
var LiveTraceCollector = class {
|
|
1488
|
-
cdp;
|
|
1489
|
-
options;
|
|
1490
|
-
handlers = [];
|
|
1491
|
-
wsUrls = /* @__PURE__ */ new Map();
|
|
1492
|
-
httpUrls = /* @__PURE__ */ new Map();
|
|
1493
|
-
events = [];
|
|
1494
|
-
startTime = Date.now();
|
|
1495
|
-
matchRegex;
|
|
1496
|
-
constructor(cdp, options = {}) {
|
|
1497
|
-
this.cdp = cdp;
|
|
1498
|
-
this.options = options;
|
|
1499
|
-
this.matchRegex = options.match ? globToRegex(options.match) : null;
|
|
1500
|
-
}
|
|
1501
|
-
async start() {
|
|
1502
|
-
await this.cdp.send("Runtime.enable");
|
|
1503
|
-
await this.cdp.send("Page.enable");
|
|
1504
|
-
await this.cdp.send("Network.enable");
|
|
1505
|
-
await this.cdp.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
1506
|
-
await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
|
|
1507
|
-
await this.cdp.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
|
|
1508
|
-
if ((this.options.mode ?? "all") !== "http") {
|
|
1509
|
-
this.subscribe("Network.webSocketCreated", (params) => {
|
|
1510
|
-
const requestId = readStringOr(params["requestId"]);
|
|
1511
|
-
const url = readStringOr(params["url"]);
|
|
1512
|
-
if (!this.matchesUrl(url)) {
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
this.wsUrls.set(requestId, url);
|
|
1516
|
-
void this.emit({
|
|
1517
|
-
channel: "ws",
|
|
1518
|
-
event: "ws.connection.created",
|
|
1519
|
-
summary: `WebSocket opened ${url}`,
|
|
1520
|
-
connectionId: requestId,
|
|
1521
|
-
requestId,
|
|
1522
|
-
url,
|
|
1523
|
-
data: { url }
|
|
1524
|
-
});
|
|
1525
|
-
});
|
|
1526
|
-
this.subscribe("Network.webSocketFrameSent", (params) => {
|
|
1527
|
-
const requestId = readStringOr(params["requestId"]);
|
|
1528
|
-
const response = params["response"];
|
|
1529
|
-
const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
|
|
1530
|
-
const url = this.wsUrls.get(requestId);
|
|
1531
|
-
if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
void this.emit({
|
|
1535
|
-
channel: "ws",
|
|
1536
|
-
event: "ws.frame.sent",
|
|
1537
|
-
summary: `WebSocket frame sent ${requestId}`,
|
|
1538
|
-
connectionId: requestId,
|
|
1539
|
-
requestId,
|
|
1540
|
-
url,
|
|
1541
|
-
data: {
|
|
1542
|
-
opcode: response?.opcode ?? 1,
|
|
1543
|
-
payload,
|
|
1544
|
-
length: response?.payloadData?.length ?? 0
|
|
1545
|
-
}
|
|
1546
|
-
});
|
|
1547
|
-
});
|
|
1548
|
-
this.subscribe("Network.webSocketFrameReceived", (params) => {
|
|
1549
|
-
const requestId = readStringOr(params["requestId"]);
|
|
1550
|
-
const response = params["response"];
|
|
1551
|
-
const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
|
|
1552
|
-
const url = this.wsUrls.get(requestId);
|
|
1553
|
-
if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
void this.emit({
|
|
1557
|
-
channel: "ws",
|
|
1558
|
-
event: "ws.frame.received",
|
|
1559
|
-
summary: `WebSocket frame received ${requestId}`,
|
|
1560
|
-
connectionId: requestId,
|
|
1561
|
-
requestId,
|
|
1562
|
-
url,
|
|
1563
|
-
data: {
|
|
1564
|
-
opcode: response?.opcode ?? 1,
|
|
1565
|
-
payload,
|
|
1566
|
-
length: response?.payloadData?.length ?? 0
|
|
1567
|
-
}
|
|
1568
|
-
});
|
|
1569
|
-
});
|
|
1570
|
-
this.subscribe("Network.webSocketClosed", (params) => {
|
|
1571
|
-
const requestId = readStringOr(params["requestId"]);
|
|
1572
|
-
const url = this.wsUrls.get(requestId);
|
|
1573
|
-
this.wsUrls.delete(requestId);
|
|
1574
|
-
void this.emit({
|
|
1575
|
-
channel: "ws",
|
|
1576
|
-
event: "ws.connection.closed",
|
|
1577
|
-
summary: `WebSocket closed ${requestId}`,
|
|
1578
|
-
severity: "warn",
|
|
1579
|
-
connectionId: requestId,
|
|
1580
|
-
requestId,
|
|
1581
|
-
url,
|
|
1582
|
-
data: { url }
|
|
1583
|
-
});
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
if ((this.options.mode ?? "all") !== "ws") {
|
|
1587
|
-
this.subscribe("Network.requestWillBeSent", (params) => {
|
|
1588
|
-
const request = params["request"];
|
|
1589
|
-
const requestId = readStringOr(params["requestId"]);
|
|
1590
|
-
const url = request?.url ?? "";
|
|
1591
|
-
if (!this.matchesUrl(url)) {
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
this.httpUrls.set(requestId, url);
|
|
1595
|
-
void this.emit({
|
|
1596
|
-
channel: "http",
|
|
1597
|
-
event: "http.request.sent",
|
|
1598
|
-
summary: `${request?.method ?? "GET"} ${url}`,
|
|
1599
|
-
requestId,
|
|
1600
|
-
url,
|
|
1601
|
-
data: {
|
|
1602
|
-
method: request?.method ?? "GET",
|
|
1603
|
-
headers: request?.headers ?? {},
|
|
1604
|
-
body: request?.postData ?? null
|
|
1605
|
-
}
|
|
1606
|
-
});
|
|
1607
|
-
});
|
|
1608
|
-
this.subscribe("Network.responseReceived", (params) => {
|
|
1609
|
-
const requestId = readStringOr(params["requestId"]);
|
|
1610
|
-
if (!this.httpUrls.has(requestId)) {
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
const response = params["response"];
|
|
1614
|
-
void this.emit({
|
|
1615
|
-
channel: "http",
|
|
1616
|
-
event: "http.response.received",
|
|
1617
|
-
summary: `${response?.status ?? 0} ${response?.url ?? this.httpUrls.get(requestId) ?? ""}`,
|
|
1618
|
-
requestId,
|
|
1619
|
-
url: response?.url ?? this.httpUrls.get(requestId),
|
|
1620
|
-
data: {
|
|
1621
|
-
status: response?.status ?? 0,
|
|
1622
|
-
headers: response?.headers ?? {},
|
|
1623
|
-
mimeType: response?.mimeType ?? null
|
|
1624
|
-
}
|
|
1625
|
-
});
|
|
1626
|
-
});
|
|
1627
|
-
this.subscribe("Network.loadingFailed", (params) => {
|
|
1628
|
-
const requestId = readStringOr(params["requestId"]);
|
|
1629
|
-
const url = readString(params["blockedReason"]) ?? this.httpUrls.get(requestId) ?? "";
|
|
1630
|
-
void this.emit({
|
|
1631
|
-
channel: "http",
|
|
1632
|
-
event: "http.response.failed",
|
|
1633
|
-
summary: `HTTP request failed ${requestId}`,
|
|
1634
|
-
severity: "error",
|
|
1635
|
-
requestId,
|
|
1636
|
-
url,
|
|
1637
|
-
data: {
|
|
1638
|
-
errorText: params["errorText"] ?? null,
|
|
1639
|
-
blockedReason: params["blockedReason"] ?? null,
|
|
1640
|
-
canceled: params["canceled"] ?? false
|
|
1641
|
-
}
|
|
1642
|
-
});
|
|
1643
|
-
});
|
|
1644
|
-
}
|
|
1645
|
-
this.subscribe("Runtime.consoleAPICalled", (params) => {
|
|
1646
|
-
const type = readStringOr(params["type"], "log");
|
|
1647
|
-
if (type !== "log" && type !== "warn" && type !== "error") {
|
|
1648
|
-
return;
|
|
1649
|
-
}
|
|
1650
|
-
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
1651
|
-
const text = args.map(formatConsoleArg).filter(Boolean).join(" ");
|
|
1652
|
-
void this.emit({
|
|
1653
|
-
channel: "console",
|
|
1654
|
-
event: `console.${type}`,
|
|
1655
|
-
severity: type === "error" ? "error" : type === "warn" ? "warn" : "info",
|
|
1656
|
-
summary: text || `console.${type}`,
|
|
1657
|
-
data: { args }
|
|
1658
|
-
});
|
|
1659
|
-
});
|
|
1660
|
-
this.subscribe("Runtime.exceptionThrown", (params) => {
|
|
1661
|
-
const details = params["exceptionDetails"] ?? {};
|
|
1662
|
-
const text = readString(details["text"]) ?? "Runtime exception";
|
|
1663
|
-
void this.emit({
|
|
1664
|
-
channel: "runtime",
|
|
1665
|
-
event: "runtime.exception",
|
|
1666
|
-
severity: "error",
|
|
1667
|
-
summary: text,
|
|
1668
|
-
data: details
|
|
1669
|
-
});
|
|
1670
|
-
});
|
|
1671
|
-
this.subscribe("Runtime.bindingCalled", (params) => {
|
|
1672
|
-
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
const raw = readStringOr(params["payload"]);
|
|
1676
|
-
try {
|
|
1677
|
-
const payload = JSON.parse(raw);
|
|
1678
|
-
const channel = this.channelForTraceEvent(payload.event);
|
|
1679
|
-
void this.emit({
|
|
1680
|
-
channel,
|
|
1681
|
-
event: payload.event,
|
|
1682
|
-
severity: payload.severity,
|
|
1683
|
-
summary: payload.summary ?? payload.event,
|
|
1684
|
-
ts: payload.ts ? new Date(payload.ts).toISOString() : void 0,
|
|
1685
|
-
data: payload.data ?? {},
|
|
1686
|
-
url: readString(payload.data?.["url"])
|
|
1687
|
-
});
|
|
1688
|
-
} catch {
|
|
1689
|
-
}
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
async stop() {
|
|
1693
|
-
for (const { event, handler } of this.handlers) {
|
|
1694
|
-
this.cdp.off(event, handler);
|
|
1695
|
-
}
|
|
1696
|
-
this.handlers.length = 0;
|
|
1697
|
-
return [...this.events];
|
|
1698
|
-
}
|
|
1699
|
-
getEvents() {
|
|
1700
|
-
return [...this.events];
|
|
1701
|
-
}
|
|
1702
|
-
subscribe(event, handler) {
|
|
1703
|
-
this.cdp.on(event, handler);
|
|
1704
|
-
this.handlers.push({ event, handler });
|
|
1705
|
-
}
|
|
1706
|
-
matchesUrl(url) {
|
|
1707
|
-
if (!this.matchRegex) {
|
|
1708
|
-
return true;
|
|
1709
|
-
}
|
|
1710
|
-
return this.matchRegex.test(url);
|
|
1711
|
-
}
|
|
1712
|
-
formatPayload(payloadData, opcode) {
|
|
1713
|
-
const data = payloadData ?? "";
|
|
1714
|
-
const maxPayload = this.options.maxPayload ?? 256;
|
|
1715
|
-
if (opcode === 2) {
|
|
1716
|
-
const byteLength = Math.floor(data.length * 3 / 4);
|
|
1717
|
-
return `[binary: ${byteLength} bytes]`;
|
|
1718
|
-
}
|
|
1719
|
-
if (data.length > maxPayload) {
|
|
1720
|
-
return `${data.slice(0, maxPayload)}... [truncated, ${data.length} total]`;
|
|
1721
|
-
}
|
|
1722
|
-
return data;
|
|
1723
|
-
}
|
|
1724
|
-
channelForTraceEvent(eventName) {
|
|
1725
|
-
if (eventName.startsWith("ws.")) return "ws";
|
|
1726
|
-
if (eventName.startsWith("http.")) return "http";
|
|
1727
|
-
if (eventName.startsWith("console.")) return "console";
|
|
1728
|
-
if (eventName.startsWith("permission.")) return "permission";
|
|
1729
|
-
if (eventName.startsWith("media.")) return "media";
|
|
1730
|
-
if (eventName.startsWith("voice.")) return "voice";
|
|
1731
|
-
if (eventName.startsWith("dom.")) return "dom";
|
|
1732
|
-
if (eventName.startsWith("runtime.")) return "runtime";
|
|
1733
|
-
return "session";
|
|
1734
|
-
}
|
|
1735
|
-
async emit(event) {
|
|
1736
|
-
const normalized = normalizeTraceEvent({
|
|
1737
|
-
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1738
|
-
sessionId: this.options.sessionId,
|
|
1739
|
-
targetId: this.options.targetId,
|
|
1740
|
-
elapsedMs: event.elapsedMs ?? Date.now() - this.startTime,
|
|
1741
|
-
...event
|
|
1742
|
-
});
|
|
1743
|
-
this.events.push(normalized);
|
|
1744
|
-
await this.options.onEvent?.(normalized);
|
|
1745
|
-
}
|
|
1746
|
-
};
|
|
1747
|
-
|
|
1748
1741
|
// src/actions/executor.ts
|
|
1749
1742
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1750
1743
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -1754,15 +1747,6 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
1754
1747
|
"text",
|
|
1755
1748
|
"screenshot"
|
|
1756
1749
|
];
|
|
1757
|
-
function readString2(value) {
|
|
1758
|
-
return typeof value === "string" ? value : void 0;
|
|
1759
|
-
}
|
|
1760
|
-
function readStringOr2(value, fallback = "") {
|
|
1761
|
-
return readString2(value) ?? fallback;
|
|
1762
|
-
}
|
|
1763
|
-
function formatConsoleArg2(entry) {
|
|
1764
|
-
return readString2(entry["value"]) ?? readString2(entry["description"]) ?? "";
|
|
1765
|
-
}
|
|
1766
1750
|
function loadExistingRecording(manifestPath) {
|
|
1767
1751
|
try {
|
|
1768
1752
|
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
@@ -1876,6 +1860,25 @@ function getSuggestion(reason) {
|
|
|
1876
1860
|
}
|
|
1877
1861
|
}
|
|
1878
1862
|
}
|
|
1863
|
+
function hasOutcomeConditions(step) {
|
|
1864
|
+
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;
|
|
1865
|
+
}
|
|
1866
|
+
function needsNetworkTracking(step) {
|
|
1867
|
+
const allConditions = [
|
|
1868
|
+
...step.expectAny ?? [],
|
|
1869
|
+
...step.expectAll ?? [],
|
|
1870
|
+
...step.failIf ?? []
|
|
1871
|
+
];
|
|
1872
|
+
return allConditions.some((c) => c.kind === "networkResponse");
|
|
1873
|
+
}
|
|
1874
|
+
function needsStateSignature(step) {
|
|
1875
|
+
const allConditions = [
|
|
1876
|
+
...step.expectAny ?? [],
|
|
1877
|
+
...step.expectAll ?? [],
|
|
1878
|
+
...step.failIf ?? []
|
|
1879
|
+
];
|
|
1880
|
+
return allConditions.some((c) => c.kind === "stateSignatureChanges");
|
|
1881
|
+
}
|
|
1879
1882
|
var BatchExecutor = class {
|
|
1880
1883
|
page;
|
|
1881
1884
|
constructor(page) {
|
|
@@ -1921,9 +1924,25 @@ var BatchExecutor = class {
|
|
|
1921
1924
|
})
|
|
1922
1925
|
);
|
|
1923
1926
|
}
|
|
1927
|
+
const hasOutcome = hasOutcomeConditions(step);
|
|
1928
|
+
let networkTracker;
|
|
1929
|
+
let beforeSignature;
|
|
1930
|
+
if (hasOutcome) {
|
|
1931
|
+
if (needsNetworkTracking(step)) {
|
|
1932
|
+
networkTracker = new NetworkResponseTracker();
|
|
1933
|
+
networkTracker.start(this.page.cdpClient);
|
|
1934
|
+
}
|
|
1935
|
+
if (needsStateSignature(step)) {
|
|
1936
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1924
1939
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1925
1940
|
if (attempt > 0) {
|
|
1926
1941
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1942
|
+
if (networkTracker) networkTracker.reset();
|
|
1943
|
+
if (hasOutcome && needsStateSignature(step)) {
|
|
1944
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
1945
|
+
}
|
|
1927
1946
|
}
|
|
1928
1947
|
try {
|
|
1929
1948
|
this.page.resetLastActionPosition();
|
|
@@ -1941,6 +1960,28 @@ var BatchExecutor = class {
|
|
|
1941
1960
|
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
1942
1961
|
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
1943
1962
|
};
|
|
1963
|
+
if (hasOutcome) {
|
|
1964
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1965
|
+
const outcome = await evaluateOutcome(this.page, {
|
|
1966
|
+
expectAny: step.expectAny,
|
|
1967
|
+
expectAll: step.expectAll,
|
|
1968
|
+
failIf: step.failIf,
|
|
1969
|
+
dangerous: step.dangerous,
|
|
1970
|
+
networkTracker,
|
|
1971
|
+
beforeSignature
|
|
1972
|
+
});
|
|
1973
|
+
stepResult.outcomeStatus = outcome.outcomeStatus;
|
|
1974
|
+
stepResult.matchedConditions = outcome.matchedConditions;
|
|
1975
|
+
stepResult.retrySafe = outcome.retrySafe;
|
|
1976
|
+
if (outcome.outcomeStatus !== "success") {
|
|
1977
|
+
stepResult.success = false;
|
|
1978
|
+
stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
|
|
1979
|
+
const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
|
|
1980
|
+
if (failedDetails.length > 0) {
|
|
1981
|
+
stepResult.suggestion = failedDetails.join("; ");
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1944
1985
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1945
1986
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1946
1987
|
}
|
|
@@ -1950,13 +1991,14 @@ var BatchExecutor = class {
|
|
|
1950
1991
|
traceId: createTraceId("action"),
|
|
1951
1992
|
elapsedMs: Date.now() - startTime,
|
|
1952
1993
|
channel: "action",
|
|
1953
|
-
event: "action.succeeded",
|
|
1954
|
-
summary: `${step.action} succeeded`,
|
|
1994
|
+
event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
|
|
1995
|
+
summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
|
|
1955
1996
|
data: {
|
|
1956
1997
|
action: step.action,
|
|
1957
1998
|
selector: step.selector ?? null,
|
|
1958
1999
|
selectorUsed: result.selectorUsed ?? null,
|
|
1959
|
-
durationMs: Date.now() - stepStart
|
|
2000
|
+
durationMs: Date.now() - stepStart,
|
|
2001
|
+
outcomeStatus: stepResult.outcomeStatus ?? null
|
|
1960
2002
|
},
|
|
1961
2003
|
actionId: `action-${i + 1}`,
|
|
1962
2004
|
stepIndex: i,
|
|
@@ -1966,6 +2008,18 @@ var BatchExecutor = class {
|
|
|
1966
2008
|
})
|
|
1967
2009
|
);
|
|
1968
2010
|
}
|
|
2011
|
+
if (hasOutcome && !stepResult.success) {
|
|
2012
|
+
if (step.dangerous) {
|
|
2013
|
+
results.push(stepResult);
|
|
2014
|
+
break;
|
|
2015
|
+
}
|
|
2016
|
+
if (attempt < maxAttempts - 1) {
|
|
2017
|
+
lastError = new Error(stepResult.error ?? "Outcome failed");
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
results.push(stepResult);
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
1969
2023
|
results.push(stepResult);
|
|
1970
2024
|
succeeded = true;
|
|
1971
2025
|
break;
|
|
@@ -1973,59 +2027,63 @@ var BatchExecutor = class {
|
|
|
1973
2027
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1974
2028
|
}
|
|
1975
2029
|
}
|
|
2030
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1976
2031
|
if (!succeeded) {
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2032
|
+
const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
|
|
2033
|
+
if (!resultAlreadyPushed) {
|
|
2034
|
+
const errorMessage = lastError?.message ?? "Unknown error";
|
|
2035
|
+
let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
|
|
2036
|
+
const { reason, coveringElement } = classifyFailure(lastError);
|
|
2037
|
+
if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
|
|
2038
|
+
try {
|
|
2039
|
+
const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
|
|
2040
|
+
const autoHints = await generateHints(this.page, selectors, step.action, 3);
|
|
2041
|
+
if (autoHints.length > 0) {
|
|
2042
|
+
hints = autoHints;
|
|
2043
|
+
}
|
|
2044
|
+
} catch {
|
|
1986
2045
|
}
|
|
1987
|
-
} catch {
|
|
1988
2046
|
}
|
|
2047
|
+
const failedResult = {
|
|
2048
|
+
index: i,
|
|
2049
|
+
action: step.action,
|
|
2050
|
+
selector: step.selector,
|
|
2051
|
+
success: false,
|
|
2052
|
+
durationMs: Date.now() - stepStart,
|
|
2053
|
+
error: errorMessage,
|
|
2054
|
+
hints,
|
|
2055
|
+
failureReason: reason,
|
|
2056
|
+
coveringElement,
|
|
2057
|
+
suggestion: getSuggestion(reason),
|
|
2058
|
+
timestamp: Date.now()
|
|
2059
|
+
};
|
|
2060
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
2061
|
+
await this.captureRecordingFrame(step, failedResult, recording);
|
|
2062
|
+
}
|
|
2063
|
+
if (recording) {
|
|
2064
|
+
recording.traceEvents.push(
|
|
2065
|
+
normalizeTraceEvent({
|
|
2066
|
+
traceId: createTraceId("action"),
|
|
2067
|
+
elapsedMs: Date.now() - startTime,
|
|
2068
|
+
channel: "action",
|
|
2069
|
+
event: "action.failed",
|
|
2070
|
+
severity: "error",
|
|
2071
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
2072
|
+
data: {
|
|
2073
|
+
action: step.action,
|
|
2074
|
+
selector: step.selector ?? null,
|
|
2075
|
+
error: errorMessage,
|
|
2076
|
+
reason
|
|
2077
|
+
},
|
|
2078
|
+
actionId: `action-${i + 1}`,
|
|
2079
|
+
stepIndex: i,
|
|
2080
|
+
selector: step.selector,
|
|
2081
|
+
url: step.url
|
|
2082
|
+
})
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
results.push(failedResult);
|
|
1989
2086
|
}
|
|
1990
|
-
const failedResult = {
|
|
1991
|
-
index: i,
|
|
1992
|
-
action: step.action,
|
|
1993
|
-
selector: step.selector,
|
|
1994
|
-
success: false,
|
|
1995
|
-
durationMs: Date.now() - stepStart,
|
|
1996
|
-
error: errorMessage,
|
|
1997
|
-
hints,
|
|
1998
|
-
failureReason: reason,
|
|
1999
|
-
coveringElement,
|
|
2000
|
-
suggestion: getSuggestion(reason),
|
|
2001
|
-
timestamp: Date.now()
|
|
2002
|
-
};
|
|
2003
|
-
if (recording && !recording.skipActions.has(step.action)) {
|
|
2004
|
-
await this.captureRecordingFrame(step, failedResult, recording);
|
|
2005
|
-
}
|
|
2006
|
-
if (recording) {
|
|
2007
|
-
recording.traceEvents.push(
|
|
2008
|
-
normalizeTraceEvent({
|
|
2009
|
-
traceId: createTraceId("action"),
|
|
2010
|
-
elapsedMs: Date.now() - startTime,
|
|
2011
|
-
channel: "action",
|
|
2012
|
-
event: "action.failed",
|
|
2013
|
-
severity: "error",
|
|
2014
|
-
summary: `${step.action} failed: ${errorMessage}`,
|
|
2015
|
-
data: {
|
|
2016
|
-
action: step.action,
|
|
2017
|
-
selector: step.selector ?? null,
|
|
2018
|
-
error: errorMessage,
|
|
2019
|
-
reason
|
|
2020
|
-
},
|
|
2021
|
-
actionId: `action-${i + 1}`,
|
|
2022
|
-
stepIndex: i,
|
|
2023
|
-
selector: step.selector,
|
|
2024
|
-
url: step.url
|
|
2025
|
-
})
|
|
2026
|
-
);
|
|
2027
|
-
}
|
|
2028
|
-
results.push(failedResult);
|
|
2029
2087
|
if (onFail === "stop" && !step.optional) {
|
|
2030
2088
|
stoppedAtIndex = i;
|
|
2031
2089
|
break;
|
|
@@ -2336,6 +2394,14 @@ var BatchExecutor = class {
|
|
|
2336
2394
|
case "forms": {
|
|
2337
2395
|
return { value: await this.page.forms() };
|
|
2338
2396
|
}
|
|
2397
|
+
case "delta": {
|
|
2398
|
+
const review = await this.page.review();
|
|
2399
|
+
return { value: review };
|
|
2400
|
+
}
|
|
2401
|
+
case "review": {
|
|
2402
|
+
const review = await this.page.review();
|
|
2403
|
+
return { value: review };
|
|
2404
|
+
}
|
|
2339
2405
|
case "screenshot": {
|
|
2340
2406
|
const data = await this.page.screenshot({
|
|
2341
2407
|
format: step.format,
|
|
@@ -2486,6 +2552,35 @@ var BatchExecutor = class {
|
|
|
2486
2552
|
const media = await this.assertMediaTrackLive(step.kind);
|
|
2487
2553
|
return { value: media };
|
|
2488
2554
|
}
|
|
2555
|
+
case "chooseOption": {
|
|
2556
|
+
const { chooseOption } = await import("./combobox-RAKBA2BW.mjs");
|
|
2557
|
+
if (!step.value) throw new Error("chooseOption requires value");
|
|
2558
|
+
const result = await chooseOption(this.page, {
|
|
2559
|
+
trigger: step.trigger ?? step.selector ?? "",
|
|
2560
|
+
listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
|
|
2561
|
+
value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
|
|
2562
|
+
match: step.match,
|
|
2563
|
+
timeout: step.timeout ?? timeout
|
|
2564
|
+
});
|
|
2565
|
+
if (!result.success) {
|
|
2566
|
+
throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
|
|
2567
|
+
}
|
|
2568
|
+
return { value: result };
|
|
2569
|
+
}
|
|
2570
|
+
case "upload": {
|
|
2571
|
+
const { uploadFiles } = await import("./upload-E6MCC2OF.mjs");
|
|
2572
|
+
if (!step.selector) throw new Error("upload requires selector");
|
|
2573
|
+
if (!step.files || step.files.length === 0) throw new Error("upload requires files");
|
|
2574
|
+
const result = await uploadFiles(this.page, {
|
|
2575
|
+
selector: step.selector,
|
|
2576
|
+
files: step.files,
|
|
2577
|
+
timeout: step.timeout ?? timeout
|
|
2578
|
+
});
|
|
2579
|
+
if (!result.accepted) {
|
|
2580
|
+
throw new Error(result.error ?? "Upload was not accepted");
|
|
2581
|
+
}
|
|
2582
|
+
return { value: result };
|
|
2583
|
+
}
|
|
2489
2584
|
default: {
|
|
2490
2585
|
const action = step.action;
|
|
2491
2586
|
const aliases = {
|
|
@@ -2587,10 +2682,10 @@ Valid actions: ${valid}`);
|
|
|
2587
2682
|
clearTimeout(timer);
|
|
2588
2683
|
};
|
|
2589
2684
|
const onCreated = (params) => {
|
|
2590
|
-
wsUrls.set(
|
|
2685
|
+
wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
|
|
2591
2686
|
};
|
|
2592
2687
|
const onFrame = (params) => {
|
|
2593
|
-
const requestId =
|
|
2688
|
+
const requestId = readStringOr(params["requestId"]);
|
|
2594
2689
|
const response = params["response"] ?? {};
|
|
2595
2690
|
const payload = response.payloadData ?? "";
|
|
2596
2691
|
const url = wsUrls.get(requestId) ?? "";
|
|
@@ -2608,13 +2703,13 @@ Valid actions: ${valid}`);
|
|
|
2608
2703
|
return;
|
|
2609
2704
|
}
|
|
2610
2705
|
try {
|
|
2611
|
-
const parsed = JSON.parse(
|
|
2706
|
+
const parsed = JSON.parse(readStringOr(params["payload"]));
|
|
2612
2707
|
if (parsed.event !== "ws.frame.received") {
|
|
2613
2708
|
return;
|
|
2614
2709
|
}
|
|
2615
2710
|
const data = parsed.data ?? {};
|
|
2616
|
-
const payload =
|
|
2617
|
-
const url =
|
|
2711
|
+
const payload = readStringOr(data["payload"]);
|
|
2712
|
+
const url = readStringOr(data["url"]);
|
|
2618
2713
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2619
2714
|
return;
|
|
2620
2715
|
}
|
|
@@ -2623,7 +2718,7 @@ Valid actions: ${valid}`);
|
|
|
2623
2718
|
}
|
|
2624
2719
|
cleanup();
|
|
2625
2720
|
resolve({
|
|
2626
|
-
requestId:
|
|
2721
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2627
2722
|
url,
|
|
2628
2723
|
payload
|
|
2629
2724
|
});
|
|
@@ -2668,13 +2763,13 @@ Valid actions: ${valid}`);
|
|
|
2668
2763
|
continue;
|
|
2669
2764
|
}
|
|
2670
2765
|
const record = entry;
|
|
2671
|
-
const event =
|
|
2766
|
+
const event = readStringOr(record["event"]);
|
|
2672
2767
|
if (event !== "ws.frame.received") {
|
|
2673
2768
|
continue;
|
|
2674
2769
|
}
|
|
2675
2770
|
const data = record["data"] ?? {};
|
|
2676
|
-
const payload =
|
|
2677
|
-
const url =
|
|
2771
|
+
const payload = readStringOr(data["payload"]);
|
|
2772
|
+
const url = readStringOr(data["url"]);
|
|
2678
2773
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2679
2774
|
continue;
|
|
2680
2775
|
}
|
|
@@ -2682,7 +2777,7 @@ Valid actions: ${valid}`);
|
|
|
2682
2777
|
continue;
|
|
2683
2778
|
}
|
|
2684
2779
|
return {
|
|
2685
|
-
requestId:
|
|
2780
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2686
2781
|
url,
|
|
2687
2782
|
payload
|
|
2688
2783
|
};
|
|
@@ -2703,11 +2798,11 @@ Valid actions: ${valid}`);
|
|
|
2703
2798
|
return;
|
|
2704
2799
|
}
|
|
2705
2800
|
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2706
|
-
errors.push(args.map(
|
|
2801
|
+
errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
|
|
2707
2802
|
};
|
|
2708
2803
|
const onException = (params) => {
|
|
2709
2804
|
const details = params["exceptionDetails"] ?? {};
|
|
2710
|
-
errors.push(
|
|
2805
|
+
errors.push(readString(details["text"]) ?? "Runtime exception");
|
|
2711
2806
|
};
|
|
2712
2807
|
const timer = setTimeout(() => {
|
|
2713
2808
|
cleanup();
|
|
@@ -3079,6 +3174,30 @@ var ACTION_RULES = {
|
|
|
3079
3174
|
kind: { type: "string", enum: ["audio", "video"] }
|
|
3080
3175
|
},
|
|
3081
3176
|
optional: {}
|
|
3177
|
+
},
|
|
3178
|
+
delta: {
|
|
3179
|
+
required: {},
|
|
3180
|
+
optional: {}
|
|
3181
|
+
},
|
|
3182
|
+
review: {
|
|
3183
|
+
required: {},
|
|
3184
|
+
optional: {}
|
|
3185
|
+
},
|
|
3186
|
+
chooseOption: {
|
|
3187
|
+
required: { value: { type: "string|string[]" } },
|
|
3188
|
+
optional: {
|
|
3189
|
+
trigger: { type: "string|string[]" },
|
|
3190
|
+
selector: { type: "string|string[]" },
|
|
3191
|
+
option: { type: "string|string[]" },
|
|
3192
|
+
match: { type: "string", enum: ["exact", "contains", "startsWith"] }
|
|
3193
|
+
}
|
|
3194
|
+
},
|
|
3195
|
+
upload: {
|
|
3196
|
+
required: {
|
|
3197
|
+
selector: { type: "string|string[]" },
|
|
3198
|
+
files: { type: "string|string[]" }
|
|
3199
|
+
},
|
|
3200
|
+
optional: {}
|
|
3082
3201
|
}
|
|
3083
3202
|
};
|
|
3084
3203
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -3118,7 +3237,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
3118
3237
|
"name",
|
|
3119
3238
|
"state",
|
|
3120
3239
|
"kind",
|
|
3121
|
-
"windowMs"
|
|
3240
|
+
"windowMs",
|
|
3241
|
+
"expectAny",
|
|
3242
|
+
"expectAll",
|
|
3243
|
+
"failIf",
|
|
3244
|
+
"dangerous",
|
|
3245
|
+
"files"
|
|
3122
3246
|
]);
|
|
3123
3247
|
function resolveAction(name) {
|
|
3124
3248
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -3338,6 +3462,64 @@ function validateSteps(steps) {
|
|
|
3338
3462
|
});
|
|
3339
3463
|
}
|
|
3340
3464
|
}
|
|
3465
|
+
if ("dangerous" in obj && obj["dangerous"] !== void 0) {
|
|
3466
|
+
if (typeof obj["dangerous"] !== "boolean") {
|
|
3467
|
+
errors.push({
|
|
3468
|
+
stepIndex: i,
|
|
3469
|
+
field: "dangerous",
|
|
3470
|
+
message: `"dangerous" expected boolean, got ${typeof obj["dangerous"]}.`
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
for (const condField of ["expectAny", "expectAll", "failIf"]) {
|
|
3475
|
+
if (condField in obj && obj[condField] !== void 0) {
|
|
3476
|
+
if (!Array.isArray(obj[condField])) {
|
|
3477
|
+
errors.push({
|
|
3478
|
+
stepIndex: i,
|
|
3479
|
+
field: condField,
|
|
3480
|
+
message: `"${condField}" expected array, got ${typeof obj[condField]}.`
|
|
3481
|
+
});
|
|
3482
|
+
} else {
|
|
3483
|
+
const conditions = obj[condField];
|
|
3484
|
+
for (let ci = 0; ci < conditions.length; ci++) {
|
|
3485
|
+
const cond = conditions[ci];
|
|
3486
|
+
if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
|
|
3487
|
+
errors.push({
|
|
3488
|
+
stepIndex: i,
|
|
3489
|
+
field: condField,
|
|
3490
|
+
message: `"${condField}[${ci}]" must be a condition object.`
|
|
3491
|
+
});
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3494
|
+
const condObj = cond;
|
|
3495
|
+
if (!("kind" in condObj) || typeof condObj["kind"] !== "string") {
|
|
3496
|
+
errors.push({
|
|
3497
|
+
stepIndex: i,
|
|
3498
|
+
field: condField,
|
|
3499
|
+
message: `"${condField}[${ci}]" missing required "kind" field.`
|
|
3500
|
+
});
|
|
3501
|
+
} else {
|
|
3502
|
+
const validKinds = [
|
|
3503
|
+
"urlMatches",
|
|
3504
|
+
"elementVisible",
|
|
3505
|
+
"elementHidden",
|
|
3506
|
+
"textAppears",
|
|
3507
|
+
"textChanges",
|
|
3508
|
+
"networkResponse",
|
|
3509
|
+
"stateSignatureChanges"
|
|
3510
|
+
];
|
|
3511
|
+
if (!validKinds.includes(condObj["kind"])) {
|
|
3512
|
+
errors.push({
|
|
3513
|
+
stepIndex: i,
|
|
3514
|
+
field: condField,
|
|
3515
|
+
message: `"${condField}[${ci}].kind" must be one of: ${validKinds.join(", ")}. Got "${condObj["kind"]}".`
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3341
3523
|
if (action === "assertText") {
|
|
3342
3524
|
if (!("expect" in obj) && !("value" in obj)) {
|
|
3343
3525
|
errors.push({
|
|
@@ -5398,6 +5580,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
5398
5580
|
});
|
|
5399
5581
|
}
|
|
5400
5582
|
|
|
5583
|
+
// src/browser/delta.ts
|
|
5584
|
+
function extractPageState(url, title, snapshot, forms, pageText) {
|
|
5585
|
+
const headings = [];
|
|
5586
|
+
const buttons = [];
|
|
5587
|
+
const alerts = [];
|
|
5588
|
+
function walkNodes(nodes) {
|
|
5589
|
+
for (const node of nodes) {
|
|
5590
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
5591
|
+
if (role === "heading" && node.name) {
|
|
5592
|
+
headings.push(node.name);
|
|
5593
|
+
}
|
|
5594
|
+
if ((role === "button" || role === "link") && node.name) {
|
|
5595
|
+
const disabled = node.disabled ?? false;
|
|
5596
|
+
buttons.push({ text: node.name, disabled, ref: node.ref });
|
|
5597
|
+
}
|
|
5598
|
+
if (role === "alert" && node.name) {
|
|
5599
|
+
alerts.push(node.name);
|
|
5600
|
+
}
|
|
5601
|
+
if (node.children) {
|
|
5602
|
+
walkNodes(node.children);
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
walkNodes(snapshot.accessibilityTree);
|
|
5607
|
+
const formFields = forms.map((f) => ({
|
|
5608
|
+
label: f.label,
|
|
5609
|
+
name: f.name,
|
|
5610
|
+
id: f.id,
|
|
5611
|
+
value: f.value,
|
|
5612
|
+
type: f.type
|
|
5613
|
+
}));
|
|
5614
|
+
return {
|
|
5615
|
+
url,
|
|
5616
|
+
title,
|
|
5617
|
+
headings,
|
|
5618
|
+
formFields,
|
|
5619
|
+
buttons,
|
|
5620
|
+
alerts,
|
|
5621
|
+
visibleText: pageText.slice(0, 3e3)
|
|
5622
|
+
};
|
|
5623
|
+
}
|
|
5624
|
+
function computeDelta(before, after) {
|
|
5625
|
+
const changes = [];
|
|
5626
|
+
if (before.url !== after.url) {
|
|
5627
|
+
changes.push({ kind: "url", before: before.url, after: after.url });
|
|
5628
|
+
}
|
|
5629
|
+
if (before.title !== after.title) {
|
|
5630
|
+
changes.push({ kind: "title", before: before.title, after: after.title });
|
|
5631
|
+
}
|
|
5632
|
+
const beforeHeadings = new Set(before.headings);
|
|
5633
|
+
const afterHeadings = new Set(after.headings);
|
|
5634
|
+
for (const h of after.headings) {
|
|
5635
|
+
if (!beforeHeadings.has(h)) {
|
|
5636
|
+
changes.push({ kind: "heading_added", after: h });
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
for (const h of before.headings) {
|
|
5640
|
+
if (!afterHeadings.has(h)) {
|
|
5641
|
+
changes.push({ kind: "heading_removed", before: h });
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
const beforeFieldMap = new Map(
|
|
5645
|
+
before.formFields.map((f) => [f.id ?? f.name ?? f.label ?? "", f])
|
|
5646
|
+
);
|
|
5647
|
+
for (const af of after.formFields) {
|
|
5648
|
+
const key = af.id ?? af.name ?? af.label ?? "";
|
|
5649
|
+
const bf = beforeFieldMap.get(key);
|
|
5650
|
+
if (bf && JSON.stringify(bf.value) !== JSON.stringify(af.value)) {
|
|
5651
|
+
changes.push({
|
|
5652
|
+
kind: "field_changed",
|
|
5653
|
+
before: String(bf.value ?? ""),
|
|
5654
|
+
after: String(af.value ?? ""),
|
|
5655
|
+
detail: af.label ?? af.name ?? af.id ?? key
|
|
5656
|
+
});
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
const beforeBtnMap = new Map(before.buttons.map((b) => [b.text, b]));
|
|
5660
|
+
for (const ab of after.buttons) {
|
|
5661
|
+
const bb = beforeBtnMap.get(ab.text);
|
|
5662
|
+
if (bb && bb.disabled !== ab.disabled) {
|
|
5663
|
+
changes.push({
|
|
5664
|
+
kind: "button_changed",
|
|
5665
|
+
detail: ab.text,
|
|
5666
|
+
before: bb.disabled ? "disabled" : "enabled",
|
|
5667
|
+
after: ab.disabled ? "disabled" : "enabled"
|
|
5668
|
+
});
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5671
|
+
const beforeAlerts = new Set(before.alerts);
|
|
5672
|
+
const afterAlerts = new Set(after.alerts);
|
|
5673
|
+
for (const a of after.alerts) {
|
|
5674
|
+
if (!beforeAlerts.has(a)) {
|
|
5675
|
+
changes.push({ kind: "alert_added", after: a });
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
for (const a of before.alerts) {
|
|
5679
|
+
if (!afterAlerts.has(a)) {
|
|
5680
|
+
changes.push({ kind: "alert_removed", before: a });
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
return {
|
|
5684
|
+
changes,
|
|
5685
|
+
before,
|
|
5686
|
+
after,
|
|
5687
|
+
hasChanges: changes.length > 0
|
|
5688
|
+
};
|
|
5689
|
+
}
|
|
5690
|
+
|
|
5401
5691
|
// src/browser/keyboard.ts
|
|
5402
5692
|
var US_KEYBOARD = {
|
|
5403
5693
|
// Letters (lowercase)
|
|
@@ -5557,8 +5847,118 @@ function parseShortcut(combo) {
|
|
|
5557
5847
|
return { modifiers, key };
|
|
5558
5848
|
}
|
|
5559
5849
|
|
|
5850
|
+
// src/browser/review.ts
|
|
5851
|
+
function extractReview(url, title, snapshot, forms, pageText) {
|
|
5852
|
+
const headings = [];
|
|
5853
|
+
const alerts = [];
|
|
5854
|
+
const statusLabels = [];
|
|
5855
|
+
const keyValues = [];
|
|
5856
|
+
const tables = [];
|
|
5857
|
+
const summaryCards = [];
|
|
5858
|
+
function walkNodes(nodes, parentHeading) {
|
|
5859
|
+
let currentHeading = parentHeading;
|
|
5860
|
+
for (const node of nodes) {
|
|
5861
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
5862
|
+
if (role === "heading" && node.name) {
|
|
5863
|
+
headings.push(node.name);
|
|
5864
|
+
currentHeading = node.name;
|
|
5865
|
+
}
|
|
5866
|
+
if (role === "alert" && node.name) {
|
|
5867
|
+
alerts.push(node.name);
|
|
5868
|
+
}
|
|
5869
|
+
if (role === "status" && node.name) {
|
|
5870
|
+
statusLabels.push(node.name);
|
|
5871
|
+
}
|
|
5872
|
+
if (role === "table" || role === "grid") {
|
|
5873
|
+
const table = extractTableFromNode(node);
|
|
5874
|
+
if (table) tables.push(table);
|
|
5875
|
+
}
|
|
5876
|
+
if ((role === "definition" || role === "term") && node.name) {
|
|
5877
|
+
if (role === "term") {
|
|
5878
|
+
keyValues.push({ key: node.name, value: "" });
|
|
5879
|
+
} else if (role === "definition" && keyValues.length > 0) {
|
|
5880
|
+
const last = keyValues[keyValues.length - 1];
|
|
5881
|
+
if (!last.value) last.value = node.name;
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
if (node.children) {
|
|
5885
|
+
walkNodes(node.children, currentHeading);
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
walkNodes(snapshot.accessibilityTree);
|
|
5890
|
+
const textKvPairs = extractKeyValueFromText(pageText);
|
|
5891
|
+
keyValues.push(...textKvPairs);
|
|
5892
|
+
const formEntries = forms.map((f) => ({
|
|
5893
|
+
label: f.label,
|
|
5894
|
+
value: f.value,
|
|
5895
|
+
type: f.type,
|
|
5896
|
+
disabled: f.disabled
|
|
5897
|
+
}));
|
|
5898
|
+
return {
|
|
5899
|
+
url,
|
|
5900
|
+
title,
|
|
5901
|
+
headings,
|
|
5902
|
+
forms: formEntries,
|
|
5903
|
+
alerts,
|
|
5904
|
+
summaryCards,
|
|
5905
|
+
tables,
|
|
5906
|
+
keyValues,
|
|
5907
|
+
statusLabels
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5910
|
+
function extractTableFromNode(node) {
|
|
5911
|
+
const headers = [];
|
|
5912
|
+
const rows = [];
|
|
5913
|
+
function findRows(n) {
|
|
5914
|
+
const role = n.role?.toLowerCase() ?? "";
|
|
5915
|
+
if (role === "columnheader" && n.name) {
|
|
5916
|
+
headers.push(n.name);
|
|
5917
|
+
}
|
|
5918
|
+
if (role === "row") {
|
|
5919
|
+
const cells = [];
|
|
5920
|
+
if (n.children) {
|
|
5921
|
+
for (const child of n.children) {
|
|
5922
|
+
const childRole = child.role?.toLowerCase() ?? "";
|
|
5923
|
+
if ((childRole === "cell" || childRole === "gridcell") && child.name) {
|
|
5924
|
+
cells.push(child.name);
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
if (cells.length > 0) rows.push(cells);
|
|
5929
|
+
}
|
|
5930
|
+
if (n.children) {
|
|
5931
|
+
for (const child of n.children) findRows(child);
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
findRows(node);
|
|
5935
|
+
if (rows.length === 0) return null;
|
|
5936
|
+
return { headers, rows };
|
|
5937
|
+
}
|
|
5938
|
+
function extractKeyValueFromText(text) {
|
|
5939
|
+
const pairs = [];
|
|
5940
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
5941
|
+
for (const line of lines) {
|
|
5942
|
+
const match = line.match(/^([A-Z][A-Za-z0-9 ]{1,30})[:—]\s+(.+)$/);
|
|
5943
|
+
if (match) {
|
|
5944
|
+
pairs.push({ key: match[1].trim(), value: match[2].trim() });
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
return pairs.slice(0, 20);
|
|
5948
|
+
}
|
|
5949
|
+
|
|
5560
5950
|
// src/browser/page.ts
|
|
5561
5951
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
5952
|
+
function normalizeAXCheckedValue(value) {
|
|
5953
|
+
if (typeof value === "boolean") {
|
|
5954
|
+
return value;
|
|
5955
|
+
}
|
|
5956
|
+
if (typeof value === "string") {
|
|
5957
|
+
if (value === "true") return true;
|
|
5958
|
+
if (value === "false") return false;
|
|
5959
|
+
}
|
|
5960
|
+
return void 0;
|
|
5961
|
+
}
|
|
5562
5962
|
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
5563
5963
|
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
5564
5964
|
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
@@ -7344,7 +7744,9 @@ var Page = class {
|
|
|
7344
7744
|
}
|
|
7345
7745
|
}
|
|
7346
7746
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
7347
|
-
const checked =
|
|
7747
|
+
const checked = normalizeAXCheckedValue(
|
|
7748
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
7749
|
+
);
|
|
7348
7750
|
return {
|
|
7349
7751
|
role,
|
|
7350
7752
|
name,
|
|
@@ -7400,7 +7802,9 @@ var Page = class {
|
|
|
7400
7802
|
const ref = nodeRefs.get(node.nodeId);
|
|
7401
7803
|
const name = node.name?.value ?? "";
|
|
7402
7804
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
7403
|
-
const checked =
|
|
7805
|
+
const checked = normalizeAXCheckedValue(
|
|
7806
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
7807
|
+
);
|
|
7404
7808
|
const value = node.value?.value;
|
|
7405
7809
|
const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
|
|
7406
7810
|
interactiveElements.push({
|
|
@@ -7467,6 +7871,45 @@ var Page = class {
|
|
|
7467
7871
|
}
|
|
7468
7872
|
}
|
|
7469
7873
|
}
|
|
7874
|
+
// ============ Delta & Review ============
|
|
7875
|
+
/**
|
|
7876
|
+
* Capture current page state for delta comparison.
|
|
7877
|
+
* Call before an action, then call delta() again after and use computeDelta().
|
|
7878
|
+
*/
|
|
7879
|
+
async captureState() {
|
|
7880
|
+
const [url, title, snapshot, forms, text] = await Promise.all([
|
|
7881
|
+
this.url(),
|
|
7882
|
+
this.title(),
|
|
7883
|
+
this.snapshot(),
|
|
7884
|
+
this.forms(),
|
|
7885
|
+
this.text()
|
|
7886
|
+
]);
|
|
7887
|
+
return extractPageState(url, title, snapshot, forms, text);
|
|
7888
|
+
}
|
|
7889
|
+
/**
|
|
7890
|
+
* Compute what changed between two page states.
|
|
7891
|
+
* If no arguments: captures current state and returns it (for use as "before").
|
|
7892
|
+
* If one argument (before state): captures current state and computes delta.
|
|
7893
|
+
*/
|
|
7894
|
+
async delta(before) {
|
|
7895
|
+
const currentState = await this.captureState();
|
|
7896
|
+
if (!before) return currentState;
|
|
7897
|
+
return computeDelta(before, currentState);
|
|
7898
|
+
}
|
|
7899
|
+
/**
|
|
7900
|
+
* Extract structured review surface from the current page.
|
|
7901
|
+
* Returns headings, form values, alerts, key-value pairs, tables, and status labels.
|
|
7902
|
+
*/
|
|
7903
|
+
async review() {
|
|
7904
|
+
const [url, title, snapshot, forms, text] = await Promise.all([
|
|
7905
|
+
this.url(),
|
|
7906
|
+
this.title(),
|
|
7907
|
+
this.snapshot(),
|
|
7908
|
+
this.forms(),
|
|
7909
|
+
this.text()
|
|
7910
|
+
]);
|
|
7911
|
+
return extractReview(url, title, snapshot, forms, text);
|
|
7912
|
+
}
|
|
7470
7913
|
// ============ Batch Execution ============
|
|
7471
7914
|
/**
|
|
7472
7915
|
* Execute a batch of steps
|
|
@@ -8645,6 +9088,10 @@ function sleep4(ms) {
|
|
|
8645
9088
|
|
|
8646
9089
|
export {
|
|
8647
9090
|
pcmToWav,
|
|
9091
|
+
readString,
|
|
9092
|
+
readStringOr,
|
|
9093
|
+
formatConsoleArg,
|
|
9094
|
+
globToRegex,
|
|
8648
9095
|
SENSITIVE_AUTOCOMPLETE_TOKENS,
|
|
8649
9096
|
redactValueForRecording,
|
|
8650
9097
|
fuzzyMatchElements,
|
|
@@ -8656,7 +9103,6 @@ export {
|
|
|
8656
9103
|
normalizeTraceEvent,
|
|
8657
9104
|
TRACE_BINDING_NAME,
|
|
8658
9105
|
TRACE_SCRIPT,
|
|
8659
|
-
LiveTraceCollector,
|
|
8660
9106
|
addBatchToPage,
|
|
8661
9107
|
validateSteps,
|
|
8662
9108
|
grantAudioPermissions,
|