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.
Files changed (37) hide show
  1. package/README.md +38 -3
  2. package/dist/actions.cjs +848 -105
  3. package/dist/actions.d.cts +101 -4
  4. package/dist/actions.d.ts +101 -4
  5. package/dist/actions.mjs +17 -1
  6. package/dist/{browser-MEWT75IB.mjs → browser-4ZHNAQR5.mjs} +2 -2
  7. package/dist/browser.cjs +1684 -130
  8. package/dist/browser.d.cts +230 -6
  9. package/dist/browser.d.ts +230 -6
  10. package/dist/browser.mjs +37 -5
  11. package/dist/chunk-EZNZ72VA.mjs +563 -0
  12. package/dist/{chunk-ZAXQ5OTV.mjs → chunk-FEEGNSHB.mjs} +606 -12
  13. package/dist/{chunk-WPNW23CE.mjs → chunk-IRLHCVNH.mjs} +345 -7
  14. package/dist/chunk-MIJ7UIKB.mjs +96 -0
  15. package/dist/{chunk-USYSHCI3.mjs → chunk-MRY3HRFJ.mjs} +841 -370
  16. package/dist/chunk-OIHU7OFY.mjs +91 -0
  17. package/dist/{chunk-7YVCOL2W.mjs → chunk-ZDODXEBD.mjs} +637 -105
  18. package/dist/cli.mjs +1280 -549
  19. package/dist/combobox-RAKBA2BW.mjs +6 -0
  20. package/dist/index.cjs +1976 -144
  21. package/dist/index.d.cts +57 -6
  22. package/dist/index.d.ts +57 -6
  23. package/dist/index.mjs +206 -7
  24. package/dist/{page-XPS6IC6V.mjs → page-SD64DY3F.mjs} +1 -1
  25. package/dist/providers.cjs +637 -2
  26. package/dist/providers.d.cts +2 -2
  27. package/dist/providers.d.ts +2 -2
  28. package/dist/providers.mjs +17 -3
  29. package/dist/{types-Cvvf0oGu.d.ts → types-B_v62K7C.d.ts} +147 -3
  30. package/dist/types-DeVSWhXj.d.cts +142 -0
  31. package/dist/types-DeVSWhXj.d.ts +142 -0
  32. package/dist/{types-C9ySEdOX.d.cts → types-Yuybzq53.d.cts} +147 -3
  33. package/dist/upload-E6MCC2OF.mjs +6 -0
  34. package/package.json +10 -3
  35. package/dist/chunk-BRAFQUMG.mjs +0 -229
  36. package/dist/types--wXNHUwt.d.cts +0 -56
  37. package/dist/types--wXNHUwt.d.ts +0 -56
@@ -2,6 +2,333 @@ import {
2
2
  CDPError
3
3
  } from "./chunk-JXAUPHZM.mjs";
4
4
 
5
+ // src/utils/strings.ts
6
+ function readString(value) {
7
+ return typeof value === "string" ? value : void 0;
8
+ }
9
+ function readStringOr(value, fallback = "") {
10
+ return readString(value) ?? fallback;
11
+ }
12
+ function formatConsoleArg(entry) {
13
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
14
+ }
15
+ function globToRegex(pattern) {
16
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
17
+ const withWildcards = escaped.replace(/\*/g, ".*");
18
+ return new RegExp(`^${withWildcards}$`);
19
+ }
20
+
21
+ // src/actions/conditions.ts
22
+ var NetworkResponseTracker = class {
23
+ responses = [];
24
+ listening = false;
25
+ handler = null;
26
+ start(cdp) {
27
+ if (this.listening) return;
28
+ this.listening = true;
29
+ this.handler = (params) => {
30
+ const response = params["response"];
31
+ if (response) {
32
+ this.responses.push({ url: response.url, status: response.status });
33
+ }
34
+ };
35
+ cdp.on("Network.responseReceived", this.handler);
36
+ }
37
+ stop(cdp) {
38
+ if (this.handler) {
39
+ cdp.off("Network.responseReceived", this.handler);
40
+ this.handler = null;
41
+ }
42
+ this.listening = false;
43
+ }
44
+ getResponses() {
45
+ return this.responses;
46
+ }
47
+ reset() {
48
+ this.responses = [];
49
+ }
50
+ };
51
+ async function captureStateSignature(page) {
52
+ try {
53
+ const url = await page.url();
54
+ const text = await page.text();
55
+ const truncated = text.slice(0, 2e3);
56
+ return `${url}|${simpleHash(truncated)}`;
57
+ } catch {
58
+ return "";
59
+ }
60
+ }
61
+ function simpleHash(str) {
62
+ let hash = 0;
63
+ for (let i = 0; i < str.length; i++) {
64
+ const char = str.charCodeAt(i);
65
+ hash = (hash << 5) - hash + char | 0;
66
+ }
67
+ return hash.toString(36);
68
+ }
69
+ async function evaluateCondition(condition, page, context = {}) {
70
+ switch (condition.kind) {
71
+ case "urlMatches": {
72
+ try {
73
+ const currentUrl = await page.url();
74
+ const regex = globToRegex(condition.pattern);
75
+ const matched = regex.test(currentUrl);
76
+ return {
77
+ condition,
78
+ matched,
79
+ detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
80
+ };
81
+ } catch {
82
+ return { condition, matched: false, detail: "Failed to get current URL" };
83
+ }
84
+ }
85
+ case "elementVisible": {
86
+ try {
87
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
88
+ for (const sel of selectors) {
89
+ const visible = await page.waitFor(sel, {
90
+ timeout: 2e3,
91
+ optional: true,
92
+ state: "visible"
93
+ });
94
+ if (visible) {
95
+ return { condition, matched: true, detail: `Element "${sel}" is visible` };
96
+ }
97
+ }
98
+ return { condition, matched: false, detail: "No matching visible element found" };
99
+ } catch {
100
+ return { condition, matched: false, detail: "Visibility check failed" };
101
+ }
102
+ }
103
+ case "elementHidden": {
104
+ try {
105
+ const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
106
+ for (const sel of selectors) {
107
+ const visible = await page.waitFor(sel, {
108
+ timeout: 500,
109
+ optional: true,
110
+ state: "visible"
111
+ });
112
+ if (visible) {
113
+ return { condition, matched: false, detail: `Element "${sel}" is still visible` };
114
+ }
115
+ }
116
+ return { condition, matched: true, detail: "Element is hidden or not found" };
117
+ } catch {
118
+ return { condition, matched: true, detail: "Element is hidden (check threw)" };
119
+ }
120
+ }
121
+ case "textAppears": {
122
+ try {
123
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
124
+ const text = await page.text(selector);
125
+ const matched = text.includes(condition.text);
126
+ return {
127
+ condition,
128
+ matched,
129
+ detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
130
+ };
131
+ } catch {
132
+ return { condition, matched: false, detail: "Failed to get page text" };
133
+ }
134
+ }
135
+ case "textChanges": {
136
+ try {
137
+ const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
138
+ const text = await page.text(selector);
139
+ if (condition.to !== void 0) {
140
+ const matched = text.includes(condition.to);
141
+ return {
142
+ condition,
143
+ matched,
144
+ detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
145
+ };
146
+ }
147
+ return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
148
+ } catch {
149
+ return { condition, matched: false, detail: "Failed to get text for change detection" };
150
+ }
151
+ }
152
+ case "networkResponse": {
153
+ const tracker = context.networkTracker;
154
+ if (!tracker) {
155
+ return { condition, matched: false, detail: "No network tracker active" };
156
+ }
157
+ const regex = globToRegex(condition.urlPattern);
158
+ const responses = tracker.getResponses();
159
+ for (const resp of responses) {
160
+ if (regex.test(resp.url)) {
161
+ if (condition.status !== void 0 && resp.status !== condition.status) {
162
+ continue;
163
+ }
164
+ return {
165
+ condition,
166
+ matched: true,
167
+ detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
168
+ };
169
+ }
170
+ }
171
+ return {
172
+ condition,
173
+ matched: false,
174
+ detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
175
+ };
176
+ }
177
+ case "stateSignatureChanges": {
178
+ if (!context.beforeSignature) {
179
+ return { condition, matched: false, detail: "No before-signature captured" };
180
+ }
181
+ const afterSignature = await captureStateSignature(page);
182
+ const matched = afterSignature !== context.beforeSignature;
183
+ return {
184
+ condition,
185
+ matched,
186
+ detail: matched ? "Page state changed" : "Page state unchanged"
187
+ };
188
+ }
189
+ default: {
190
+ const _exhaustive = condition;
191
+ return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
192
+ }
193
+ }
194
+ }
195
+ async function evaluateOutcome(page, options) {
196
+ const {
197
+ expectAny,
198
+ expectAll,
199
+ failIf,
200
+ dangerous = false,
201
+ networkTracker,
202
+ beforeSignature
203
+ } = options;
204
+ const allMatched = [];
205
+ const context = { networkTracker, beforeSignature };
206
+ if (failIf && failIf.length > 0) {
207
+ for (const condition of failIf) {
208
+ const result = await evaluateCondition(condition, page, context);
209
+ allMatched.push(result);
210
+ if (result.matched) {
211
+ return {
212
+ outcomeStatus: "failed",
213
+ matchedConditions: allMatched,
214
+ retrySafe: !dangerous
215
+ };
216
+ }
217
+ }
218
+ }
219
+ if (expectAll && expectAll.length > 0) {
220
+ let allPassed = true;
221
+ for (const condition of expectAll) {
222
+ const result = await evaluateCondition(condition, page, context);
223
+ allMatched.push(result);
224
+ if (!result.matched) {
225
+ allPassed = false;
226
+ }
227
+ }
228
+ if (!allPassed) {
229
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
230
+ return {
231
+ outcomeStatus: status,
232
+ matchedConditions: allMatched,
233
+ retrySafe: !dangerous
234
+ };
235
+ }
236
+ if (!expectAny || expectAny.length === 0) {
237
+ return {
238
+ outcomeStatus: "success",
239
+ matchedConditions: allMatched,
240
+ retrySafe: true
241
+ };
242
+ }
243
+ }
244
+ if (expectAny && expectAny.length > 0) {
245
+ let anyPassed = false;
246
+ for (const condition of expectAny) {
247
+ const result = await evaluateCondition(condition, page, context);
248
+ allMatched.push(result);
249
+ if (result.matched) {
250
+ anyPassed = true;
251
+ }
252
+ }
253
+ if (anyPassed) {
254
+ return {
255
+ outcomeStatus: "success",
256
+ matchedConditions: allMatched,
257
+ retrySafe: true
258
+ };
259
+ }
260
+ const status = dangerous ? "unsafe_to_retry" : "ambiguous";
261
+ return {
262
+ outcomeStatus: status,
263
+ matchedConditions: allMatched,
264
+ retrySafe: !dangerous
265
+ };
266
+ }
267
+ return {
268
+ outcomeStatus: "success",
269
+ matchedConditions: allMatched,
270
+ retrySafe: true
271
+ };
272
+ }
273
+
274
+ // src/actions/combinators.ts
275
+ async function conditionAny(conditions, page, context) {
276
+ const results = [];
277
+ let winnerIndex;
278
+ for (let i = 0; i < conditions.length; i++) {
279
+ const result = await evaluateCondition(conditions[i], page, context);
280
+ results.push(result);
281
+ if (result.matched && winnerIndex === void 0) {
282
+ winnerIndex = i;
283
+ }
284
+ }
285
+ return {
286
+ matched: winnerIndex !== void 0,
287
+ matchedConditions: results,
288
+ winnerIndex
289
+ };
290
+ }
291
+ async function conditionAll(conditions, page, context) {
292
+ const results = [];
293
+ let allMatched = true;
294
+ for (const condition of conditions) {
295
+ const result = await evaluateCondition(condition, page, context);
296
+ results.push(result);
297
+ if (!result.matched) allMatched = false;
298
+ }
299
+ return {
300
+ matched: allMatched,
301
+ matchedConditions: results
302
+ };
303
+ }
304
+ async function conditionNot(condition, page, context) {
305
+ const result = await evaluateCondition(condition, page, context);
306
+ return {
307
+ matched: !result.matched,
308
+ matchedConditions: [
309
+ {
310
+ condition: result.condition,
311
+ matched: !result.matched,
312
+ detail: result.matched ? `NOT: condition was true (inverted to false): ${result.detail}` : `NOT: condition was false (inverted to true): ${result.detail}`
313
+ }
314
+ ]
315
+ };
316
+ }
317
+ async function conditionRace(conditions, page, options = {}) {
318
+ const { timeout = 1e4, pollInterval = 200, networkTracker, beforeSignature } = options;
319
+ const context = { networkTracker, beforeSignature };
320
+ const startTime = Date.now();
321
+ const deadline = startTime + timeout;
322
+ const immediate = await conditionAny(conditions, page, context);
323
+ if (immediate.matched) return immediate;
324
+ while (Date.now() < deadline) {
325
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
326
+ const result = await conditionAny(conditions, page, context);
327
+ if (result.matched) return result;
328
+ }
329
+ return await conditionAny(conditions, page, context);
330
+ }
331
+
5
332
  // src/actions/executor.ts
6
333
  import * as fs from "fs";
7
334
  import { join } from "path";
@@ -860,7 +1187,9 @@ function buildTraceSummaries(events) {
860
1187
  };
861
1188
  }
862
1189
  function summarizeWs(events) {
863
- const relevant = events.filter((event) => event.channel === "ws" || event.event.startsWith("ws."));
1190
+ const relevant = events.filter(
1191
+ (event) => event.channel === "ws" || event.event.startsWith("ws.")
1192
+ );
864
1193
  const connections = /* @__PURE__ */ new Map();
865
1194
  for (const event of relevant) {
866
1195
  const id = event.connectionId ?? event.requestId ?? event.traceId;
@@ -890,7 +1219,7 @@ function summarizeWs(events) {
890
1219
  }
891
1220
  const values = [...connections.values()];
892
1221
  const reconnects = values.reduce((count, connection) => {
893
- return connection.closedAt && !connection.createdAt ? count : count;
1222
+ return connection.closedAt && !connection.createdAt ? count + 1 : count;
894
1223
  }, 0);
895
1224
  return {
896
1225
  view: "ws",
@@ -1143,6 +1472,31 @@ function frameToStep(frame) {
1143
1472
  }
1144
1473
  }
1145
1474
 
1475
+ // src/trace/model.ts
1476
+ function createTraceId(prefix = "evt") {
1477
+ const random = Math.random().toString(36).slice(2, 10);
1478
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1479
+ }
1480
+ function normalizeTraceEvent(event) {
1481
+ return {
1482
+ traceId: event.traceId ?? createTraceId(event.channel),
1483
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1484
+ elapsedMs: event.elapsedMs ?? 0,
1485
+ severity: event.severity ?? inferSeverity(event.event),
1486
+ data: event.data ?? {},
1487
+ ...event
1488
+ };
1489
+ }
1490
+ function inferSeverity(eventName) {
1491
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1492
+ return "error";
1493
+ }
1494
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1495
+ return "warn";
1496
+ }
1497
+ return "info";
1498
+ }
1499
+
1146
1500
  // src/trace/script.ts
1147
1501
  var TRACE_BINDING_NAME = "__bpTraceBinding";
1148
1502
  var TRACE_SCRIPT = `
@@ -1422,38 +1776,6 @@ var TRACE_SCRIPT = `
1422
1776
  })();
1423
1777
  `;
1424
1778
 
1425
- // src/trace/model.ts
1426
- function createTraceId(prefix = "evt") {
1427
- const random = Math.random().toString(36).slice(2, 10);
1428
- return `${prefix}-${Date.now().toString(36)}-${random}`;
1429
- }
1430
- function normalizeTraceEvent(event) {
1431
- return {
1432
- traceId: event.traceId ?? createTraceId(event.channel),
1433
- ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1434
- elapsedMs: event.elapsedMs ?? 0,
1435
- severity: event.severity ?? inferSeverity(event.event),
1436
- data: event.data ?? {},
1437
- ...event
1438
- };
1439
- }
1440
- function inferSeverity(eventName) {
1441
- if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1442
- return "error";
1443
- }
1444
- if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1445
- return "warn";
1446
- }
1447
- return "info";
1448
- }
1449
-
1450
- // src/trace/live.ts
1451
- function globToRegex(pattern) {
1452
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1453
- const withWildcards = escaped.replace(/\*/g, ".*");
1454
- return new RegExp(`^${withWildcards}$`);
1455
- }
1456
-
1457
1779
  // src/actions/executor.ts
1458
1780
  var DEFAULT_TIMEOUT = 3e4;
1459
1781
  var DEFAULT_RECORDING_SKIP_ACTIONS = [
@@ -1576,6 +1898,25 @@ function getSuggestion(reason) {
1576
1898
  }
1577
1899
  }
1578
1900
  }
1901
+ function hasOutcomeConditions(step) {
1902
+ return step.expectAny !== void 0 && step.expectAny.length > 0 || step.expectAll !== void 0 && step.expectAll.length > 0 || step.failIf !== void 0 && step.failIf.length > 0;
1903
+ }
1904
+ function needsNetworkTracking(step) {
1905
+ const allConditions = [
1906
+ ...step.expectAny ?? [],
1907
+ ...step.expectAll ?? [],
1908
+ ...step.failIf ?? []
1909
+ ];
1910
+ return allConditions.some((c) => c.kind === "networkResponse");
1911
+ }
1912
+ function needsStateSignature(step) {
1913
+ const allConditions = [
1914
+ ...step.expectAny ?? [],
1915
+ ...step.expectAll ?? [],
1916
+ ...step.failIf ?? []
1917
+ ];
1918
+ return allConditions.some((c) => c.kind === "stateSignatureChanges");
1919
+ }
1579
1920
  var BatchExecutor = class {
1580
1921
  page;
1581
1922
  constructor(page) {
@@ -1621,9 +1962,25 @@ var BatchExecutor = class {
1621
1962
  })
1622
1963
  );
1623
1964
  }
1965
+ const hasOutcome = hasOutcomeConditions(step);
1966
+ let networkTracker;
1967
+ let beforeSignature;
1968
+ if (hasOutcome) {
1969
+ if (needsNetworkTracking(step)) {
1970
+ networkTracker = new NetworkResponseTracker();
1971
+ networkTracker.start(this.page.cdpClient);
1972
+ }
1973
+ if (needsStateSignature(step)) {
1974
+ beforeSignature = await captureStateSignature(this.page);
1975
+ }
1976
+ }
1624
1977
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1625
1978
  if (attempt > 0) {
1626
1979
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
1980
+ if (networkTracker) networkTracker.reset();
1981
+ if (hasOutcome && needsStateSignature(step)) {
1982
+ beforeSignature = await captureStateSignature(this.page);
1983
+ }
1627
1984
  }
1628
1985
  try {
1629
1986
  this.page.resetLastActionPosition();
@@ -1641,6 +1998,28 @@ var BatchExecutor = class {
1641
1998
  coordinates: this.page.getLastActionCoordinates() ?? void 0,
1642
1999
  boundingBox: this.page.getLastActionBoundingBox() ?? void 0
1643
2000
  };
2001
+ if (hasOutcome) {
2002
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
2003
+ const outcome = await evaluateOutcome(this.page, {
2004
+ expectAny: step.expectAny,
2005
+ expectAll: step.expectAll,
2006
+ failIf: step.failIf,
2007
+ dangerous: step.dangerous,
2008
+ networkTracker,
2009
+ beforeSignature
2010
+ });
2011
+ stepResult.outcomeStatus = outcome.outcomeStatus;
2012
+ stepResult.matchedConditions = outcome.matchedConditions;
2013
+ stepResult.retrySafe = outcome.retrySafe;
2014
+ if (outcome.outcomeStatus !== "success") {
2015
+ stepResult.success = false;
2016
+ stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
2017
+ const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
2018
+ if (failedDetails.length > 0) {
2019
+ stepResult.suggestion = failedDetails.join("; ");
2020
+ }
2021
+ }
2022
+ }
1644
2023
  if (recording && !recording.skipActions.has(step.action)) {
1645
2024
  await this.captureRecordingFrame(step, stepResult, recording);
1646
2025
  }
@@ -1650,13 +2029,14 @@ var BatchExecutor = class {
1650
2029
  traceId: createTraceId("action"),
1651
2030
  elapsedMs: Date.now() - startTime,
1652
2031
  channel: "action",
1653
- event: "action.succeeded",
1654
- summary: `${step.action} succeeded`,
2032
+ event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
2033
+ summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
1655
2034
  data: {
1656
2035
  action: step.action,
1657
2036
  selector: step.selector ?? null,
1658
2037
  selectorUsed: result.selectorUsed ?? null,
1659
- durationMs: Date.now() - stepStart
2038
+ durationMs: Date.now() - stepStart,
2039
+ outcomeStatus: stepResult.outcomeStatus ?? null
1660
2040
  },
1661
2041
  actionId: `action-${i + 1}`,
1662
2042
  stepIndex: i,
@@ -1666,6 +2046,18 @@ var BatchExecutor = class {
1666
2046
  })
1667
2047
  );
1668
2048
  }
2049
+ if (hasOutcome && !stepResult.success) {
2050
+ if (step.dangerous) {
2051
+ results.push(stepResult);
2052
+ break;
2053
+ }
2054
+ if (attempt < maxAttempts - 1) {
2055
+ lastError = new Error(stepResult.error ?? "Outcome failed");
2056
+ continue;
2057
+ }
2058
+ results.push(stepResult);
2059
+ break;
2060
+ }
1669
2061
  results.push(stepResult);
1670
2062
  succeeded = true;
1671
2063
  break;
@@ -1673,59 +2065,63 @@ var BatchExecutor = class {
1673
2065
  lastError = error instanceof Error ? error : new Error(String(error));
1674
2066
  }
1675
2067
  }
2068
+ if (networkTracker) networkTracker.stop(this.page.cdpClient);
1676
2069
  if (!succeeded) {
1677
- const errorMessage = lastError?.message ?? "Unknown error";
1678
- let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
1679
- const { reason, coveringElement } = classifyFailure(lastError);
1680
- if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
1681
- try {
1682
- const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
1683
- const autoHints = await generateHints(this.page, selectors, step.action, 3);
1684
- if (autoHints.length > 0) {
1685
- hints = autoHints;
2070
+ const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
2071
+ if (!resultAlreadyPushed) {
2072
+ const errorMessage = lastError?.message ?? "Unknown error";
2073
+ let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
2074
+ const { reason, coveringElement } = classifyFailure(lastError);
2075
+ if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
2076
+ try {
2077
+ const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
2078
+ const autoHints = await generateHints(this.page, selectors, step.action, 3);
2079
+ if (autoHints.length > 0) {
2080
+ hints = autoHints;
2081
+ }
2082
+ } catch {
1686
2083
  }
1687
- } catch {
1688
2084
  }
2085
+ const failedResult = {
2086
+ index: i,
2087
+ action: step.action,
2088
+ selector: step.selector,
2089
+ success: false,
2090
+ durationMs: Date.now() - stepStart,
2091
+ error: errorMessage,
2092
+ hints,
2093
+ failureReason: reason,
2094
+ coveringElement,
2095
+ suggestion: getSuggestion(reason),
2096
+ timestamp: Date.now()
2097
+ };
2098
+ if (recording && !recording.skipActions.has(step.action)) {
2099
+ await this.captureRecordingFrame(step, failedResult, recording);
2100
+ }
2101
+ if (recording) {
2102
+ recording.traceEvents.push(
2103
+ normalizeTraceEvent({
2104
+ traceId: createTraceId("action"),
2105
+ elapsedMs: Date.now() - startTime,
2106
+ channel: "action",
2107
+ event: "action.failed",
2108
+ severity: "error",
2109
+ summary: `${step.action} failed: ${errorMessage}`,
2110
+ data: {
2111
+ action: step.action,
2112
+ selector: step.selector ?? null,
2113
+ error: errorMessage,
2114
+ reason
2115
+ },
2116
+ actionId: `action-${i + 1}`,
2117
+ stepIndex: i,
2118
+ selector: step.selector,
2119
+ url: step.url
2120
+ })
2121
+ );
2122
+ }
2123
+ results.push(failedResult);
1689
2124
  }
1690
- const failedResult = {
1691
- index: i,
1692
- action: step.action,
1693
- selector: step.selector,
1694
- success: false,
1695
- durationMs: Date.now() - stepStart,
1696
- error: errorMessage,
1697
- hints,
1698
- failureReason: reason,
1699
- coveringElement,
1700
- suggestion: getSuggestion(reason),
1701
- timestamp: Date.now()
1702
- };
1703
- if (recording && !recording.skipActions.has(step.action)) {
1704
- await this.captureRecordingFrame(step, failedResult, recording);
1705
- }
1706
- if (recording) {
1707
- recording.traceEvents.push(
1708
- normalizeTraceEvent({
1709
- traceId: createTraceId("action"),
1710
- elapsedMs: Date.now() - startTime,
1711
- channel: "action",
1712
- event: "action.failed",
1713
- severity: "error",
1714
- summary: `${step.action} failed: ${errorMessage}`,
1715
- data: {
1716
- action: step.action,
1717
- selector: step.selector ?? null,
1718
- error: errorMessage,
1719
- reason
1720
- },
1721
- actionId: `action-${i + 1}`,
1722
- stepIndex: i,
1723
- selector: step.selector,
1724
- url: step.url
1725
- })
1726
- );
1727
- }
1728
- results.push(failedResult);
1729
2125
  if (onFail === "stop" && !step.optional) {
1730
2126
  stoppedAtIndex = i;
1731
2127
  break;
@@ -2036,6 +2432,14 @@ var BatchExecutor = class {
2036
2432
  case "forms": {
2037
2433
  return { value: await this.page.forms() };
2038
2434
  }
2435
+ case "delta": {
2436
+ const review = await this.page.review();
2437
+ return { value: review };
2438
+ }
2439
+ case "review": {
2440
+ const review = await this.page.review();
2441
+ return { value: review };
2442
+ }
2039
2443
  case "screenshot": {
2040
2444
  const data = await this.page.screenshot({
2041
2445
  format: step.format,
@@ -2186,6 +2590,35 @@ var BatchExecutor = class {
2186
2590
  const media = await this.assertMediaTrackLive(step.kind);
2187
2591
  return { value: media };
2188
2592
  }
2593
+ case "chooseOption": {
2594
+ const { chooseOption } = await import("./combobox-RAKBA2BW.mjs");
2595
+ if (!step.value) throw new Error("chooseOption requires value");
2596
+ const result = await chooseOption(this.page, {
2597
+ trigger: step.trigger ?? step.selector ?? "",
2598
+ listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
2599
+ value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
2600
+ match: step.match,
2601
+ timeout: step.timeout ?? timeout
2602
+ });
2603
+ if (!result.success) {
2604
+ throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
2605
+ }
2606
+ return { value: result };
2607
+ }
2608
+ case "upload": {
2609
+ const { uploadFiles } = await import("./upload-E6MCC2OF.mjs");
2610
+ if (!step.selector) throw new Error("upload requires selector");
2611
+ if (!step.files || step.files.length === 0) throw new Error("upload requires files");
2612
+ const result = await uploadFiles(this.page, {
2613
+ selector: step.selector,
2614
+ files: step.files,
2615
+ timeout: step.timeout ?? timeout
2616
+ });
2617
+ if (!result.accepted) {
2618
+ throw new Error(result.error ?? "Upload was not accepted");
2619
+ }
2620
+ return { value: result };
2621
+ }
2189
2622
  default: {
2190
2623
  const action = step.action;
2191
2624
  const aliases = {
@@ -2263,8 +2696,13 @@ Valid actions: ${valid}`);
2263
2696
  await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
2264
2697
  } catch {
2265
2698
  }
2266
- await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
2267
- await this.page.cdpClient.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
2699
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
2700
+ source: TRACE_SCRIPT
2701
+ });
2702
+ await this.page.cdpClient.send("Runtime.evaluate", {
2703
+ expression: TRACE_SCRIPT,
2704
+ awaitPromise: false
2705
+ });
2268
2706
  }
2269
2707
  async waitForWsMessage(match, where, timeout) {
2270
2708
  await this.ensureTraceHooks();
@@ -2282,12 +2720,12 @@ Valid actions: ${valid}`);
2282
2720
  clearTimeout(timer);
2283
2721
  };
2284
2722
  const onCreated = (params) => {
2285
- wsUrls.set(String(params["requestId"] ?? ""), String(params["url"] ?? ""));
2723
+ wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
2286
2724
  };
2287
2725
  const onFrame = (params) => {
2288
- const requestId = String(params["requestId"] ?? "");
2726
+ const requestId = readStringOr(params["requestId"]);
2289
2727
  const response = params["response"] ?? {};
2290
- const payload = String(response.payloadData ?? "");
2728
+ const payload = response.payloadData ?? "";
2291
2729
  const url = wsUrls.get(requestId) ?? "";
2292
2730
  if (!regex.test(url) && !regex.test(payload)) {
2293
2731
  return;
@@ -2303,13 +2741,13 @@ Valid actions: ${valid}`);
2303
2741
  return;
2304
2742
  }
2305
2743
  try {
2306
- const parsed = JSON.parse(String(params["payload"] ?? ""));
2744
+ const parsed = JSON.parse(readStringOr(params["payload"]));
2307
2745
  if (parsed.event !== "ws.frame.received") {
2308
2746
  return;
2309
2747
  }
2310
2748
  const data = parsed.data ?? {};
2311
- const payload = String(data["payload"] ?? "");
2312
- const url = String(data["url"] ?? "");
2749
+ const payload = readStringOr(data["payload"]);
2750
+ const url = readStringOr(data["url"]);
2313
2751
  if (!regex.test(url) && !regex.test(payload)) {
2314
2752
  return;
2315
2753
  }
@@ -2318,7 +2756,7 @@ Valid actions: ${valid}`);
2318
2756
  }
2319
2757
  cleanup();
2320
2758
  resolve({
2321
- requestId: String(data["connectionId"] ?? ""),
2759
+ requestId: readStringOr(data["connectionId"]),
2322
2760
  url,
2323
2761
  payload
2324
2762
  });
@@ -2362,13 +2800,14 @@ Valid actions: ${valid}`);
2362
2800
  if (!entry || typeof entry !== "object") {
2363
2801
  continue;
2364
2802
  }
2365
- const event = String(entry["event"] ?? "");
2803
+ const record = entry;
2804
+ const event = readStringOr(record["event"]);
2366
2805
  if (event !== "ws.frame.received") {
2367
2806
  continue;
2368
2807
  }
