@delacour/warden-e2e 0.3.1 → 0.5.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.
Files changed (34) hide show
  1. package/{chunk-8em9kgsk.mjs → chunk-j9b17ade.mjs} +200 -37
  2. package/index.mjs +1 -1
  3. package/package.json +2 -2
  4. package/preload.mjs +61 -10
  5. package/types/core/src/affected/affected.schema.d.ts +185 -0
  6. package/types/core/src/batch/preset.schema.d.ts +59 -0
  7. package/types/core/src/builds/app-option.schema.d.ts +9 -1
  8. package/types/core/src/builds/builds.defaults.d.ts +62 -0
  9. package/types/core/src/builds/config.d.ts +122 -0
  10. package/types/core/src/builds/config.schema.d.ts +392 -0
  11. package/types/core/src/builds/define-config.d.ts +719 -0
  12. package/types/core/src/builds/kinds.d.ts +56 -0
  13. package/types/core/src/builds/native-ids.d.ts +14 -0
  14. package/types/core/src/builds/resolve.d.ts +26 -0
  15. package/types/core/src/duration.d.ts +5 -0
  16. package/types/core/src/exec.d.ts +2 -0
  17. package/types/core/src/ports.d.ts +76 -0
  18. package/types/core/src/providers/provider.types.d.ts +7 -0
  19. package/types/core/src/sims/slim.profile.d.ts +54 -0
  20. package/types/e2e/src/devtools/cdp.d.ts +7 -2
  21. package/types/e2e/src/devtools/devtools.types.d.ts +13 -0
  22. package/types/e2e/src/devtools/facet.d.ts +2 -0
  23. package/types/e2e/src/devtools/logbox.d.ts +27 -0
  24. package/types/e2e/src/devtools/logbox.script.d.ts +17 -0
  25. package/types/e2e/src/devtools/session.d.ts +8 -1
  26. package/types/e2e/src/driver/ios/ios-driver.d.ts +7 -6
  27. package/types/e2e/src/driver/ios/simd-protocol.d.ts +23 -0
  28. package/types/e2e/src/driver/ios/type-budget.d.ts +8 -4
  29. package/types/e2e/src/driver/observation-cache.d.ts +3 -1
  30. package/types/e2e/src/driver/typing.d.ts +10 -0
  31. package/types/e2e/src/preload.d.ts +5 -0
  32. package/types/e2e/src/screen/field.d.ts +11 -1
  33. package/types/e2e/src/screen/locator.d.ts +24 -5
  34. package/types/e2e/src/session.d.ts +4 -1
@@ -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);
@@ -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
  }
@@ -2313,7 +2359,7 @@ async function openCdp(wsUrl, options = {}) {
2313
2359
  if (method)
2314
2360
  dispatch(method, params);
2315
2361
  });
