browser-pilot 0.0.16 → 0.0.17

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