2369
- const data = entry["data"] ?? {};
2370
- const payload = String(data["payload"] ?? "");
2371
- const url = String(data["url"] ?? "");
2808
+ const data = record["data"] ?? {};
2809
+ const payload = readStringOr(data["payload"]);
2810
+ const url = readStringOr(data["url"]);
2372
2811
  if (!regex.test(url) && !regex.test(payload)) {
2373
2812
  continue;
2374
2813
  }
@@ -2376,7 +2815,7 @@ Valid actions: ${valid}`);
2376
2815
  continue;
2377
2816
  }
2378
2817
  return {
2379
- requestId: String(data["connectionId"] ?? ""),
2818
+ requestId: readStringOr(data["connectionId"]),
2380
2819
  url,
2381
2820
  payload
2382
2821
  };
@@ -2397,13 +2836,11 @@ Valid actions: ${valid}`);
2397
2836
  return;
2398
2837
  }
2399
2838
  const args = Array.isArray(params["args"]) ? params["args"] : [];
2400
- errors.push(
2401
- args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
2402
- );
2839
+ errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
2403
2840
  };
2404
2841
  const onException = (params) => {
2405
2842
  const details = params["exceptionDetails"] ?? {};
2406
- errors.push(String(details["text"] ?? "Runtime exception"));
2843
+ errors.push(readString(details["text"]) ?? "Runtime exception");
2407
2844
  };
2408
2845
  const timer = setTimeout(() => {
2409
2846
  cleanup();
@@ -2775,6 +3212,30 @@ var ACTION_RULES = {
2775
3212
  kind: { type: "string", enum: ["audio", "video"] }
2776
3213
  },
2777
3214
  optional: {}
3215
+ },
3216
+ delta: {
3217
+ required: {},
3218
+ optional: {}
3219
+ },
3220
+ review: {
3221
+ required: {},
3222
+ optional: {}
3223
+ },
3224
+ chooseOption: {
3225
+ required: { value: { type: "string|string[]" } },
3226
+ optional: {
3227
+ trigger: { type: "string|string[]" },
3228
+ selector: { type: "string|string[]" },
3229
+ option: { type: "string|string[]" },
3230
+ match: { type: "string", enum: ["exact", "contains", "startsWith"] }
3231
+ }
3232
+ },
3233
+ upload: {
3234
+ required: {
3235
+ selector: { type: "string|string[]" },
3236
+ files: { type: "string|string[]" }
3237
+ },
3238
+ optional: {}
2778
3239
  }
2779
3240
  };
2780
3241
  var VALID_ACTIONS = Object.keys(ACTION_RULES);
@@ -2814,7 +3275,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
2814
3275
  "name",
2815
3276
  "state",
2816
3277
  "kind",
2817
- "windowMs"
3278
+ "windowMs",
3279
+ "expectAny",
3280
+ "expectAll",
3281
+ "failIf",
3282
+ "dangerous",
3283
+ "files"
2818
3284
  ]);
2819
3285
  function resolveAction(name) {
2820
3286
  if (VALID_ACTIONS.includes(name)) {
@@ -3034,6 +3500,64 @@ function validateSteps(steps) {
3034
3500
  });
3035
3501
  }
3036
3502
  }
3503
+ if ("dangerous" in obj && obj["dangerous"] !== void 0) {
3504
+ if (typeof obj["dangerous"] !== "boolean") {
3505
+ errors.push({
3506
+ stepIndex: i,
3507
+ field: "dangerous",
3508
+ message: `"dangerous" expected boolean, got ${typeof obj["dangerous"]}.`
3509
+ });
3510
+ }
3511
+ }
3512
+ for (const condField of ["expectAny", "expectAll", "failIf"]) {
3513
+ if (condField in obj && obj[condField] !== void 0) {
3514
+ if (!Array.isArray(obj[condField])) {
3515
+ errors.push({
3516
+ stepIndex: i,
3517
+ field: condField,
3518
+ message: `"${condField}" expected array, got ${typeof obj[condField]}.`
3519
+ });
3520
+ } else {
3521
+ const conditions = obj[condField];
3522
+ for (let ci = 0; ci < conditions.length; ci++) {
3523
+ const cond = conditions[ci];
3524
+ if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
3525
+ errors.push({
3526
+ stepIndex: i,
3527
+ field: condField,
3528
+ message: `"${condField}[${ci}]" must be a condition object.`
3529
+ });
3530
+ continue;
3531
+ }
3532
+ const condObj = cond;
3533
+ if (!("kind" in condObj) || typeof condObj["kind"] !== "string") {
3534
+ errors.push({
3535
+ stepIndex: i,
3536
+ field: condField,
3537
+ message: `"${condField}[${ci}]" missing required "kind" field.`
3538
+ });
3539
+ } else {
3540
+ const validKinds = [
3541
+ "urlMatches",
3542
+ "elementVisible",
3543
+ "elementHidden",
3544
+ "textAppears",
3545
+ "textChanges",
3546
+ "networkResponse",
3547
+ "stateSignatureChanges"
3548
+ ];
3549
+ if (!validKinds.includes(condObj["kind"])) {
3550
+ errors.push({
3551
+ stepIndex: i,
3552
+ field: condField,
3553
+ message: `"${condField}[${ci}].kind" must be one of: ${validKinds.join(", ")}. Got "${condObj["kind"]}".`
3554
+ });
3555
+ }
3556
+ }
3557
+ }
3558
+ }
3559
+ }
3560
+ }
3037
3561
  if (action === "assertText") {
3038
3562
  if (!("expect" in obj) && !("value" in obj)) {
3039
3563
  errors.push({
@@ -3110,6 +3634,14 @@ function validateSteps(steps) {
3110
3634
  }
3111
3635
 
3112
3636
  export {
3637
+ NetworkResponseTracker,
3638
+ captureStateSignature,
3639
+ evaluateCondition,
3640
+ evaluateOutcome,
3641
+ conditionAny,
3642
+ conditionAll,
3643
+ conditionNot,
3644
+ conditionRace,
3113
3645
  ActionabilityError,
3114
3646
  ensureActionable,
3115
3647
  generateHints,