2316
- ws.addEventListener("close", () => {
2362
+ ws.addEventListener("close", (event) => {
2317
2363
  if (closed)
2318
2364
  return;
2319
2365
  closed = true;
@@ -2326,7 +2372,7 @@ async function openCdp(wsUrl, options = {}) {
2326
2372
  binding.settle(devtoolsErr("cdp_closed", "CDP connection closed"));
2327
2373
  if (!intentional)
2328
2374
  for (const handler of [...closeHandlers])
2329
- handler();
2375
+ handler({ code: event.code, reason: event.reason });
2330
2376
  });
2331
2377
  const subscribe = (set, handler) => {
2332
2378
  set.add(handler);
@@ -2427,7 +2473,8 @@ var stackTraceSchema = z2.looseObject({
2427
2473
  callFrames: z2.array(z2.looseObject({
2428
2474
  url: z2.string().optional(),
2429
2475
  lineNumber: z2.number().optional(),
2430
- columnNumber: z2.number().optional()
2476
+ columnNumber: z2.number().optional(),
2477
+ functionName: z2.string().optional()
2431
2478
  }))
2432
2479
  });
2433
2480
  var consoleCalledSchema = z2.looseObject({
@@ -2438,7 +2485,7 @@ var consoleCalledSchema = z2.looseObject({
2438
2485
  });
2439
2486
  var exceptionThrownSchema = z2.looseObject({
2440
2487
  timestamp: z2.number().optional(),
2441
- exceptionDetails: exceptionDetailsSchema
2488
+ exceptionDetails: exceptionDetailsSchema.extend({ stackTrace: stackTraceSchema.optional() })
2442
2489
  });
2443
2490
  var LEVELS = {
2444
2491
  log: "log",
@@ -2529,6 +2576,20 @@ function sourceOf(url, line, col) {
2529
2576
  return;
2530
2577
  return { url, line: (line ?? 0) + 1, col: (col ?? 0) + 1 };
2531
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
+ }
2532
2593
  var lineKey = (ts, text) => `${ts}\x00${text}`;
2533
2594
  function createConsoleCapture(options = {}) {
2534
2595
  const ring = options.ring ?? DEFAULT_RING;
@@ -2576,13 +2637,15 @@ function createConsoleCapture(options = {}) {
2576
2637
  const formatted = formatConsoleArgs(args);
2577
2638
  const frame = stackTrace?.callFrames[0];
2578
2639
  const source = sourceOf(frame?.url, frame?.lineNumber, frame?.columnNumber);
2640
+ const stack = framesOf(stackTrace);
2579
2641
  return {
2580
2642
  ts: timestamp ?? now(),
2581
2643
  level: LEVELS[type] ?? "log",
2582
2644
  kind: "console",
2583
2645
  text: formatted.text,
2584
2646
  args: formatted.args,
2585
- ...source ? { source } : {}
2647
+ ...source ? { source } : {},
2648
+ ...stack ? { stack } : {}
2586
2649
  };
2587
2650
  };
2588
2651
  const fromException = (params) => {
@@ -2591,12 +2654,14 @@ function createConsoleCapture(options = {}) {
2591
2654
  return;
2592
2655
  const { exceptionDetails: details, timestamp } = parsed.data;
2593
2656
  const source = sourceOf(details.url, details.lineNumber, details.columnNumber);
2657
+ const stack = framesOf(details.stackTrace);
2594
2658
  return {
2595
2659
  ts: timestamp ?? now(),
2596
2660
  level: "error",
2597
2661
  kind: "exception",
2598
2662
  text: describeException(details),
2599
- ...source ? { source } : {}
2663
+ ...source ? { source } : {},
2664
+ ...stack ? { stack } : {}
2600
2665
  };
2601
2666
  };
2602
2667
  return {
@@ -2620,6 +2685,71 @@ function createConsoleCapture(options = {}) {
2620
2685
  };
2621
2686
  }
2622
2687
 
2688
+ // ../../packages/e2e/src/devtools/logbox.script.ts
2689
+ var LOGBOX_SCRIPT = `function (g) {
2690
+ var req = g.__r;
2691
+ if (typeof req !== "function" || typeof req.getModules !== "function") return "no-require";
2692
+ var modules = req.getModules();
2693
+ if (!modules || typeof modules.forEach !== "function") return "no-require";
2694
+ var data = null;
2695
+ modules.forEach(function (mod) {
2696
+ if (data || !mod || typeof mod.verboseName !== "string" || !mod.isInitialized) return;
2697
+ var name = mod.verboseName.replace(/\\\\/g, "/");
2698
+ if (!/(^|\\/)react-native\\/Libraries\\/LogBox\\/Data\\/LogBoxData\\.js$/.test(name)) return;
2699
+ var exports = mod.publicModule && mod.publicModule.exports;
2700
+ if (exports && typeof exports.addIgnorePatterns === "function") data = exports;
2701
+ });
2702
+ if (!data) return "not-ready";
2703
+ data.addIgnorePatterns([/[\\s\\S]*/]);
2704
+ if (typeof data.setDisabled === "function") data.setDisabled(true);
2705
+ if (typeof data.setSelectedLog === "function") data.setSelectedLog(-1);
2706
+ return "off";
2707
+ }`;
2708
+ var LOGBOX_EXPRESSION = `(${LOGBOX_SCRIPT})(typeof globalThis !== "undefined" ? globalThis : (typeof global !== "undefined" ? global : this))`;
2709
+
2710
+ // ../../packages/e2e/src/devtools/logbox.ts
2711
+ var INTERVAL_MS = 250;
2712
+ var ATTEMPTS = 80;
2713
+ var timerSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2714
+ function muteLogBox(session, options = {}) {
2715
+ const sleep = options.sleep ?? timerSleep;
2716
+ const intervalMs = options.intervalMs ?? INTERVAL_MS;
2717
+ const attempts = options.attempts ?? ATTEMPTS;
2718
+ let generation = 0;
2719
+ let stopped = false;
2720
+ async function tryOnce() {
2721
+ const reply = await session.evaluate(LOGBOX_EXPRESSION);
2722
+ if (!reply.success)
2723
+ return reply.error.message;
2724
+ return reply.data === "off" ? undefined : String(reply.data);
2725
+ }
2726
+ async function apply(mine) {
2727
+ const current = () => !stopped && mine === generation;
2728
+ let last = "no reply";
2729
+ for (let attempt = 1;attempt <= attempts && current(); attempt++) {
2730
+ last = await tryOnce();
2731
+ if (last === undefined)
2732
+ return;
2733
+ if (attempt < attempts)
2734
+ await sleep(intervalMs);
2735
+ }
2736
+ if (current())
2737
+ options.log?.(`devtools: LogBox left on (${last} after ${attempts} attempts)`);
2738
+ }
2739
+ const start = () => apply(++generation);
2740
+ const off = session.on("reconnected", () => {
2741
+ start();
2742
+ });
2743
+ const settled = start();
2744
+ return {
2745
+ settled,
2746
+ stop() {
2747
+ stopped = true;
2748
+ off();
2749
+ }
2750
+ };
2751
+ }
2752
+
2623
2753
  // ../../packages/e2e/src/devtools/network.ts
2624
2754
  import { z as z3 } from "zod";
2625
2755
 
@@ -2760,10 +2890,10 @@ var pageEntrySchema = z3.looseObject({
2760
2890
  responseBodyTruncated: z3.boolean().optional(),
2761
2891
  error: z3.string().optional()
2762
2892
  });
2763
- var timerSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2893
+ var timerSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2764
2894
  async function attachNetwork(session, options = {}) {
2765
2895
  const ring = options.ring ?? DEFAULT_RING2;
2766
- const sleep = options.sleep ?? timerSleep;
2896
+ const sleep = options.sleep ?? timerSleep2;
2767
2897
  const pollMs = options.pollMs ?? DEFAULT_POLL_MS2;
2768
2898
  const installExpression = networkInstallExpression({
2769
2899
  ring,
@@ -3009,13 +3139,13 @@ var RECONNECT_BUDGET_MS = 30000;
3009
3139
  var RELOAD_WAIT_MS = 5000;
3010
3140
  var HANDSHAKE_OPTIONAL_TIMEOUT_MS = 3000;
3011
3141
  var RELOAD_EXPRESSION = '(function(){var g=globalThis;if(g.expo&&typeof g.expo.reloadAppAsync==="function"){setTimeout(function(){g.expo.reloadAppAsync("warden e2e reloadJs")},0);return "expo"}var m=g.nativeModuleProxy&&g.nativeModuleProxy.DevSettings;if(m&&typeof m.reload==="function"){setTimeout(function(){m.reload()},0);return "devsettings"}return false})()';
3012
- var timerSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3142
+ var timerSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3013
3143
  async function connectDevtools(o) {
3014
3144
  const { metroUrl } = o;
3015
3145
  if (!metroUrl) {
3016
3146
  return devtoolsErr("devtools_unavailable", "no Metro URL: release builds have no devtools (start a dev client)");
3017
3147
  }
3018
- const sleep = o.sleep ?? timerSleep2;
3148
+ const sleep = o.sleep ?? timerSleep3;
3019
3149
  const log = o.log ?? (() => {
3020
3150
  return;
3021
3151
  });
@@ -3033,7 +3163,8 @@ async function connectDevtools(o) {
3033
3163
  const cdpHandlers = new Map;
3034
3164
  const eventHandlers = {
3035
3165
  reconnected: new Set,
3036
- lost: new Set
3166
+ lost: new Set,
3167
+ yielded: new Set
3037
3168
  };
3038
3169
  let unsubscribeCdp = [];
3039
3170
  let connection = 0;
@@ -3083,9 +3214,17 @@ async function connectDevtools(o) {
3083
3214
  for (const handler of handlers)
3084
3215
  unsubscribeCdp.push(attached.cdp.on(method, handler));
3085
3216
  }
3086
- attached.cdp.onClose(() => {
3087
- if (state.kind === "up" && state.attached.cdp === attached.cdp)
3088
- reconnect("socket closed");
3217
+ attached.cdp.onClose((info) => {
3218
+ if (state.kind !== "up" || state.attached.cdp !== attached.cdp)
3219
+ return;
3220
+ if (o.yieldOnClose?.(info)) {
3221
+ state = { kind: "closed" };
3222
+ log(`devtools: yielded (${info.code} ${info.reason})`);
3223
+ for (const handler of [...eventHandlers.yielded])
3224
+ handler(info);
3225
+ return;
3226
+ }
3227
+ reconnect("socket closed");
3089
3228
  });
3090
3229
  attached.cdp.on("Runtime.executionContextsCleared", () => {
3091
3230
  if (state.kind === "up" && state.attached.cdp === attached.cdp)
@@ -3782,7 +3921,7 @@ var scriptResultSchema = z5.union([
3782
3921
  z5.object({ screen: z5.string().optional(), components: z5.array(componentSchema), truncated: z5.boolean() })
3783
3922
  ]);
3784
3923
  var bindingPayloadSchema2 = z5.object({ requestId: z5.string(), result: z5.unknown() });
3785
- var timerSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3924
+ var timerSleep4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3786
3925
  var requestCounter = 0;
3787
3926
  function scriptError(message) {
3788
3927
  if (message === "no-hook")
@@ -3845,7 +3984,7 @@ async function viaBinding(session, expression, requestId, timeoutMs) {
3845
3984
  }
3846
3985
  async function fetchTree(session, options = {}, deps = {}) {
3847
3986
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
3848
- const resolved = { sleep: deps.sleep ?? timerSleep3, now: deps.now ?? Date.now };
3987
+ const resolved = { sleep: deps.sleep ?? timerSleep4, now: deps.now ?? Date.now };
3849
3988
  requestCounter += 1;
3850
3989
  const requestId = `tree-${Date.now().toString(36)}-${requestCounter}`;
3851
3990
  const config = {
@@ -3896,6 +4035,7 @@ async function openDevtoolsFacet(o) {
3896
4035
  return attached;
3897
4036
  }
3898
4037
  const network = attached.data;
4038
+ const logbox = o.muteLogBox ? muteLogBox(session, { log: o.log }) : undefined;
3899
4039
  return ok({
3900
4040
  evaluate: (expression, options) => session.evaluate(expression, options),
3901
4041
  reloadJs: () => session.reloadJs(),
@@ -3921,6 +4061,7 @@ async function openDevtoolsFacet(o) {
3921
4061
  return ok(paths);
3922
4062
  },
3923
4063
  close() {
4064
+ logbox?.stop();
3924
4065
  network.stop();
3925
4066
  session.close();
3926
4067
  }
@@ -3937,6 +4078,25 @@ function spawnFailure(cmd, cwd, error) {
3937
4078
  return `cannot run \`${cmd.join(" ")}\`: no such directory ${cwd}`;
3938
4079
  return `cannot run \`${cmd.join(" ")}\`: ${error instanceof Error ? error.message : String(error)}`;
3939
4080
  }
4081
+ async function readLines(stream, onLine) {
4082
+ const decoder = new TextDecoder;
4083
+ let text = "";
4084
+ let pending = "";
4085
+ for await (const chunk of stream) {
4086
+ const piece = decoder.decode(chunk, { stream: true });
4087
+ text += piece;
4088
+ const lines = (pending + piece).split(/\r?\n/);
4089
+ pending = lines.pop() ?? "";
4090
+ for (const line of lines)
4091
+ onLine(line);
4092
+ }
4093
+ const rest = decoder.decode();
4094
+ text += rest;
4095
+ pending += rest;
4096
+ if (pending !== "")
4097
+ onLine(pending);
4098
+ return text;
4099
+ }
3940
4100
  var bunExec = async (cmd, opts = {}) => {
3941
4101
  try {
3942
4102
  const proc = Bun.spawn([...cmd], {
@@ -3948,11 +4108,9 @@ var bunExec = async (cmd, opts = {}) => {
3948
4108
  timeout: opts.timeoutMs,
3949
4109
  ...opts.signal !== undefined ? { signal: opts.signal } : {}
3950
4110
  });
3951
- const [stdout, stderr, exitCode] = await Promise.all([
3952
- new Response(proc.stdout).text(),
3953
- new Response(proc.stderr).text(),
3954
- proc.exited
3955
- ]);
4111
+ const { onLine } = opts;
4112
+ const read = (stream) => onLine ? readLines(stream, onLine) : new Response(stream).text();
4113
+ const [stdout, stderr, exitCode] = await Promise.all([read(proc.stdout), read(proc.stderr), proc.exited]);
3956
4114
  return { exitCode, stdout, stderr };
3957
4115
  } catch (error) {
3958
4116
  return { exitCode: 127, stdout: "", stderr: spawnFailure(cmd, opts.cwd, error) };
@@ -4186,6 +4344,8 @@ var frontmostDataSchema = z6.looseObject({
4186
4344
  pid: z6.number().optional(),
4187
4345
  frontmostPid: z6.number().optional()
4188
4346
  });
4347
+ var launchDataSchema = z6.looseObject({ pid: z6.number() });
4348
+ var terminateDataSchema = z6.looseObject({ terminated: z6.boolean() });
4189
4349
  var orientationDataSchema = z6.object({
4190
4350
  orientation: z6.enum([
4191
4351
  "portrait",
@@ -4207,7 +4367,9 @@ var opSchemas = {
4207
4367
  rebind: rebindDataSchema,
4208
4368
  alertButtons: alertButtonsDataSchema,
4209
4369
  frontmost: frontmostDataSchema,
4210
- orientation: orientationDataSchema
4370
+ orientation: orientationDataSchema,
4371
+ launch: launchDataSchema,
4372
+ terminate: terminateDataSchema
4211
4373
  };
4212
4374
  function hasOpSchema(op) {
4213
4375
  return Object.hasOwn(opSchemas, op);
@@ -5108,7 +5270,8 @@ var openDevtools = (env) => openDevtoolsFacet({
5108
5270
  WebSocket,
5109
5271
  timeoutMs: 5000,
5110
5272
  log: (line) => process.stderr.write(`[devtools] ${line}
5111
- `)
5273
+ `),
5274
+ muteLogBox: true
5112
5275
  });
5113
5276
  var SESSION_KEY = Symbol.for("@delacour/warden-e2e/session");
5114
5277
  var slot = () => globalThis;
@@ -5855,6 +6018,6 @@ function onlyWasSeen() {
5855
6018
 
5856
6019
  // ../../packages/e2e/src/version.ts
5857
6020
  var WORKSPACE_VERSION = "0.0.0-workspace";
5858
- var WARDEN_E2E_VERSION2 = "0.3.1";
6021
+ var WARDEN_E2E_VERSION2 = "0.5.0";
5859
6022
 
5860
6023
  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 };
package/index.mjs CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  registrationDefaults,
32
32
  markOnly,
33
33
  WARDEN_E2E_VERSION2
34
- } from "./chunk-8em9kgsk.mjs";
34
+ } from "./chunk-j9b17ade.mjs";
35
35
  // ../../packages/e2e/src/expect/expect.ts
36
36
  import { expect as bunExpect } from "bun:test";
37
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delacour/warden-e2e",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript spec API for `warden e2e`: test, expect, screen, device, app",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,7 +37,7 @@
37
37
  "LICENSE"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@delacour/warden": "0.3.1"
40
+ "@delacour/warden": "0.5.0"
41
41
  },
42
42
  "dependencies": {
43
43
  "zod": "^4.4.3"
package/preload.mjs CHANGED
@@ -68,7 +68,7 @@ import {
68
68
  onlyWasSeen,
69
69
  WORKSPACE_VERSION,
70
70
  WARDEN_E2E_VERSION2
71
- } from "./chunk-8em9kgsk.mjs";
71
+ } from "./chunk-j9b17ade.mjs";
72
72
 
73
73
  // ../../packages/e2e/src/preload.ts
74
74
  import { afterAll, setDefaultTimeout } from "bun:test";
@@ -3238,8 +3238,8 @@ function createSimctl(deps) {
3238
3238
 
3239
3239
  // ../../packages/e2e/src/driver/ios/type-budget.ts
3240
3240
  var TYPE_BASE_MS = 1e4;
3241
- var TYPE_PER_CHAR_MS = 70;
3242
- var TYPE_PER_REPEAT_MS = 300;
3241
+ var TYPE_PER_CHAR_MS = 100;
3242
+ var TYPE_PER_REPEAT_MS = 200;
3243
3243
  function typeTextBudgetMs(text) {
3244
3244
  const characters = Array.from(text);
3245
3245
  let repeats = 0;
@@ -3257,6 +3257,8 @@ var SETTLE_MARGIN_MS = 2000;
3257
3257
  var REBIND_TIMEOUT_MS = 3000;
3258
3258
  var GESTURE_MARGIN_MS = 5000;
3259
3259
  var FRONTMOST_TIMEOUT_MS = 3000;
3260
+ var LAUNCH_TIMEOUT_MS = 30000;
3261
+ var APP_OP_TIMEOUT_MS = 15000;
3260
3262
  var KEY_USAGE = {
3261
3263
  Enter: 40,
3262
3264
  Escape: 41,
@@ -3348,6 +3350,9 @@ function restOf(error, text) {
3348
3350
  function decodeBase64(png) {
3349
3351
  return new Uint8Array(Buffer.from(png, "base64"));
3350
3352
  }
3353
+ function predatesOp(error) {
3354
+ return error.code === "E_UNSUPPORTED" || error.code === "E_BAD_REQUEST" && /unknown (input )?op/.test(error.message);
3355
+ }
3351
3356
  function createIosDriver(swap, simctl, opts) {
3352
3357
  const { udid, now, sleep } = opts;
3353
3358
  const { client } = swap;
@@ -3359,6 +3364,7 @@ function createIosDriver(swap, simctl, opts) {
3359
3364
  let settleUnsupported = !nativeSettle;
3360
3365
  let rebindUnsupported = false;
3361
3366
  let gestureUnsupported = false;
3367
+ let appOpsOutdated = false;
3362
3368
  const capabilities = {
3363
3369
  pinch: true,
3364
3370
  maxPointers: 2,
@@ -3426,6 +3432,30 @@ function createIosDriver(swap, simctl, opts) {
3426
3432
  }
3427
3433
  return result;
3428
3434
  };
3435
+ async function appOp(op, p, timeoutMs) {
3436
+ if (appOpsOutdated) {
3437
+ noteSpanDetail("via", "simctl");
3438
+ return;
3439
+ }
3440
+ const result = await request(op, p, { timeoutMs });
3441
+ if (!result.success && predatesOp(result.error)) {
3442
+ appOpsOutdated = true;
3443
+ noteSpanDetail("via", "simctl");
3444
+ noteSpanDetail("reason", "helper_outdated");
3445
+ opts.log?.(`[ios] this warden-simd predates the \`${op}\` op (${result.error.code}: ${result.error.message}); app lifecycle goes through simctl until the helper is restarted (warden ui close --udid ${udid}) or rebuilt (bun run --cwd packages/e2e native:build)`);
3446
+ return;
3447
+ }
3448
+ noteSpanDetail("via", "simd");
3449
+ return result;
3450
+ }
3451
+ async function appOutcome(viaHelper, fallback, note) {
3452
+ if (viaHelper === undefined)
3453
+ return fallback();
3454
+ if (!viaHelper.success)
3455
+ return viaHelper;
3456
+ note(viaHelper.data);
3457
+ return okVoid;
3458
+ }
3429
3459
  const fetchSnapshot = async (o) => {
3430
3460
  const result = await request("snapshot", params({ interactiveOnly: o?.interactiveOnly, backend: o?.backend }));
3431
3461
  if (!result.success)
@@ -3463,11 +3493,17 @@ function createIosDriver(swap, simctl, opts) {
3463
3493
  const rest = restOf(typed.error, text);
3464
3494
  if (rest === undefined)
3465
3495
  return typed;
3466
- const copied = await simctl.pbcopy(rest);
3467
- if (!copied.success)
3468
- return copied;
3469
3496
  opts.log?.(`[ios] typeText: pasting ${rest.length} unmappable character(s)`);
3470
- return sendVoid("paste");
3497
+ return pasteText(rest);
3498
+ }
3499
+ async function pasteText(text) {
3500
+ if (text === "")
3501
+ return ok(undefined);
3502
+ const copied = await simctl.pbcopy(text);
3503
+ const result = copied.success ? await sendVoid("paste") : copied;
3504
+ if (!result.success)
3505
+ opts.log?.(`[ios] pasteText failed (${result.error.code}): ${result.error.message}`);
3506
+ return result;
3471
3507
  }
3472
3508
  async function hashSettle(o) {
3473
3509
  try {
@@ -3631,6 +3667,7 @@ function createIosDriver(swap, simctl, opts) {
3631
3667
  typeText,
3632
3668
  key: (key, modifiers) => sendVoid("key", params({ usage: KEY_USAGE[key], modifiers: modifiers && [...modifiers] })),
3633
3669
  selectAll: () => sendVoid("selectAll"),
3670
+ pasteText,
3634
3671
  pressButton,
3635
3672
  screenshot,
3636
3673
  orientation,
@@ -3647,7 +3684,8 @@ function createIosDriver(swap, simctl, opts) {
3647
3684
  },
3648
3685
  app: {
3649
3686
  launch: async (bundleId, o) => {
3650
- const launched = await simctl.launch(bundleId, o);
3687
+ const viaHelper = await appOp("launch", params({ bundleId, args: o?.args && [...o.args], env: o?.env, terminateRunning: o?.relaunch !== false }), LAUNCH_TIMEOUT_MS);
3688
+ const launched = await appOutcome(viaHelper, () => simctl.launch(bundleId, o), (data) => noteSpanDetail("pid", data.pid));
3651
3689
  if (launched.success) {
3652
3690
  launchedBundle = bundleId;
3653
3691
  if (await rebind(bundleId))
@@ -3656,12 +3694,18 @@ function createIosDriver(swap, simctl, opts) {
3656
3694
  return launched;
3657
3695
  },
3658
3696
  terminate: async (bundleId) => {
3659
- const terminated = await simctl.terminate(bundleId);
3697
+ const viaHelper = await appOp("terminate", { bundleId }, APP_OP_TIMEOUT_MS);
3698
+ const terminated = await appOutcome(viaHelper, () => simctl.terminate(bundleId), (data) => noteSpanDetail("terminated", data.terminated));
3660
3699
  if (terminated.success && launchedBundle === bundleId)
3661
3700
  launchedBundle = undefined;
3662
3701
  return terminated;
3663
3702
  },
3664
- openUrl: (url) => simctl.openUrl(url),
3703
+ openUrl: async (url) => {
3704
+ const viaHelper = await appOp("openUrl", { url }, APP_OP_TIMEOUT_MS);
3705
+ return appOutcome(viaHelper, () => simctl.openUrl(url), () => {
3706
+ return;
3707
+ });
3708
+ },
3665
3709
  install: (path) => simctl.install(path),
3666
3710
  uninstall: (bundleId) => simctl.uninstall(bundleId),
3667
3711
  isInstalled: (bundleId) => simctl.isInstalled(bundleId),
@@ -3939,6 +3983,8 @@ function createApp({ session, driver, env, log = stderrLog2, clock = systemClock
3939
3983
  if (options.env !== undefined)
3940
3984
  opts.env = options.env;
3941
3985
  await unwrap(() => driver.app.launch(bundleId, opts), `launch ${bundleId}`, session.signal);
3986
+ if (env.metroUrl !== undefined)
3987
+ session.devtools?.();
3942
3988
  if (options.url !== undefined) {
3943
3989
  const url = options.url;
3944
3990
  await unwrap(() => driver.app.openUrl(url), `open ${url}`, session.signal);
@@ -4773,6 +4819,10 @@ ${read.error}
4773
4819
  }
4774
4820
  });
4775
4821
  }
4822
+ function scrubRunnerEnv(env) {
4823
+ delete env.BUN_BE_BUN;
4824
+ }
4825
+ scrubRunnerEnv(process.env);
4776
4826
  if (!hasDefaultDriverFactory()) {
4777
4827
  setDefaultDriverFactory(defaultDriverFactory);
4778
4828
  setDefaultNativeLog(realNativeLogDeps(bunExec, androidTools(process.env).adb));
@@ -4796,5 +4846,6 @@ export {
4796
4846
  BUN_TIMEOUT_MARGIN_MS2 as BUN_TIMEOUT_MARGIN_MS,
4797
4847
  installPreload,
4798
4848
  isCi,
4849
+ scrubRunnerEnv,
4799
4850
  versionMismatch
4800
4851
  };