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
|
@@ -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";
|
|
@@ -866,6 +1135,7 @@ function buildTraceSummary(events, view) {
|
|
|
866
1135
|
case "session":
|
|
867
1136
|
return summarizeSession(events);
|
|
868
1137
|
}
|
|
1138
|
+
throw new Error(`Unsupported trace view: ${view}`);
|
|
869
1139
|
}
|
|
870
1140
|
function buildTraceSummaries(events) {
|
|
871
1141
|
return {
|
|
@@ -879,7 +1149,9 @@ function buildTraceSummaries(events) {
|
|
|
879
1149
|
};
|
|
880
1150
|
}
|
|
881
1151
|
function summarizeWs(events) {
|
|
882
|
-
const relevant = events.filter(
|
|
1152
|
+
const relevant = events.filter(
|
|
1153
|
+
(event) => event.channel === "ws" || event.event.startsWith("ws.")
|
|
1154
|
+
);
|
|
883
1155
|
const connections = /* @__PURE__ */ new Map();
|
|
884
1156
|
for (const event of relevant) {
|
|
885
1157
|
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
@@ -909,7 +1181,7 @@ function summarizeWs(events) {
|
|
|
909
1181
|
}
|
|
910
1182
|
const values = [...connections.values()];
|
|
911
1183
|
const reconnects = values.reduce((count, connection) => {
|
|
912
|
-
return connection.closedAt && !connection.createdAt ? count : count;
|
|
1184
|
+
return connection.closedAt && !connection.createdAt ? count + 1 : count;
|
|
913
1185
|
}, 0);
|
|
914
1186
|
return {
|
|
915
1187
|
view: "ws",
|
|
@@ -1162,6 +1434,31 @@ function frameToStep(frame) {
|
|
|
1162
1434
|
}
|
|
1163
1435
|
}
|
|
1164
1436
|
|
|
1437
|
+
// src/trace/model.ts
|
|
1438
|
+
function createTraceId(prefix = "evt") {
|
|
1439
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1440
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1441
|
+
}
|
|
1442
|
+
function normalizeTraceEvent(event) {
|
|
1443
|
+
return {
|
|
1444
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1445
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1446
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
1447
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
1448
|
+
data: event.data ?? {},
|
|
1449
|
+
...event
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
function inferSeverity(eventName) {
|
|
1453
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1454
|
+
return "error";
|
|
1455
|
+
}
|
|
1456
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1457
|
+
return "warn";
|
|
1458
|
+
}
|
|
1459
|
+
return "info";
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1165
1462
|
// src/trace/script.ts
|
|
1166
1463
|
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
1167
1464
|
var TRACE_SCRIPT = `
|
|
@@ -1441,298 +1738,6 @@ var TRACE_SCRIPT = `
|
|
|
1441
1738
|
})();
|
|
1442
1739
|
`;
|
|
1443
1740
|
|
|
1444
|
-
// src/trace/model.ts
|
|
1445
|
-
function createTraceId(prefix = "evt") {
|
|
1446
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
1447
|
-
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1448
|
-
}
|
|
1449
|
-
function normalizeTraceEvent(event) {
|
|
1450
|
-
return {
|
|
1451
|
-
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1452
|
-
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1453
|
-
elapsedMs: event.elapsedMs ?? 0,
|
|
1454
|
-
severity: event.severity ?? inferSeverity(event.event),
|
|
1455
|
-
data: event.data ?? {},
|
|
1456
|
-
...event
|
|
1457
|
-
};
|
|
1458
|
-
}
|
|
1459
|
-
function inferSeverity(eventName) {
|
|
1460
|
-
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1461
|
-
return "error";
|
|
1462
|
-
}
|
|
1463
|
-
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1464
|
-
return "warn";
|
|
1465
|
-
}
|
|
1466
|
-
return "info";
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// src/trace/live.ts
|
|
1470
|
-
function globToRegex(pattern) {
|
|
1471
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1472
|
-
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1473
|
-
return new RegExp(`^${withWildcards}$`);
|
|
1474
|
-
}
|
|
1475
|
-
var LiveTraceCollector = class {
|
|
1476
|
-
cdp;
|
|
1477
|
-
options;
|
|
1478
|
-
handlers = [];
|
|
1479
|
-
wsUrls = /* @__PURE__ */ new Map();
|
|
1480
|
-
httpUrls = /* @__PURE__ */ new Map();
|
|
1481
|
-
events = [];
|
|
1482
|
-
startTime = Date.now();
|
|
1483
|
-
matchRegex;
|
|
1484
|
-
constructor(cdp, options = {}) {
|
|
1485
|
-
this.cdp = cdp;
|
|
1486
|
-
this.options = options;
|
|
1487
|
-
this.matchRegex = options.match ? globToRegex(options.match) : null;
|
|
1488
|
-
}
|
|
1489
|
-
async start() {
|
|
1490
|
-
await this.cdp.send("Runtime.enable");
|
|
1491
|
-
await this.cdp.send("Page.enable");
|
|
1492
|
-
await this.cdp.send("Network.enable");
|
|
1493
|
-
await this.cdp.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
1494
|
-
await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
|
|
1495
|
-
await this.cdp.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
|
|
1496
|
-
if ((this.options.mode ?? "all") !== "http") {
|
|
1497
|
-
this.subscribe("Network.webSocketCreated", (params) => {
|
|
1498
|
-
const requestId = String(params["requestId"] ?? "");
|
|
1499
|
-
const url = String(params["url"] ?? "");
|
|
1500
|
-
if (!this.matchesUrl(url)) {
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
this.wsUrls.set(requestId, url);
|
|
1504
|
-
void this.emit({
|
|
1505
|
-
channel: "ws",
|
|
1506
|
-
event: "ws.connection.created",
|
|
1507
|
-
summary: `WebSocket opened ${url}`,
|
|
1508
|
-
connectionId: requestId,
|
|
1509
|
-
requestId,
|
|
1510
|
-
url,
|
|
1511
|
-
data: { url }
|
|
1512
|
-
});
|
|
1513
|
-
});
|
|
1514
|
-
this.subscribe("Network.webSocketFrameSent", (params) => {
|
|
1515
|
-
const requestId = String(params["requestId"] ?? "");
|
|
1516
|
-
const response = params["response"];
|
|
1517
|
-
const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
|
|
1518
|
-
const url = this.wsUrls.get(requestId);
|
|
1519
|
-
if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
void this.emit({
|
|
1523
|
-
channel: "ws",
|
|
1524
|
-
event: "ws.frame.sent",
|
|
1525
|
-
summary: `WebSocket frame sent ${requestId}`,
|
|
1526
|
-
connectionId: requestId,
|
|
1527
|
-
requestId,
|
|
1528
|
-
url,
|
|
1529
|
-
data: {
|
|
1530
|
-
opcode: response?.opcode ?? 1,
|
|
1531
|
-
payload,
|
|
1532
|
-
length: response?.payloadData?.length ?? 0
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1535
|
-
});
|
|
1536
|
-
this.subscribe("Network.webSocketFrameReceived", (params) => {
|
|
1537
|
-
const requestId = String(params["requestId"] ?? "");
|
|
1538
|
-
const response = params["response"];
|
|
1539
|
-
const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
|
|
1540
|
-
const url = this.wsUrls.get(requestId);
|
|
1541
|
-
if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
void this.emit({
|
|
1545
|
-
channel: "ws",
|
|
1546
|
-
event: "ws.frame.received",
|
|
1547
|
-
summary: `WebSocket frame received ${requestId}`,
|
|
1548
|
-
connectionId: requestId,
|
|
1549
|
-
requestId,
|
|
1550
|
-
url,
|
|
1551
|
-
data: {
|
|
1552
|
-
opcode: response?.opcode ?? 1,
|
|
1553
|
-
payload,
|
|
1554
|
-
length: response?.payloadData?.length ?? 0
|
|
1555
|
-
}
|
|
1556
|
-
});
|
|
1557
|
-
});
|
|
1558
|
-
this.subscribe("Network.webSocketClosed", (params) => {
|
|
1559
|
-
const requestId = String(params["requestId"] ?? "");
|
|
1560
|
-
const url = this.wsUrls.get(requestId);
|
|
1561
|
-
this.wsUrls.delete(requestId);
|
|
1562
|
-
void this.emit({
|
|
1563
|
-
channel: "ws",
|
|
1564
|
-
event: "ws.connection.closed",
|
|
1565
|
-
summary: `WebSocket closed ${requestId}`,
|
|
1566
|
-
severity: "warn",
|
|
1567
|
-
connectionId: requestId,
|
|
1568
|
-
requestId,
|
|
1569
|
-
url,
|
|
1570
|
-
data: { url }
|
|
1571
|
-
});
|
|
1572
|
-
});
|
|
1573
|
-
}
|
|
1574
|
-
if ((this.options.mode ?? "all") !== "ws") {
|
|
1575
|
-
this.subscribe("Network.requestWillBeSent", (params) => {
|
|
1576
|
-
const request = params["request"];
|
|
1577
|
-
const requestId = String(params["requestId"] ?? "");
|
|
1578
|
-
const url = String(request?.url ?? "");
|
|
1579
|
-
if (!this.matchesUrl(url)) {
|
|
1580
|
-
return;
|
|
1581
|
-
}
|
|
1582
|
-
this.httpUrls.set(requestId, url);
|
|
1583
|
-
void this.emit({
|
|
1584
|
-
channel: "http",
|
|
1585
|
-
event: "http.request.sent",
|
|
1586
|
-
summary: `${request?.method ?? "GET"} ${url}`,
|
|
1587
|
-
requestId,
|
|
1588
|
-
url,
|
|
1589
|
-
data: {
|
|
1590
|
-
method: request?.method ?? "GET",
|
|
1591
|
-
headers: request?.headers ?? {},
|
|
1592
|
-
body: request?.postData ?? null
|
|
1593
|
-
}
|
|
1594
|
-
});
|
|
1595
|
-
});
|
|
1596
|
-
this.subscribe("Network.responseReceived", (params) => {
|
|
1597
|
-
const requestId = String(params["requestId"] ?? "");
|
|
1598
|
-
if (!this.httpUrls.has(requestId)) {
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
const response = params["response"];
|
|
1602
|
-
void this.emit({
|
|
1603
|
-
channel: "http",
|
|
1604
|
-
event: "http.response.received",
|
|
1605
|
-
summary: `${response?.status ?? 0} ${response?.url ?? this.httpUrls.get(requestId) ?? ""}`,
|
|
1606
|
-
requestId,
|
|
1607
|
-
url: response?.url ?? this.httpUrls.get(requestId),
|
|
1608
|
-
data: {
|
|
1609
|
-
status: response?.status ?? 0,
|
|
1610
|
-
headers: response?.headers ?? {},
|
|
1611
|
-
mimeType: response?.mimeType ?? null
|
|
1612
|
-
}
|
|
1613
|
-
});
|
|
1614
|
-
});
|
|
1615
|
-
this.subscribe("Network.loadingFailed", (params) => {
|
|
1616
|
-
const requestId = String(params["requestId"] ?? "");
|
|
1617
|
-
const url = String(params["blockedReason"] ?? this.httpUrls.get(requestId) ?? "");
|
|
1618
|
-
void this.emit({
|
|
1619
|
-
channel: "http",
|
|
1620
|
-
event: "http.response.failed",
|
|
1621
|
-
summary: `HTTP request failed ${requestId}`,
|
|
1622
|
-
severity: "error",
|
|
1623
|
-
requestId,
|
|
1624
|
-
url,
|
|
1625
|
-
data: {
|
|
1626
|
-
errorText: params["errorText"] ?? null,
|
|
1627
|
-
blockedReason: params["blockedReason"] ?? null,
|
|
1628
|
-
canceled: params["canceled"] ?? false
|
|
1629
|
-
}
|
|
1630
|
-
});
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
this.subscribe("Runtime.consoleAPICalled", (params) => {
|
|
1634
|
-
const type = String(params["type"] ?? "log");
|
|
1635
|
-
if (type !== "log" && type !== "warn" && type !== "error") {
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
1639
|
-
const text = args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ");
|
|
1640
|
-
void this.emit({
|
|
1641
|
-
channel: "console",
|
|
1642
|
-
event: `console.${type}`,
|
|
1643
|
-
severity: type === "error" ? "error" : type === "warn" ? "warn" : "info",
|
|
1644
|
-
summary: text || `console.${type}`,
|
|
1645
|
-
data: { args }
|
|
1646
|
-
});
|
|
1647
|
-
});
|
|
1648
|
-
this.subscribe("Runtime.exceptionThrown", (params) => {
|
|
1649
|
-
const details = params["exceptionDetails"] ?? {};
|
|
1650
|
-
const text = String(details["text"] ?? "Runtime exception");
|
|
1651
|
-
void this.emit({
|
|
1652
|
-
channel: "runtime",
|
|
1653
|
-
event: "runtime.exception",
|
|
1654
|
-
severity: "error",
|
|
1655
|
-
summary: text,
|
|
1656
|
-
data: details
|
|
1657
|
-
});
|
|
1658
|
-
});
|
|
1659
|
-
this.subscribe("Runtime.bindingCalled", (params) => {
|
|
1660
|
-
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
1661
|
-
return;
|
|
1662
|
-
}
|
|
1663
|
-
const raw = String(params["payload"] ?? "");
|
|
1664
|
-
try {
|
|
1665
|
-
const payload = JSON.parse(raw);
|
|
1666
|
-
const channel = this.channelForTraceEvent(payload.event);
|
|
1667
|
-
void this.emit({
|
|
1668
|
-
channel,
|
|
1669
|
-
event: payload.event,
|
|
1670
|
-
severity: payload.severity,
|
|
1671
|
-
summary: payload.summary ?? payload.event,
|
|
1672
|
-
ts: payload.ts ? new Date(payload.ts).toISOString() : void 0,
|
|
1673
|
-
data: payload.data ?? {},
|
|
1674
|
-
url: typeof payload.data?.["url"] === "string" ? payload.data["url"] : void 0
|
|
1675
|
-
});
|
|
1676
|
-
} catch {
|
|
1677
|
-
}
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
async stop() {
|
|
1681
|
-
for (const { event, handler } of this.handlers) {
|
|
1682
|
-
this.cdp.off(event, handler);
|
|
1683
|
-
}
|
|
1684
|
-
this.handlers.length = 0;
|
|
1685
|
-
return [...this.events];
|
|
1686
|
-
}
|
|
1687
|
-
getEvents() {
|
|
1688
|
-
return [...this.events];
|
|
1689
|
-
}
|
|
1690
|
-
subscribe(event, handler) {
|
|
1691
|
-
this.cdp.on(event, handler);
|
|
1692
|
-
this.handlers.push({ event, handler });
|
|
1693
|
-
}
|
|
1694
|
-
matchesUrl(url) {
|
|
1695
|
-
if (!this.matchRegex) {
|
|
1696
|
-
return true;
|
|
1697
|
-
}
|
|
1698
|
-
return this.matchRegex.test(url);
|
|
1699
|
-
}
|
|
1700
|
-
formatPayload(payloadData, opcode) {
|
|
1701
|
-
const data = payloadData ?? "";
|
|
1702
|
-
const maxPayload = this.options.maxPayload ?? 256;
|
|
1703
|
-
if (opcode === 2) {
|
|
1704
|
-
const byteLength = Math.floor(data.length * 3 / 4);
|
|
1705
|
-
return `[binary: ${byteLength} bytes]`;
|
|
1706
|
-
}
|
|
1707
|
-
if (data.length > maxPayload) {
|
|
1708
|
-
return `${data.slice(0, maxPayload)}... [truncated, ${data.length} total]`;
|
|
1709
|
-
}
|
|
1710
|
-
return data;
|
|
1711
|
-
}
|
|
1712
|
-
channelForTraceEvent(eventName) {
|
|
1713
|
-
if (eventName.startsWith("ws.")) return "ws";
|
|
1714
|
-
if (eventName.startsWith("http.")) return "http";
|
|
1715
|
-
if (eventName.startsWith("console.")) return "console";
|
|
1716
|
-
if (eventName.startsWith("permission.")) return "permission";
|
|
1717
|
-
if (eventName.startsWith("media.")) return "media";
|
|
1718
|
-
if (eventName.startsWith("voice.")) return "voice";
|
|
1719
|
-
if (eventName.startsWith("dom.")) return "dom";
|
|
1720
|
-
if (eventName.startsWith("runtime.")) return "runtime";
|
|
1721
|
-
return "session";
|
|
1722
|
-
}
|
|
1723
|
-
async emit(event) {
|
|
1724
|
-
const normalized = normalizeTraceEvent({
|
|
1725
|
-
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1726
|
-
sessionId: this.options.sessionId,
|
|
1727
|
-
targetId: this.options.targetId,
|
|
1728
|
-
elapsedMs: event.elapsedMs ?? Date.now() - this.startTime,
|
|
1729
|
-
...event
|
|
1730
|
-
});
|
|
1731
|
-
this.events.push(normalized);
|
|
1732
|
-
await this.options.onEvent?.(normalized);
|
|
1733
|
-
}
|
|
1734
|
-
};
|
|
1735
|
-
|
|
1736
1741
|
// src/actions/executor.ts
|
|
1737
1742
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1738
1743
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -1855,6 +1860,25 @@ function getSuggestion(reason) {
|
|
|
1855
1860
|
}
|
|
1856
1861
|
}
|
|
1857
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
|
+
}
|
|
1858
1882
|
var BatchExecutor = class {
|
|
1859
1883
|
page;
|
|
1860
1884
|
constructor(page) {
|
|
@@ -1900,9 +1924,25 @@ var BatchExecutor = class {
|
|
|
1900
1924
|
})
|
|
1901
1925
|
);
|
|
1902
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
|
+
}
|
|
1903
1939
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1904
1940
|
if (attempt > 0) {
|
|
1905
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
|
+
}
|
|
1906
1946
|
}
|
|
1907
1947
|
try {
|
|
1908
1948
|
this.page.resetLastActionPosition();
|
|
@@ -1920,6 +1960,28 @@ var BatchExecutor = class {
|
|
|
1920
1960
|
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
1921
1961
|
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
1922
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
|
+
}
|
|
1923
1985
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1924
1986
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1925
1987
|
}
|
|
@@ -1929,13 +1991,14 @@ var BatchExecutor = class {
|
|
|
1929
1991
|
traceId: createTraceId("action"),
|
|
1930
1992
|
elapsedMs: Date.now() - startTime,
|
|
1931
1993
|
channel: "action",
|
|
1932
|
-
event: "action.succeeded",
|
|
1933
|
-
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}`,
|
|
1934
1996
|
data: {
|
|
1935
1997
|
action: step.action,
|
|
1936
1998
|
selector: step.selector ?? null,
|
|
1937
1999
|
selectorUsed: result.selectorUsed ?? null,
|
|
1938
|
-
durationMs: Date.now() - stepStart
|
|
2000
|
+
durationMs: Date.now() - stepStart,
|
|
2001
|
+
outcomeStatus: stepResult.outcomeStatus ?? null
|
|
1939
2002
|
},
|
|
1940
2003
|
actionId: `action-${i + 1}`,
|
|
1941
2004
|
stepIndex: i,
|
|
@@ -1945,6 +2008,18 @@ var BatchExecutor = class {
|
|
|
1945
2008
|
})
|
|
1946
2009
|
);
|
|
1947
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
|
+
}
|
|
1948
2023
|
results.push(stepResult);
|
|
1949
2024
|
succeeded = true;
|
|
1950
2025
|
break;
|
|
@@ -1952,59 +2027,63 @@ var BatchExecutor = class {
|
|
|
1952
2027
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1953
2028
|
}
|
|
1954
2029
|
}
|
|
2030
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1955
2031
|
if (!succeeded) {
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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 {
|
|
1965
2045
|
}
|
|
1966
|
-
} catch {
|
|
1967
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);
|
|
1968
2086
|
}
|
|
1969
|
-
const failedResult = {
|
|
1970
|
-
index: i,
|
|
1971
|
-
action: step.action,
|
|
1972
|
-
selector: step.selector,
|
|
1973
|
-
success: false,
|
|
1974
|
-
durationMs: Date.now() - stepStart,
|
|
1975
|
-
error: errorMessage,
|
|
1976
|
-
hints,
|
|
1977
|
-
failureReason: reason,
|
|
1978
|
-
coveringElement,
|
|
1979
|
-
suggestion: getSuggestion(reason),
|
|
1980
|
-
timestamp: Date.now()
|
|
1981
|
-
};
|
|
1982
|
-
if (recording && !recording.skipActions.has(step.action)) {
|
|
1983
|
-
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1984
|
-
}
|
|
1985
|
-
if (recording) {
|
|
1986
|
-
recording.traceEvents.push(
|
|
1987
|
-
normalizeTraceEvent({
|
|
1988
|
-
traceId: createTraceId("action"),
|
|
1989
|
-
elapsedMs: Date.now() - startTime,
|
|
1990
|
-
channel: "action",
|
|
1991
|
-
event: "action.failed",
|
|
1992
|
-
severity: "error",
|
|
1993
|
-
summary: `${step.action} failed: ${errorMessage}`,
|
|
1994
|
-
data: {
|
|
1995
|
-
action: step.action,
|
|
1996
|
-
selector: step.selector ?? null,
|
|
1997
|
-
error: errorMessage,
|
|
1998
|
-
reason
|
|
1999
|
-
},
|
|
2000
|
-
actionId: `action-${i + 1}`,
|
|
2001
|
-
stepIndex: i,
|
|
2002
|
-
selector: step.selector,
|
|
2003
|
-
url: step.url
|
|
2004
|
-
})
|
|
2005
|
-
);
|
|
2006
|
-
}
|
|
2007
|
-
results.push(failedResult);
|
|
2008
2087
|
if (onFail === "stop" && !step.optional) {
|
|
2009
2088
|
stoppedAtIndex = i;
|
|
2010
2089
|
break;
|
|
@@ -2315,6 +2394,14 @@ var BatchExecutor = class {
|
|
|
2315
2394
|
case "forms": {
|
|
2316
2395
|
return { value: await this.page.forms() };
|
|
2317
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
|
+
}
|
|
2318
2405
|
case "screenshot": {
|
|
2319
2406
|
const data = await this.page.screenshot({
|
|
2320
2407
|
format: step.format,
|
|
@@ -2465,6 +2552,35 @@ var BatchExecutor = class {
|
|
|
2465
2552
|
const media = await this.assertMediaTrackLive(step.kind);
|
|
2466
2553
|
return { value: media };
|
|
2467
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
|
+
}
|
|
2468
2584
|
default: {
|
|
2469
2585
|
const action = step.action;
|
|
2470
2586
|
const aliases = {
|
|
@@ -2542,8 +2658,13 @@ Valid actions: ${valid}`);
|
|
|
2542
2658
|
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
2543
2659
|
} catch {
|
|
2544
2660
|
}
|
|
2545
|
-
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2546
|
-
|
|
2661
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2662
|
+
source: TRACE_SCRIPT
|
|
2663
|
+
});
|
|
2664
|
+
await this.page.cdpClient.send("Runtime.evaluate", {
|
|
2665
|
+
expression: TRACE_SCRIPT,
|
|
2666
|
+
awaitPromise: false
|
|
2667
|
+
});
|
|
2547
2668
|
}
|
|
2548
2669
|
async waitForWsMessage(match, where, timeout) {
|
|
2549
2670
|
await this.ensureTraceHooks();
|
|
@@ -2561,12 +2682,12 @@ Valid actions: ${valid}`);
|
|
|
2561
2682
|
clearTimeout(timer);
|
|
2562
2683
|
};
|
|
2563
2684
|
const onCreated = (params) => {
|
|
2564
|
-
wsUrls.set(
|
|
2685
|
+
wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
|
|
2565
2686
|
};
|
|
2566
2687
|
const onFrame = (params) => {
|
|
2567
|
-
const requestId =
|
|
2688
|
+
const requestId = readStringOr(params["requestId"]);
|
|
2568
2689
|
const response = params["response"] ?? {};
|
|
2569
|
-
const payload =
|
|
2690
|
+
const payload = response.payloadData ?? "";
|
|
2570
2691
|
const url = wsUrls.get(requestId) ?? "";
|
|
2571
2692
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2572
2693
|
return;
|
|
@@ -2582,13 +2703,13 @@ Valid actions: ${valid}`);
|
|
|
2582
2703
|
return;
|
|
2583
2704
|
}
|
|
2584
2705
|
try {
|
|
2585
|
-
const parsed = JSON.parse(
|
|
2706
|
+
const parsed = JSON.parse(readStringOr(params["payload"]));
|
|
2586
2707
|
if (parsed.event !== "ws.frame.received") {
|
|
2587
2708
|
return;
|
|
2588
2709
|
}
|
|
2589
2710
|
const data = parsed.data ?? {};
|
|
2590
|
-
const payload =
|
|
2591
|
-
const url =
|
|
2711
|
+
const payload = readStringOr(data["payload"]);
|
|
2712
|
+
const url = readStringOr(data["url"]);
|
|
2592
2713
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2593
2714
|
return;
|
|
2594
2715
|
}
|
|
@@ -2597,7 +2718,7 @@ Valid actions: ${valid}`);
|
|
|
2597
2718
|
}
|
|
2598
2719
|
cleanup();
|
|
2599
2720
|
resolve({
|
|
2600
|
-
requestId:
|
|
2721
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2601
2722
|
url,
|
|
2602
2723
|
payload
|
|
2603
2724
|
});
|
|
@@ -2641,13 +2762,14 @@ Valid actions: ${valid}`);
|
|
|
2641
2762
|
if (!entry || typeof entry !== "object") {
|
|
2642
2763
|
continue;
|
|
2643
2764
|
}
|
|
2644
|
-
const
|
|
2765
|
+
const record = entry;
|
|
2766
|
+
const event = readStringOr(record["event"]);
|
|
2645
2767
|
if (event !== "ws.frame.received") {
|
|
2646
2768
|
continue;
|
|
2647
2769
|
}
|
|
2648
|
-
const data =
|
|
2649
|
-
const payload =
|
|
2650
|
-
const url =
|
|
2770
|
+
const data = record["data"] ?? {};
|
|
2771
|
+
const payload = readStringOr(data["payload"]);
|
|
2772
|
+
const url = readStringOr(data["url"]);
|
|
2651
2773
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2652
2774
|
continue;
|
|
2653
2775
|
}
|
|
@@ -2655,7 +2777,7 @@ Valid actions: ${valid}`);
|
|
|
2655
2777
|
continue;
|
|
2656
2778
|
}
|
|
2657
2779
|
return {
|
|
2658
|
-
requestId:
|
|
2780
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2659
2781
|
url,
|
|
2660
2782
|
payload
|
|
2661
2783
|
};
|
|
@@ -2676,13 +2798,11 @@ Valid actions: ${valid}`);
|
|
|
2676
2798
|
return;
|
|
2677
2799
|
}
|
|
2678
2800
|
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2679
|
-
errors.push(
|
|
2680
|
-
args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
|
|
2681
|
-
);
|
|
2801
|
+
errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
|
|
2682
2802
|
};
|
|
2683
2803
|
const onException = (params) => {
|
|
2684
2804
|
const details = params["exceptionDetails"] ?? {};
|
|
2685
|
-
errors.push(
|
|
2805
|
+
errors.push(readString(details["text"]) ?? "Runtime exception");
|
|
2686
2806
|
};
|
|
2687
2807
|
const timer = setTimeout(() => {
|
|
2688
2808
|
cleanup();
|
|
@@ -3054,6 +3174,30 @@ var ACTION_RULES = {
|
|
|
3054
3174
|
kind: { type: "string", enum: ["audio", "video"] }
|
|
3055
3175
|
},
|
|
3056
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: {}
|
|
3057
3201
|
}
|
|
3058
3202
|
};
|
|
3059
3203
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -3093,7 +3237,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
3093
3237
|
"name",
|
|
3094
3238
|
"state",
|
|
3095
3239
|
"kind",
|
|
3096
|
-
"windowMs"
|
|
3240
|
+
"windowMs",
|
|
3241
|
+
"expectAny",
|
|
3242
|
+
"expectAll",
|
|
3243
|
+
"failIf",
|
|
3244
|
+
"dangerous",
|
|
3245
|
+
"files"
|
|
3097
3246
|
]);
|
|
3098
3247
|
function resolveAction(name) {
|
|
3099
3248
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -3313,6 +3462,64 @@ function validateSteps(steps) {
|
|
|
3313
3462
|
});
|
|
3314
3463
|
}
|
|
3315
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
|
+
}
|
|
3316
3523
|
if (action === "assertText") {
|
|
3317
3524
|
if (!("expect" in obj) && !("value" in obj)) {
|
|
3318
3525
|
errors.push({
|
|
@@ -5373,6 +5580,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
5373
5580
|
});
|
|
5374
5581
|
}
|
|
5375
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
|
+
|
|
5376
5691
|
// src/browser/keyboard.ts
|
|
5377
5692
|
var US_KEYBOARD = {
|
|
5378
5693
|
// Letters (lowercase)
|
|
@@ -5532,8 +5847,118 @@ function parseShortcut(combo) {
|
|
|
5532
5847
|
return { modifiers, key };
|
|
5533
5848
|
}
|
|
5534
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
|
+
|
|
5535
5950
|
// src/browser/page.ts
|
|
5536
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
|
+
}
|
|
5537
5962
|
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
5538
5963
|
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
5539
5964
|
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
@@ -7319,7 +7744,9 @@ var Page = class {
|
|
|
7319
7744
|
}
|
|
7320
7745
|
}
|
|
7321
7746
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
7322
|
-
const checked =
|
|
7747
|
+
const checked = normalizeAXCheckedValue(
|
|
7748
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
7749
|
+
);
|
|
7323
7750
|
return {
|
|
7324
7751
|
role,
|
|
7325
7752
|
name,
|
|
@@ -7375,7 +7802,9 @@ var Page = class {
|
|
|
7375
7802
|
const ref = nodeRefs.get(node.nodeId);
|
|
7376
7803
|
const name = node.name?.value ?? "";
|
|
7377
7804
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
7378
|
-
const checked =
|
|
7805
|
+
const checked = normalizeAXCheckedValue(
|
|
7806
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
7807
|
+
);
|
|
7379
7808
|
const value = node.value?.value;
|
|
7380
7809
|
const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
|
|
7381
7810
|
interactiveElements.push({
|
|
@@ -7442,6 +7871,45 @@ var Page = class {
|
|
|
7442
7871
|
}
|
|
7443
7872
|
}
|
|
7444
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
|
+
}
|
|
7445
7913
|
// ============ Batch Execution ============
|
|
7446
7914
|
/**
|
|
7447
7915
|
* Execute a batch of steps
|
|
@@ -8620,6 +9088,10 @@ function sleep4(ms) {
|
|
|
8620
9088
|
|
|
8621
9089
|
export {
|
|
8622
9090
|
pcmToWav,
|
|
9091
|
+
readString,
|
|
9092
|
+
readStringOr,
|
|
9093
|
+
formatConsoleArg,
|
|
9094
|
+
globToRegex,
|
|
8623
9095
|
SENSITIVE_AUTOCOMPLETE_TOKENS,
|
|
8624
9096
|
redactValueForRecording,
|
|
8625
9097
|
fuzzyMatchElements,
|
|
@@ -8627,11 +9099,10 @@ export {
|
|
|
8627
9099
|
buildTraceSummaries,
|
|
8628
9100
|
createRecordingManifest,
|
|
8629
9101
|
canonicalizeRecordingArtifact,
|
|
8630
|
-
TRACE_BINDING_NAME,
|
|
8631
|
-
TRACE_SCRIPT,
|
|
8632
9102
|
createTraceId,
|
|
8633
9103
|
normalizeTraceEvent,
|
|
8634
|
-
|
|
9104
|
+
TRACE_BINDING_NAME,
|
|
9105
|
+
TRACE_SCRIPT,
|
|
8635
9106
|
addBatchToPage,
|
|
8636
9107
|
validateSteps,
|
|
8637
9108
|
grantAudioPermissions,
|