@delacour/warden-e2e 0.2.0 → 0.4.0

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.
@@ -64,10 +64,17 @@ var READS = new Set([
64
64
  "previous",
65
65
  "peek"
66
66
  ]);
67
+ var WAITS = new Set(["settle"]);
67
68
  var READS_WITHOUT_ARGUMENT = new Set(["orientation", "appearance", "clipboard"]);
68
69
  function isCall(value) {
69
70
  return typeof value === "function";
70
71
  }
72
+ function reportsQuiet(result) {
73
+ if (typeof result !== "object" || result === null || !("success" in result) || result.success !== true)
74
+ return false;
75
+ const data = "data" in result ? result.data : undefined;
76
+ return typeof data === "object" && data !== null && "quiet" in data && data.quiet === true;
77
+ }
71
78
  function isThenable(value) {
72
79
  return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
73
80
  }
@@ -137,21 +144,31 @@ function cacheObservations(driver, options = {}) {
137
144
  return { success: true, data: reader.alertIn(taken.data) };
138
145
  };
139
146
  };
140
- const asInput = (fn, target) => {
147
+ const edged = (fn, target, edge) => {
141
148
  return (...args) => {
142
- inputEdge();
149
+ edge();
143
150
  let result;
144
151
  try {
145
152
  result = Reflect.apply(fn, target, args);
146
153
  } catch (error) {
147
- inputEdge();
154
+ edge();
148
155
  throw error;
149
156
  }
150
157
  if (!isThenable(result)) {
151
- inputEdge();
158
+ edge();
152
159
  return result;
153
160
  }
154
- return Promise.resolve(result).finally(inputEdge);
161
+ return Promise.resolve(result).finally(edge);
162
+ };
163
+ };
164
+ const asInput = (fn, target) => edged(fn, target, inputEdge);
165
+ const asWait = (fn, target) => {
166
+ const waited = edged(fn, target, moveOn);
167
+ return async (...args) => {
168
+ const result = await waited(...args);
169
+ if (reportsQuiet(result))
170
+ lastInputAt = undefined;
171
+ return result;
155
172
  };
156
173
  };
157
174
  const wrapMember = (name, value, target) => {
@@ -159,6 +176,8 @@ function cacheObservations(driver, options = {}) {
159
176
  return wrap(value, {});
160
177
  if (!isCall(value) || READS.has(name))
161
178
  return value;
179
+ if (WAITS.has(name))
180
+ return asWait(value, target);
162
181
  const input = asInput(value, target);
163
182
  if (!READS_WITHOUT_ARGUMENT.has(name))
164
183
  return input;
@@ -271,6 +290,9 @@ function canSelectAll(driver) {
271
290
  function canSetText(driver) {
272
291
  return typeof driver.setText === "function";
273
292
  }
293
+ function canPasteText(driver) {
294
+ return typeof driver.pasteText === "function";
295
+ }
274
296
  var segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
275
297
  function graphemesOf(text) {
276
298
  return Array.from(segmenter.segment(text), (part) => part.segment);
@@ -834,34 +856,34 @@ async function confirmFieldValue(request) {
834
856
  const { now, sleep } = request;
835
857
  const start = now();
836
858
  const cap = request.maxMs === undefined ? Number.POSITIVE_INFINITY : start + request.maxMs;
837
- let deadline = Math.min(start + request.budgetMs, cap);
838
- let held = "(gone)";
839
- let streak = 0;
859
+ const reads = request.reads ?? CONFIRM_READS;
860
+ let poll = { deadline: Math.min(start + request.budgetMs, cap), held: "(gone)", streak: 0 };
840
861
  for (;; ) {
841
862
  const read = await request.read();
842
863
  if (!read.ok)
843
864
  return { status: "failed", error: read.error };
844
- const { reading } = read;
845
- if (reading.kind === "unreadable") {
846
- streak = 0;
847
- held = reading.note;
848
- } else if (reading.value === request.text) {
849
- streak += 1;
850
- if (streak >= (request.reads ?? CONFIRM_READS))
851
- return { status: "confirmed" };
852
- held = reading.value;
853
- deadline = Math.max(deadline, Math.min(now() + 2 * FIELD_POLL_MS, cap));
854
- } else {
855
- streak = 0;
856
- if (reading.value !== held)
857
- deadline = Math.max(deadline, Math.min(now() + FIELD_PROGRESS_MS, cap));
858
- held = reading.value;
859
- }
860
- if (now() >= deadline)
861
- return { status: "mismatch", held };
865
+ poll = foldReading(poll, read.reading, { text: request.text, now: now(), cap, reads });
866
+ if (poll.streak > 0 && poll.streak >= reads)
867
+ return { status: "confirmed" };
868
+ if (now() >= poll.deadline)
869
+ return { status: "mismatch", held: poll.held };
862
870
  await sleep(FIELD_POLL_MS);
863
871
  }
864
872
  }
873
+ function foldReading(poll, reading, at) {
874
+ if (reading.kind === "unreadable")
875
+ return { ...poll, streak: 0, held: reading.note };
876
+ if (reading.value === at.text) {
877
+ const streak = poll.streak + 1;
878
+ if (streak >= at.reads)
879
+ return { ...poll, streak };
880
+ const deadline = Math.max(poll.deadline, Math.min(at.now + 2 * FIELD_POLL_MS, at.cap));
881
+ return { deadline, held: reading.value, streak };
882
+ }
883
+ const progressed = reading.value !== poll.held;
884
+ const deadline = progressed ? Math.max(poll.deadline, Math.min(at.now + FIELD_PROGRESS_MS, at.cap)) : poll.deadline;
885
+ return { deadline, held: reading.value, streak: 0 };
886
+ }
865
887
  async function hitTestField(driver, wanted) {
866
888
  const { x, y, width, height } = wanted.element.frame;
867
889
  const hit = await driver.elementAtPoint({ x: x + width / 2, y: y + height / 2 });
@@ -921,7 +943,7 @@ async function focusAndClear(driver, sleep, point, entry, method = "auto") {
921
943
  return prepared;
922
944
  return prepared.data.pending === "selection" ? pressKeys(driver, sleep, "Backspace", 1) : ok(undefined);
923
945
  }
924
- async function focusClearType(driver, sleep, point, entry, text, method = "auto", env = process.env) {
946
+ async function focusClearType(driver, sleep, point, entry, text, method = "auto", env = process.env, via = "paste") {
925
947
  const wantsSetText = method === "setText" || method === "auto" && setTextOptedIn(env);
926
948
  if (wantsSetText && entry !== undefined && fieldContent(entry).kind === "text" && canSetText(driver)) {
927
949
  const focused = await driver.tap(point);
@@ -930,15 +952,20 @@ async function focusClearType(driver, sleep, point, entry, text, method = "auto"
930
952
  const replaced = await driver.setText(text);
931
953
  if (replaced.success)
932
954
  return replaced;
933
- return typeOver(driver, sleep, await prepareFocused(driver, sleep, entry, method), text);
955
+ return typeOver(driver, sleep, await prepareFocused(driver, sleep, entry, method), text, "type");
934
956
  }
935
- return typeOver(driver, sleep, await focusAndSelect(driver, sleep, point, entry, method), text);
957
+ return typeOver(driver, sleep, await focusAndSelect(driver, sleep, point, entry, method), text, via);
936
958
  }
937
- async function typeOver(driver, sleep, prepared, text) {
959
+ async function typeOver(driver, sleep, prepared, text, via) {
938
960
  if (!prepared.success)
939
961
  return prepared;
940
962
  if (text === "")
941
963
  return prepared.data.pending === "selection" ? pressKeys(driver, sleep, "Backspace", 1) : ok(undefined);
964
+ if (via === "paste" && canPasteText(driver)) {
965
+ const pasted = await driver.pasteText(text);
966
+ if (pasted.success)
967
+ return pasted;
968
+ }
942
969
  return typeText(driver, sleep, text);
943
970
  }
944
971
 
@@ -1750,10 +1777,20 @@ class Locator {
1750
1777
  const reason = blockedReason(entry, spec, observed);
1751
1778
  if (reason !== undefined)
1752
1779
  return { done: false, state: { kind: "blocked", reason, snapshot: observed.snapshot } };
1753
- if (!anyFrame && !this.heldStill(entry, observation.earlier))
1754
- return "moving";
1780
+ if (!anyFrame && !this.heldStill(entry, observation.earlier)) {
1781
+ const compared = observation.earlier?.previous !== undefined;
1782
+ if (compared || !await this.stillByHitTest(entry, observed))
1783
+ return "moving";
1784
+ }
1755
1785
  return { done: true, value: { entry, observed } };
1756
1786
  }
1787
+ async stillByHitTest(entry, observed) {
1788
+ const { driver } = observed;
1789
+ if (driver.capabilities.cheapHitTest !== true || this.rt.host.answerSystemAlert !== undefined)
1790
+ return false;
1791
+ const hit = await driver.elementAtPoint(tapPointOf(entry));
1792
+ return hit.success && hit.data !== null && sameElement(hit.data, entry.element);
1793
+ }
1757
1794
  heldStill(entry, earlier) {
1758
1795
  if (earlier === undefined)
1759
1796
  return true;
@@ -1851,14 +1888,15 @@ class Locator {
1851
1888
  });
1852
1889
  }
1853
1890
  async fill(text, options = {}) {
1854
- assertOptions("fill", options, ["timeout"]);
1891
+ assertOptions("fill", options, ["via", "timeout"]);
1892
+ const via = fillViaOf(this, options.via);
1855
1893
  return this.act("fill", async () => {
1856
1894
  const budget = this.rt.budget(options.timeout);
1857
1895
  let target = await this.actionable({ enabled: true, tapPointOnScreen: false }, budget.remaining());
1858
1896
  let firstHeld;
1859
1897
  for (let attempt = 1;; attempt += 1) {
1860
1898
  const point = focusPoint(target.entry.element.frame);
1861
- this.rt.drive("fill", await focusClearType(target.observed.driver, this.pause, point, target.entry, text, clearMethodFor(attempt)));
1899
+ this.rt.drive("fill", await focusClearType(target.observed.driver, this.pause, point, target.entry, text, clearMethodFor(attempt), process.env, viaFor(attempt, via)));
1862
1900
  if (target.entry.secure)
1863
1901
  return;
1864
1902
  const outcome = await this.confirm(target, text, budget);
@@ -2067,6 +2105,14 @@ function retryPlan(options) {
2067
2105
  function describeUntil(until) {
2068
2106
  return typeof until === "function" ? "check" : String(until);
2069
2107
  }
2108
+ function fillViaOf(locator, via) {
2109
+ if (via === undefined || via === "paste" || via === "type")
2110
+ return via ?? "paste";
2111
+ throw new E2eError2("INVALID_ARGUMENT", `${locator}: fill via must be "paste" or "type", got ${JSON.stringify(via)}`);
2112
+ }
2113
+ function viaFor(attempt, via) {
2114
+ return attempt === 1 ? via : "type";
2115
+ }
2070
2116
  function clearMethodFor(attempt) {
2071
2117
  return attempt === 1 ? "auto" : "counted";
2072
2118
  }
@@ -2201,6 +2247,32 @@ function textOf2(data) {
2201
2247
  return new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
2202
2248
  return;
2203
2249
  }
2250
+ function taggedBindingPayload(params) {
2251
+ const call = bindingCalledSchema.safeParse(params);
2252
+ if (!call.success)
2253
+ return;
2254
+ let payload;
2255
+ try {
2256
+ payload = JSON.parse(call.data.payload);
2257
+ } catch {
2258
+ payload = undefined;
2259
+ }
2260
+ const tagged = bindingPayloadSchema.safeParse(payload);
2261
+ return tagged.success ? { requestId: tagged.data.requestId, payload } : undefined;
2262
+ }
2263
+ function parseEnvelope(data) {
2264
+ const text = textOf2(data);
2265
+ if (text === undefined)
2266
+ return;
2267
+ let json;
2268
+ try {
2269
+ json = JSON.parse(text);
2270
+ } catch {
2271
+ return;
2272
+ }
2273
+ const message = envelopeSchema.safeParse(json);
2274
+ return message.success ? message.data : undefined;
2275
+ }
2204
2276
  function inspectorOrigin(wsUrl) {
2205
2277
  if (!URL.canParse(wsUrl))
2206
2278
  return;
@@ -2261,18 +2333,9 @@ async function openCdp(wsUrl, options = {}) {
2261
2333
  };
2262
2334
  const dispatch = (method, params) => {
2263
2335
  if (method === "Runtime.bindingCalled") {
2264
- const call = bindingCalledSchema.safeParse(params);
2265
- if (call.success) {
2266
- let payload;
2267
- try {
2268
- payload = JSON.parse(call.data.payload);
2269
- } catch {
2270
- payload = undefined;
2271
- }
2272
- const tagged = bindingPayloadSchema.safeParse(payload);
2273
- if (tagged.success)
2274
- bindings.get(tagged.data.requestId)?.settle(ok(payload));
2275
- }
2336
+ const tagged = taggedBindingPayload(params);
2337
+ if (tagged)
2338
+ bindings.get(tagged.requestId)?.settle(ok(tagged.payload));
2276
2339
  }
2277
2340
  for (const handler of [...anyHandlers])
2278
2341
  handler(method, params);
@@ -2280,19 +2343,10 @@ async function openCdp(wsUrl, options = {}) {
2280
2343
  handler(params);
2281
2344
  };
2282
2345
  ws.addEventListener("message", (event) => {
2283
- const text = textOf2(event.data);
2284
- if (text === undefined)
2285
- return;
2286
- let json;
2287
- try {
2288
- json = JSON.parse(text);
2289
- } catch {
2290
- return;
2291
- }
2292
- const message = envelopeSchema.safeParse(json);
2293
- if (!message.success)
2346
+ const message = parseEnvelope(event.data);
2347
+ if (!message)
2294
2348
  return;
2295
- const { id, error, result, method, params } = message.data;
2349
+ const { id, error, result, method, params } = message;
2296
2350
  if (id !== undefined) {
2297
2351
  const request = pending.get(id);
2298
2352
  if (!request)
@@ -2305,7 +2359,7 @@ async function openCdp(wsUrl, options = {}) {
2305
2359
  if (method)
2306
2360
  dispatch(method, params);
2307
2361
  });
2308
- ws.addEventListener("close", () => {
2362
+ ws.addEventListener("close", (event) => {
2309
2363
  if (closed)
2310
2364
  return;
2311
2365
  closed = true;
@@ -2318,7 +2372,7 @@ async function openCdp(wsUrl, options = {}) {
2318
2372
  binding.settle(devtoolsErr("cdp_closed", "CDP connection closed"));
2319
2373
  if (!intentional)
2320
2374
  for (const handler of [...closeHandlers])
2321
- handler();
2375
+ handler({ code: event.code, reason: event.reason });
2322
2376
  });
2323
2377
  const subscribe = (set, handler) => {
2324
2378
  set.add(handler);
@@ -2419,7 +2473,8 @@ var stackTraceSchema = z2.looseObject({
2419
2473
  callFrames: z2.array(z2.looseObject({
2420
2474
  url: z2.string().optional(),
2421
2475
  lineNumber: z2.number().optional(),
2422
- columnNumber: z2.number().optional()
2476
+ columnNumber: z2.number().optional(),
2477
+ functionName: z2.string().optional()
2423
2478
  }))
2424
2479
  });
2425
2480
  var consoleCalledSchema = z2.looseObject({
@@ -2430,7 +2485,7 @@ var consoleCalledSchema = z2.looseObject({
2430
2485
  });
2431
2486
  var exceptionThrownSchema = z2.looseObject({
2432
2487
  timestamp: z2.number().optional(),
2433
- exceptionDetails: exceptionDetailsSchema
2488
+ exceptionDetails: exceptionDetailsSchema.extend({ stackTrace: stackTraceSchema.optional() })
2434
2489
  });
2435
2490
  var LEVELS = {
2436
2491
  log: "log",
@@ -2455,6 +2510,20 @@ function formatPreview(object) {
2455
2510
  const name = object.className && object.className !== "Object" ? `${object.className} ` : "";
2456
2511
  return `${name}{${body}${more}}`;
2457
2512
  }
2513
+ function formatObject(object) {
2514
+ if (object.subtype === "null")
2515
+ return "null";
2516
+ if (object.value !== undefined)
2517
+ return JSON.stringify(object.value);
2518
+ if (object.subtype === "error")
2519
+ return object.description ?? "Error";
2520
+ return formatPreview(object) ?? object.description ?? object.className ?? "Object";
2521
+ }
2522
+ function formatFunction(object) {
2523
+ const first = (object.description ?? "function").split(`
2524
+ `)[0] ?? "function";
2525
+ return first.length > MAX_FUNCTION_TEXT ? `${first.slice(0, MAX_FUNCTION_TEXT)}\u2026` : first;
2526
+ }
2458
2527
  function formatValue2(object) {
2459
2528
  switch (object.type) {
2460
2529
  case "string":
@@ -2465,24 +2534,18 @@ function formatValue2(object) {
2465
2534
  case "boolean":
2466
2535
  case "bigint":
2467
2536
  return object.unserializableValue ?? object.description ?? String(object.value);
2468
- case "function": {
2469
- const first = (object.description ?? "function").split(`
2470
- `)[0] ?? "function";
2471
- return first.length > MAX_FUNCTION_TEXT ? `${first.slice(0, MAX_FUNCTION_TEXT)}\u2026` : first;
2472
- }
2473
- case "object": {
2474
- if (object.subtype === "null")
2475
- return "null";
2476
- if (object.value !== undefined)
2477
- return JSON.stringify(object.value);
2478
- if (object.subtype === "error")
2479
- return object.description ?? "Error";
2480
- return formatPreview(object) ?? object.description ?? object.className ?? "Object";
2481
- }
2537
+ case "function":
2538
+ return formatFunction(object);
2539
+ case "object":
2540
+ return formatObject(object);
2482
2541
  default:
2483
2542
  return object.description ?? `[${object.type}]`;
2484
2543
  }
2485
2544
  }
2545
+ function formatNumeric(spec, arg) {
2546
+ const n = Number(arg.value ?? arg.description);
2547
+ return Number.isNaN(n) ? "NaN" : String(spec === "f" ? n : Math.trunc(n));
2548
+ }
2486
2549
  function applyFormat(format, rest) {
2487
2550
  let used = 0;
2488
2551
  const text = format.replace(/%([sdifoOjc%])/g, (whole, spec) => {
@@ -2494,10 +2557,8 @@ function applyFormat(format, rest) {
2494
2557
  used++;
2495
2558
  if (spec === "c")
2496
2559
  return "";
2497
- if (spec === "d" || spec === "i" || spec === "f") {
2498
- const n = Number(arg.value ?? arg.description);
2499
- return Number.isNaN(n) ? "NaN" : String(spec === "f" ? n : Math.trunc(n));
2500
- }
2560
+ if (spec === "d" || spec === "i" || spec === "f")
2561
+ return formatNumeric(spec, arg);
2501
2562
  return formatValue2(arg);
2502
2563
  });
2503
2564
  return { text, used };
@@ -2515,6 +2576,20 @@ function sourceOf(url, line, col) {
2515
2576
  return;
2516
2577
  return { url, line: (line ?? 0) + 1, col: (col ?? 0) + 1 };
2517
2578
  }
2579
+ function framesOf(trace) {
2580
+ const frames = [];
2581
+ for (const frame of trace?.callFrames ?? []) {
2582
+ if (!frame.url)
2583
+ continue;
2584
+ frames.push({
2585
+ file: frame.url,
2586
+ line: (frame.lineNumber ?? 0) + 1,
2587
+ col: (frame.columnNumber ?? 0) + 1,
2588
+ ...frame.functionName ? { fn: frame.functionName } : {}
2589
+ });
2590
+ }
2591
+ return frames.length > 0 ? frames : undefined;
2592
+ }
2518
2593
  var lineKey = (ts, text) => `${ts}\x00${text}`;
2519
2594
  function createConsoleCapture(options = {}) {
2520
2595
  const ring = options.ring ?? DEFAULT_RING;
@@ -2562,13 +2637,15 @@ function createConsoleCapture(options = {}) {
2562
2637
  const formatted = formatConsoleArgs(args);
2563
2638
  const frame = stackTrace?.callFrames[0];
2564
2639
  const source = sourceOf(frame?.url, frame?.lineNumber, frame?.columnNumber);
2640
+ const stack = framesOf(stackTrace);
2565
2641
  return {
2566
2642
  ts: timestamp ?? now(),
2567
2643
  level: LEVELS[type] ?? "log",
2568
2644
  kind: "console",
2569
2645
  text: formatted.text,
2570
2646
  args: formatted.args,
2571
- ...source ? { source } : {}
2647
+ ...source ? { source } : {},
2648
+ ...stack ? { stack } : {}
2572
2649
  };
2573
2650
  };
2574
2651
  const fromException = (params) => {
@@ -2577,12 +2654,14 @@ function createConsoleCapture(options = {}) {
2577
2654
  return;
2578
2655
  const { exceptionDetails: details, timestamp } = parsed.data;
2579
2656
  const source = sourceOf(details.url, details.lineNumber, details.columnNumber);
2657
+ const stack = framesOf(details.stackTrace);
2580
2658
  return {
2581
2659
  ts: timestamp ?? now(),
2582
2660
  level: "error",
2583
2661
  kind: "exception",
2584
2662
  text: describeException(details),
2585
- ...source ? { source } : {}
2663
+ ...source ? { source } : {},
2664
+ ...stack ? { stack } : {}
2586
2665
  };
2587
2666
  };
2588
2667
  return {
@@ -3019,7 +3098,8 @@ async function connectDevtools(o) {
3019
3098
  const cdpHandlers = new Map;
3020
3099
  const eventHandlers = {
3021
3100
  reconnected: new Set,
3022
- lost: new Set
3101
+ lost: new Set,
3102
+ yielded: new Set
3023
3103
  };
3024
3104
  let unsubscribeCdp = [];
3025
3105
  let connection = 0;
@@ -3069,60 +3149,78 @@ async function connectDevtools(o) {
3069
3149
  for (const handler of handlers)
3070
3150
  unsubscribeCdp.push(attached.cdp.on(method, handler));
3071
3151
  }
3072
- attached.cdp.onClose(() => {
3073
- if (state.kind === "up" && state.attached.cdp === attached.cdp)
3074
- reconnect("socket closed");
3152
+ attached.cdp.onClose((info) => {
3153
+ if (state.kind !== "up" || state.attached.cdp !== attached.cdp)
3154
+ return;
3155
+ if (o.yieldOnClose?.(info)) {
3156
+ state = { kind: "closed" };
3157
+ log(`devtools: yielded (${info.code} ${info.reason})`);
3158
+ for (const handler of [...eventHandlers.yielded])
3159
+ handler(info);
3160
+ return;
3161
+ }
3162
+ reconnect("socket closed");
3075
3163
  });
3076
3164
  attached.cdp.on("Runtime.executionContextsCleared", () => {
3077
3165
  if (state.kind === "up" && state.attached.cdp === attached.cdp)
3078
3166
  reconnect("JS context cleared");
3079
3167
  });
3080
3168
  }
3169
+ const closedError = () => devtoolsErr("cdp_closed", "devtools session is closed");
3170
+ function adopt(attached) {
3171
+ attach(attached);
3172
+ log(`devtools: reconnected to ${attached.target.id}`);
3173
+ for (const handler of [...eventHandlers.reconnected])
3174
+ handler(attached.target);
3175
+ }
3176
+ function adoptIfOpen(attached) {
3177
+ if (phase() === "closed") {
3178
+ attached.cdp.close();
3179
+ return closedError();
3180
+ }
3181
+ adopt(attached);
3182
+ return ok(attached);
3183
+ }
3184
+ function markLost(error) {
3185
+ if (phase() === "closed")
3186
+ return;
3187
+ state = { kind: "lost", error };
3188
+ for (const handler of [...eventHandlers.lost])
3189
+ handler(error);
3190
+ }
3191
+ async function reconnectLoop(previous) {
3192
+ previous.cdp.close();
3193
+ let last = { code: "devtools_unavailable", message: "reconnect failed" };
3194
+ const startedAt = now();
3195
+ let slept = 0;
3196
+ const leftMs = () => budgetMs - Math.max(now() - startedAt, slept);
3197
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
3198
+ if (phase() === "closed")
3199
+ return closedError();
3200
+ const result = await establish();
3201
+ if (result.success)
3202
+ return adoptIfOpen(result.data);
3203
+ last = result.error;
3204
+ const left = leftMs();
3205
+ const cap = Number.isFinite(maxAttempts) ? `/${maxAttempts}` : "";
3206
+ log(`devtools: reconnect attempt ${attempt}${cap} failed: ${last.message}`);
3207
+ if (attempt >= maxAttempts || left <= 0)
3208
+ break;
3209
+ const pause = Math.min(backoffAfter(attempt), left);
3210
+ slept += pause;
3211
+ await sleep(pause);
3212
+ }
3213
+ markLost(last);
3214
+ return { success: false, error: last };
3215
+ }
3081
3216
  function reconnect(reason) {
3082
3217
  if (state.kind === "reconnecting")
3083
3218
  return state.done;
3084
3219
  if (state.kind !== "up")
3085
- return Promise.resolve(devtoolsErr("cdp_closed", "devtools session is closed"));
3220
+ return Promise.resolve(closedError());
3086
3221
  const previous = state.attached;
3087
3222
  log(`devtools: ${reason}, reconnecting`);
3088
- const done = (async () => {
3089
- previous.cdp.close();
3090
- let last = { code: "devtools_unavailable", message: "reconnect failed" };
3091
- const startedAt = now();
3092
- let slept = 0;
3093
- const leftMs = () => budgetMs - Math.max(now() - startedAt, slept);
3094
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
3095
- if (phase() === "closed")
3096
- return devtoolsErr("cdp_closed", "devtools session is closed");
3097
- const result = await establish();
3098
- if (result.success) {
3099
- if (phase() === "closed") {
3100
- result.data.cdp.close();
3101
- return devtoolsErr("cdp_closed", "devtools session is closed");
3102
- }
3103
- attach(result.data);
3104
- log(`devtools: reconnected to ${result.data.target.id}`);
3105
- for (const handler of [...eventHandlers.reconnected])
3106
- handler(result.data.target);
3107
- return result;
3108
- }
3109
- last = result.error;
3110
- const left = leftMs();
3111
- const cap = Number.isFinite(maxAttempts) ? `/${maxAttempts}` : "";
3112
- log(`devtools: reconnect attempt ${attempt}${cap} failed: ${last.message}`);
3113
- if (attempt >= maxAttempts || left <= 0)
3114
- break;
3115
- const pause = Math.min(backoffAfter(attempt), left);
3116
- slept += pause;
3117
- await sleep(pause);
3118
- }
3119
- if (phase() !== "closed") {
3120
- state = { kind: "lost", error: last };
3121
- for (const handler of [...eventHandlers.lost])
3122
- handler(last);
3123
- }
3124
- return { success: false, error: last };
3125
- })();
3223
+ const done = reconnectLoop(previous);
3126
3224
  state = { kind: "reconnecting", done };
3127
3225
  epoch++;
3128
3226
  for (const wake of [...epochWaiters])
@@ -3136,16 +3234,13 @@ async function connectDevtools(o) {
3136
3234
  if (phase() === "closed") {
3137
3235
  if (result.success)
3138
3236
  result.data.cdp.close();
3139
- return devtoolsErr("cdp_closed", "devtools session is closed");
3237
+ return closedError();
3140
3238
  }
3141
3239
  if (!result.success) {
3142
3240
  state = { kind: "lost", error: result.error };
3143
3241
  return result;
3144
3242
  }
3145
- attach(result.data);
3146
- log(`devtools: reconnected to ${result.data.target.id}`);
3147
- for (const handler of [...eventHandlers.reconnected])
3148
- handler(result.data.target);
3243
+ adopt(result.data);
3149
3244
  return result;
3150
3245
  })();
3151
3246
  state = { kind: "reconnecting", done };
@@ -3160,6 +3255,44 @@ async function connectDevtools(o) {
3160
3255
  return revive();
3161
3256
  return devtoolsErr("cdp_closed", "devtools session is closed");
3162
3257
  }
3258
+ async function requestReload(method, baseUrl) {
3259
+ if (method !== "http") {
3260
+ log(`devtools: reload via ${method}`);
3261
+ return;
3262
+ }
3263
+ log("devtools: reload via http");
3264
+ try {
3265
+ const res = await o.fetch(`${baseUrl.replace(/\/+$/, "")}/reload`, { method: "POST" });
3266
+ if (!res.ok) {
3267
+ return devtoolsErr("devtools_unavailable", `reload failed: POST /reload answered ${res.status}`);
3268
+ }
3269
+ } catch (error) {
3270
+ return devtoolsErr("devtools_unavailable", `reload failed: POST /reload threw ${String(error)}`);
3271
+ }
3272
+ return;
3273
+ }
3274
+ async function awaitReload(method, startedAt, waitMs) {
3275
+ if (epoch === startedAt)
3276
+ await waitForEpochChange(waitMs);
3277
+ if (epoch === startedAt && state.kind === "up") {
3278
+ log(`devtools: reload via ${method} not observed after ${waitMs} ms`);
3279
+ return devtoolsErr("reload_failed", `reload via ${method} acknowledged but the JS context did not restart within ${waitMs} ms`);
3280
+ }
3281
+ if (state.kind !== "reconnecting")
3282
+ return;
3283
+ const back = await state.done;
3284
+ return back.success ? undefined : back;
3285
+ }
3286
+ function waitForEpochChange(waitMs) {
3287
+ return new Promise((resolve) => {
3288
+ const wake = () => {
3289
+ epochWaiters.delete(wake);
3290
+ resolve();
3291
+ };
3292
+ epochWaiters.add(wake);
3293
+ sleep(waitMs).then(wake);
3294
+ });
3295
+ }
3163
3296
  const first = await establish();
3164
3297
  if (!first.success)
3165
3298
  return first;
@@ -3197,39 +3330,8 @@ async function connectDevtools(o) {
3197
3330
  return devtoolsErr(probe.error.code, `reload probe failed: ${probe.error.message}`);
3198
3331
  }
3199
3332
  const method = readReloadMethod(probe.data) ?? "http";
3200
- if (method === "http") {
3201
- log("devtools: reload via http");
3202
- try {
3203
- const res = await o.fetch(`${metroUrl.replace(/\/+$/, "")}/reload`, { method: "POST" });
3204
- if (!res.ok) {
3205
- return devtoolsErr("devtools_unavailable", `reload failed: POST /reload answered ${res.status}`);
3206
- }
3207
- } catch (error) {
3208
- return devtoolsErr("devtools_unavailable", `reload failed: POST /reload threw ${String(error)}`);
3209
- }
3210
- } else {
3211
- log(`devtools: reload via ${method}`);
3212
- }
3213
- if (epoch === startedAt) {
3214
- await new Promise((resolve) => {
3215
- const wake = () => {
3216
- epochWaiters.delete(wake);
3217
- resolve();
3218
- };
3219
- epochWaiters.add(wake);
3220
- sleep(waitMs).then(wake);
3221
- });
3222
- }
3223
- if (epoch === startedAt && state.kind === "up") {
3224
- log(`devtools: reload via ${method} not observed after ${waitMs} ms`);
3225
- return devtoolsErr("reload_failed", `reload via ${method} acknowledged but the JS context did not restart within ${waitMs} ms`);
3226
- }
3227
- if (state.kind === "reconnecting") {
3228
- const back = await state.done;
3229
- if (!back.success)
3230
- return back;
3231
- }
3232
- return ok({ method });
3333
+ const failure = await requestReload(method, metroUrl) ?? await awaitReload(method, startedAt, waitMs);
3334
+ return failure ?? ok({ method });
3233
3335
  },
3234
3336
  get target() {
3235
3337
  if (state.kind === "up")
@@ -3909,6 +4011,25 @@ function spawnFailure(cmd, cwd, error) {
3909
4011
  return `cannot run \`${cmd.join(" ")}\`: no such directory ${cwd}`;
3910
4012
  return `cannot run \`${cmd.join(" ")}\`: ${error instanceof Error ? error.message : String(error)}`;
3911
4013
  }
4014
+ async function readLines(stream, onLine) {
4015
+ const decoder = new TextDecoder;
4016
+ let text = "";
4017
+ let pending = "";
4018
+ for await (const chunk of stream) {
4019
+ const piece = decoder.decode(chunk, { stream: true });
4020
+ text += piece;
4021
+ const lines = (pending + piece).split(/\r?\n/);
4022
+ pending = lines.pop() ?? "";
4023
+ for (const line of lines)
4024
+ onLine(line);
4025
+ }
4026
+ const rest = decoder.decode();
4027
+ text += rest;
4028
+ pending += rest;
4029
+ if (pending !== "")
4030
+ onLine(pending);
4031
+ return text;
4032
+ }
3912
4033
  var bunExec = async (cmd, opts = {}) => {
3913
4034
  try {
3914
4035
  const proc = Bun.spawn([...cmd], {
@@ -3920,11 +4041,9 @@ var bunExec = async (cmd, opts = {}) => {
3920
4041
  timeout: opts.timeoutMs,
3921
4042
  ...opts.signal !== undefined ? { signal: opts.signal } : {}
3922
4043
  });
3923
- const [stdout, stderr, exitCode] = await Promise.all([
3924
- new Response(proc.stdout).text(),
3925
- new Response(proc.stderr).text(),
3926
- proc.exited
3927
- ]);
4044
+ const { onLine } = opts;
4045
+ const read = (stream) => onLine ? readLines(stream, onLine) : new Response(stream).text();
4046
+ const [stdout, stderr, exitCode] = await Promise.all([read(proc.stdout), read(proc.stderr), proc.exited]);
3928
4047
  return { exitCode, stdout, stderr };
3929
4048
  } catch (error) {
3930
4049
  return { exitCode: 127, stdout: "", stderr: spawnFailure(cmd, opts.cwd, error) };
@@ -4158,6 +4277,8 @@ var frontmostDataSchema = z6.looseObject({
4158
4277
  pid: z6.number().optional(),
4159
4278
  frontmostPid: z6.number().optional()
4160
4279
  });
4280
+ var launchDataSchema = z6.looseObject({ pid: z6.number() });
4281
+ var terminateDataSchema = z6.looseObject({ terminated: z6.boolean() });
4161
4282
  var orientationDataSchema = z6.object({
4162
4283
  orientation: z6.enum([
4163
4284
  "portrait",
@@ -4179,7 +4300,9 @@ var opSchemas = {
4179
4300
  rebind: rebindDataSchema,
4180
4301
  alertButtons: alertButtonsDataSchema,
4181
4302
  frontmost: frontmostDataSchema,
4182
- orientation: orientationDataSchema
4303
+ orientation: orientationDataSchema,
4304
+ launch: launchDataSchema,
4305
+ terminate: terminateDataSchema
4183
4306
  };
4184
4307
  function hasOpSchema(op) {
4185
4308
  return Object.hasOwn(opSchemas, op);
@@ -5456,7 +5579,7 @@ function parseBudgets(raw) {
5456
5579
 
5457
5580
  // ../../packages/e2e/src/perf/device-perf.ts
5458
5581
  var PERF_DIR = "perf";
5459
- function parseEnvelope(stdout) {
5582
+ function parseEnvelope2(stdout) {
5460
5583
  try {
5461
5584
  const json = JSON.parse(stdout.trim().split(`
5462
5585
  `).at(-1) ?? "");
@@ -5509,7 +5632,7 @@ function createDevicePerf(deps) {
5509
5632
  if (deps.unavailable !== undefined)
5510
5633
  throw new E2eError2("SESSION", `device.perf unavailable: ${deps.unavailable}`);
5511
5634
  const { code, stdout, stderr } = await deps.exec(["perf", sub, "--json", "--udid", deps.udid, ...extra]);
5512
- const env = parseEnvelope(stdout);
5635
+ const env = parseEnvelope2(stdout);
5513
5636
  if (env === undefined || !env.ok) {
5514
5637
  const why = env === undefined ? stderr.trim() || `exit ${code}` : `${env.error.code}: ${env.error.message}`;
5515
5638
  throw new E2eError2("SESSION", `warden perf ${sub} failed: ${why}`);
@@ -5827,6 +5950,6 @@ function onlyWasSeen() {
5827
5950
 
5828
5951
  // ../../packages/e2e/src/version.ts
5829
5952
  var WORKSPACE_VERSION = "0.0.0-workspace";
5830
- var WARDEN_E2E_VERSION2 = "0.2.0";
5953
+ var WARDEN_E2E_VERSION2 = "0.4.0";
5831
5954
 
5832
5955
  export { E2eError2, noteSpanDetail, addSpanDetail, canObserve, canDetectAlerts, settleDetailOf, DEFAULT_SETTLE_QUIET_MS, DEFAULT_SETTLE_TIMEOUT_MS, settleByHash, snapshotHash, ok, err, swipeVector, screenRect, fieldValue, PINCH_MS, ROTATE_MS, PATH_MS, centreOf, pinchRadius, rotateRadius, requireGesture, assertScale, assertDegrees, isInteractiveRole, resolveRole, normalizeText, matchesText, indexSnapshot, testIdQuery, textQuery, roleQuery, selectorExpression, renderSnapshot, renderHeader, resolve, locateByTestIdViaReact, isLocator, Locator, pickOne, textOf, scrollUntilVisible, bunExec, execError, driverError, driverErr, logEventSchema, fatalEventSchema, responseSchema, mapWireError, encodeRequest, bunSpawn, stdioTransport, spawnSimd, connectSimd, isHidLost, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, parseBudgets, createDevicePerf, perfStartFor, spawnWarden, hash6, attemptDir, screenshotPath, specIdOf, realNativeLogDeps, captureNativeLog, callerFile, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };