@delacour/warden-e2e 0.6.0 → 0.8.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 (151) hide show
  1. package/{chunk-yb1hcjqm.mjs → chunk-vs5npy59.mjs} +3370 -218
  2. package/index.mjs +247 -16
  3. package/package.json +2 -2
  4. package/preload.mjs +8264 -2387
  5. package/types/core/src/a11y/a11y.config.d.ts +47 -0
  6. package/types/core/src/affected/affected.schema.d.ts +142 -1
  7. package/types/core/src/allocate.d.ts +2 -1
  8. package/types/core/src/android-keyguard.d.ts +36 -0
  9. package/types/core/src/batch/preset.schema.d.ts +13 -1
  10. package/types/core/src/biometrics.d.ts +15 -0
  11. package/types/core/src/builds/app-option.schema.d.ts +1 -1
  12. package/types/core/src/builds/builds.defaults.d.ts +15 -0
  13. package/types/core/src/builds/config.d.ts +5 -1
  14. package/types/core/src/builds/config.schema.d.ts +275 -2
  15. package/types/core/src/builds/define-config.d.ts +159 -0
  16. package/types/core/src/builds/install-device.d.ts +37 -0
  17. package/types/core/src/builds/install.d.ts +17 -3
  18. package/types/core/src/builds/kinds.d.ts +21 -3
  19. package/types/core/src/builds/lock.d.ts +38 -0
  20. package/types/core/src/builds/resolve.d.ts +12 -2
  21. package/types/core/src/camera-mode.d.ts +17 -0
  22. package/types/core/src/camera-placeholder.d.ts +9 -0
  23. package/types/core/src/camera-profile.d.ts +12 -0
  24. package/types/core/src/claim-errors.d.ts +34 -0
  25. package/types/core/src/conditions/a11y-keys.d.ts +45 -0
  26. package/types/core/src/conditions/camera-media.d.ts +30 -0
  27. package/types/core/src/conditions/conditions.config.d.ts +141 -0
  28. package/types/core/src/conditions/conditions.d.ts +22 -0
  29. package/types/core/src/conditions/conditions.types.d.ts +171 -0
  30. package/types/core/src/conditions/inject-state.d.ts +13 -0
  31. package/types/core/src/conditions/launch-settings.d.ts +14 -0
  32. package/types/core/src/conditions/network.presets.d.ts +42 -0
  33. package/types/core/src/conditions/text-size.d.ts +19 -0
  34. package/types/core/src/golden/android-biometrics.d.ts +35 -0
  35. package/types/core/src/ios-runner/build.d.ts +28 -0
  36. package/types/core/src/ios-runner/cache.d.ts +44 -0
  37. package/types/core/src/ios-runner/classify.d.ts +7 -0
  38. package/types/core/src/ios-runner/pack.d.ts +17 -0
  39. package/types/core/src/ios-runner/runner-build.types.d.ts +76 -0
  40. package/types/core/src/ios-runner/signing.d.ts +40 -0
  41. package/types/core/src/ios-runner/sim-build.d.ts +36 -0
  42. package/types/core/src/ios-runner/stage.d.ts +26 -0
  43. package/types/core/src/lease-events.d.ts +126 -0
  44. package/types/core/src/netproxy/control.d.ts +75 -0
  45. package/types/core/src/netproxy/decide.d.ts +22 -0
  46. package/types/core/src/netproxy/handle-file.d.ts +12 -0
  47. package/types/core/src/netproxy/lease-proxy.d.ts +53 -0
  48. package/types/core/src/netproxy/netproxy.main.d.ts +40 -0
  49. package/types/core/src/netproxy/pipe.d.ts +37 -0
  50. package/types/core/src/netproxy/proxy.d.ts +48 -0
  51. package/types/core/src/netproxy/shaping.d.ts +43 -0
  52. package/types/core/src/ownership.d.ts +44 -0
  53. package/types/core/src/providers/android.d.ts +46 -2
  54. package/types/core/src/providers/provider.types.d.ts +9 -0
  55. package/types/core/src/queue.d.ts +43 -0
  56. package/types/core/src/store.d.ts +61 -5
  57. package/types/core/src/types.d.ts +57 -0
  58. package/types/core/src/xcode.d.ts +22 -0
  59. package/types/e2e/src/a11y/a11y-target.d.ts +23 -0
  60. package/types/e2e/src/a11y/a11y.types.d.ts +81 -0
  61. package/types/e2e/src/a11y/announcements.d.ts +24 -0
  62. package/types/e2e/src/a11y/audit.d.ts +22 -0
  63. package/types/e2e/src/a11y/native/atf.d.ts +51 -0
  64. package/types/e2e/src/a11y/native/xcuitest.d.ts +60 -0
  65. package/types/e2e/src/a11y/order.d.ts +5 -0
  66. package/types/e2e/src/a11y/rules/duplicate-name.d.ts +6 -0
  67. package/types/e2e/src/a11y/rules/hidden-focusable.d.ts +3 -0
  68. package/types/e2e/src/a11y/rules/image-no-name.d.ts +6 -0
  69. package/types/e2e/src/a11y/rules/label-in-name.d.ts +7 -0
  70. package/types/e2e/src/a11y/rules/missing-name.d.ts +3 -0
  71. package/types/e2e/src/a11y/rules/role-in-name.d.ts +3 -0
  72. package/types/e2e/src/a11y/rules/rule.context.d.ts +22 -0
  73. package/types/e2e/src/a11y/rules/target-size.d.ts +21 -0
  74. package/types/e2e/src/a11y/rules/text-clipped.d.ts +9 -0
  75. package/types/e2e/src/a11y/speech.d.ts +26 -0
  76. package/types/e2e/src/a11y/talkback-log.d.ts +13 -0
  77. package/types/e2e/src/alert-buttons.d.ts +9 -0
  78. package/types/e2e/src/app.d.ts +3 -2
  79. package/types/e2e/src/conditions/condition-events.d.ts +68 -0
  80. package/types/e2e/src/conditions/device-conditions.d.ts +198 -0
  81. package/types/e2e/src/conditions/geo.d.ts +15 -0
  82. package/types/e2e/src/conditions/gpx.d.ts +8 -0
  83. package/types/e2e/src/conditions/lease-recorder.d.ts +27 -0
  84. package/types/e2e/src/conditions/lease-store.d.ts +9 -0
  85. package/types/e2e/src/conditions/location-plan.d.ts +17 -0
  86. package/types/e2e/src/conditions/location-scenarios.data.d.ts +17 -0
  87. package/types/e2e/src/conditions/route-ticker.d.ts +53 -0
  88. package/types/e2e/src/conditions/scheduler.d.ts +83 -0
  89. package/types/e2e/src/device-a11y.d.ts +97 -0
  90. package/types/e2e/src/device.d.ts +67 -4
  91. package/types/e2e/src/driver/a11y-events.d.ts +30 -0
  92. package/types/e2e/src/driver/a11y-read.d.ts +12 -0
  93. package/types/e2e/src/driver/android/android-animations.d.ts +7 -0
  94. package/types/e2e/src/driver/android/android-app.d.ts +4 -0
  95. package/types/e2e/src/driver/android/android-argv.d.ts +21 -0
  96. package/types/e2e/src/driver/android/android-camera-host.d.ts +23 -0
  97. package/types/e2e/src/driver/android/android-camera.d.ts +28 -0
  98. package/types/e2e/src/driver/android/android-clock.d.ts +28 -0
  99. package/types/e2e/src/driver/android/android-device.d.ts +8 -0
  100. package/types/e2e/src/driver/android/android-driver.d.ts +22 -0
  101. package/types/e2e/src/driver/android/android-network.d.ts +66 -0
  102. package/types/e2e/src/driver/android/android-phone.d.ts +47 -0
  103. package/types/e2e/src/driver/android/android-screen-reader.d.ts +23 -0
  104. package/types/e2e/src/driver/android/android-settings.d.ts +41 -0
  105. package/types/e2e/src/driver/android/android-spoken.d.ts +18 -0
  106. package/types/e2e/src/driver/android/android-state.d.ts +10 -0
  107. package/types/e2e/src/driver/android/android-talkback.d.ts +57 -0
  108. package/types/e2e/src/driver/android/atf-device.d.ts +23 -0
  109. package/types/e2e/src/driver/android/atf-dex.generated.d.ts +7 -0
  110. package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +5 -5
  111. package/types/e2e/src/driver/android/uiad-ops.d.ts +27 -1
  112. package/types/e2e/src/driver/android/uiad-protocol.d.ts +8 -0
  113. package/types/e2e/src/driver/android/uiad-screen-reader.d.ts +33 -0
  114. package/types/e2e/src/driver/clock-recorder.d.ts +9 -0
  115. package/types/e2e/src/driver/device-settings.d.ts +10 -0
  116. package/types/e2e/src/driver/driver.types.d.ts +242 -1
  117. package/types/e2e/src/driver/element-actions.d.ts +16 -0
  118. package/types/e2e/src/driver/ios/device/capabilities.d.ts +3 -0
  119. package/types/e2e/src/driver/ios/device/device-driver.d.ts +49 -0
  120. package/types/e2e/src/driver/ios/device/devicectl.d.ts +38 -0
  121. package/types/e2e/src/driver/ios/device/runner-protocol.d.ts +21 -0
  122. package/types/e2e/src/driver/ios/hid-recovery.d.ts +15 -0
  123. package/types/e2e/src/driver/ios/inject.d.ts +124 -0
  124. package/types/e2e/src/driver/ios/ios-a11y.d.ts +73 -0
  125. package/types/e2e/src/driver/ios/ios-audit.d.ts +36 -0
  126. package/types/e2e/src/driver/ios/ios-driver.d.ts +50 -1
  127. package/types/e2e/src/driver/ios/ios-network.d.ts +51 -0
  128. package/types/e2e/src/driver/ios/ios-settings.d.ts +44 -0
  129. package/types/e2e/src/driver/ios/sim-runner.d.ts +41 -0
  130. package/types/e2e/src/driver/ios/simctl.d.ts +41 -1
  131. package/types/e2e/src/driver/ios/simd-transport.d.ts +11 -2
  132. package/types/e2e/src/driver/ios/type-budget.d.ts +6 -1
  133. package/types/e2e/src/driver/native-audit.d.ts +26 -0
  134. package/types/e2e/src/driver/network-proxy.d.ts +52 -0
  135. package/types/e2e/src/driver/open.d.ts +20 -0
  136. package/types/e2e/src/driver/physical-conditions.d.ts +11 -0
  137. package/types/e2e/src/driver/requirements.d.ts +14 -0
  138. package/types/e2e/src/driver/screen-reader.d.ts +26 -0
  139. package/types/e2e/src/driver/typing.d.ts +1 -1
  140. package/types/e2e/src/env.d.ts +47 -0
  141. package/types/e2e/src/expect/a11y-matchers.d.ts +33 -0
  142. package/types/e2e/src/expect/expect.d.ts +7 -2
  143. package/types/e2e/src/expect/matchers.d.ts +14 -0
  144. package/types/e2e/src/fixture-session.d.ts +4 -0
  145. package/types/e2e/src/fixtures.d.ts +6 -1
  146. package/types/e2e/src/index.d.ts +6 -0
  147. package/types/e2e/src/runner/callsite.d.ts +3 -1
  148. package/types/e2e/src/runner/scope.d.ts +1 -0
  149. package/types/e2e/src/runner/test.d.ts +6 -0
  150. package/types/e2e/src/screen/render.d.ts +8 -0
  151. package/types/e2e/src/traced-device.d.ts +2 -1
@@ -21,6 +21,20 @@ function e2eErr(error) {
21
21
  return { success: false, error: E2eError2.from(error) };
22
22
  }
23
23
 
24
+ // ../../packages/e2e/src/a11y/a11y-target.ts
25
+ var registry = new WeakMap;
26
+ function registerA11yTarget(target, hooks) {
27
+ registry.set(target, hooks);
28
+ }
29
+ function a11yTargetOf(target) {
30
+ return typeof target === "object" && target !== null ? registry.get(target) : undefined;
31
+ }
32
+
33
+ // ../../packages/e2e/src/driver/element-actions.ts
34
+ function canActOnElements(driver) {
35
+ return typeof driver.pressElement === "function";
36
+ }
37
+
24
38
  // ../../packages/e2e/src/driver/span-detail.ts
25
39
  import { AsyncLocalStorage } from "async_hooks";
26
40
  var collectors = new AsyncLocalStorage;
@@ -1272,6 +1286,24 @@ function head(entry) {
1272
1286
  function describeEntry(entry) {
1273
1287
  return head(entry).join(" ");
1274
1288
  }
1289
+ function a11yWords(entry) {
1290
+ const a11y = entry.element.a11y;
1291
+ if (!a11y)
1292
+ return [];
1293
+ const words = [];
1294
+ if (a11y.hint)
1295
+ words.push(`hint=${quote(a11y.hint, MAX_NAME)}`);
1296
+ if (a11y.actions && a11y.actions.length > 0) {
1297
+ words.push(`actions=[${a11y.actions.map((action) => quote(action, MAX_VALUE)).join(",")}]`);
1298
+ }
1299
+ if (a11y.a11yFocused)
1300
+ words.push("a11y-focused");
1301
+ if (a11y.important === false)
1302
+ words.push("not-important");
1303
+ if (a11y.liveRegion)
1304
+ words.push(`live=${a11y.liveRegion}`);
1305
+ return words;
1306
+ }
1275
1307
  function renderEntryLine(entry, options = {}) {
1276
1308
  const parts = [];
1277
1309
  if (options.ref !== undefined)
@@ -1282,6 +1314,8 @@ function renderEntryLine(entry, options = {}) {
1282
1314
  parts.push(...stateWords(entry));
1283
1315
  const { x, y, width, height } = entry.element.frame;
1284
1316
  parts.push(`(${Math.round(x)},${Math.round(y)} ${Math.round(width)}\xD7${Math.round(height)})`);
1317
+ if (options.a11y)
1318
+ parts.push(...a11yWords(entry));
1285
1319
  return `${" ".repeat(options.indent ?? 0)}${parts.join(" ")}`;
1286
1320
  }
1287
1321
  function isListed(entry) {
@@ -1297,6 +1331,7 @@ function renderSnapshot(snapshot, options) {
1297
1331
  const visible = listed.slice(0, limit);
1298
1332
  const lines = visible.map((entry) => renderEntryLine(entry, {
1299
1333
  indent: options.interactiveOnly ? 0 : entry.depth,
1334
+ ...options.a11y ? { a11y: true } : {},
1300
1335
  ...options.refs.has(entry.element.id) ? { ref: options.refs.get(entry.element.id) } : {}
1301
1336
  }));
1302
1337
  const omitted = listed.length - visible.length;
@@ -1842,7 +1877,12 @@ class Locator {
1842
1877
  const spec = { enabled: !withPosition, tapPointOnScreen: !withPosition };
1843
1878
  const found = (options.until === undefined ? await this.aimFromKept(spec, options.position) : undefined) ?? await this.actionable(spec, options.timeout ?? this.rt.actionTimeout(), true);
1844
1879
  const point = "react" in found ? reactTapPoint(found.react, options.position) : tapPointOf(found.entry, options.position);
1845
- this.rt.drive("tap", await found.observed.driver.tap(point));
1880
+ const { driver } = found.observed;
1881
+ if (!withPosition && "entry" in found && canActOnElements(driver)) {
1882
+ if (this.rt.drive("tap", await driver.pressElement(found.entry.element)))
1883
+ return found.observed.snapshot;
1884
+ }
1885
+ this.rt.drive("tap", await driver.tap(point));
1846
1886
  return found.observed.snapshot;
1847
1887
  }
1848
1888
  async arrivedWithin(plan, pressedOn) {
@@ -1953,6 +1993,10 @@ class Locator {
1953
1993
  }
1954
1994
  return this.act("swipe", async () => {
1955
1995
  const { entry, observed } = await this.actionable({ enabled: false, tapPointOnScreen: false }, options.timeout ?? this.rt.actionTimeout());
1996
+ if (canActOnElements(observed.driver)) {
1997
+ if (this.rt.drive("swipe", await observed.driver.scrollElement(options.direction, entry.element)))
1998
+ return;
1999
+ }
1956
2000
  const vector = swipeVector(options.direction, visibleRect(entry.element.frame, observed.snapshot.screen), options.momentum);
1957
2001
  this.rt.drive("swipe", await observed.driver.swipe(vector.from, vector.to, vector.options));
1958
2002
  });
@@ -2170,7 +2214,9 @@ async function scrollUntilVisible(rt, target, options = {}) {
2170
2214
  throw scrollGaveUp(target, entry === undefined, `${swipes} swipes ${direction}`, rt.excerpt(observed.snapshot));
2171
2215
  }
2172
2216
  const vector = swipeVector(direction, screenRect(observed.snapshot.screen), options.momentum);
2173
- rt.drive("scrollUntilVisible", await observed.driver.swipe(vector.from, vector.to, vector.options));
2217
+ const scrolled = canActOnElements(observed.driver) && rt.drive("scrollUntilVisible", await observed.driver.scrollElement(direction));
2218
+ if (!scrolled)
2219
+ rt.drive("scrollUntilVisible", await observed.driver.swipe(vector.from, vector.to, vector.options));
2174
2220
  await observed.driver.settle(SCROLL_SETTLE);
2175
2221
  }
2176
2222
  });
@@ -3076,6 +3122,9 @@ function describeDevices(targets) {
3076
3122
  }
3077
3123
  return [...seen.values()].join(", ");
3078
3124
  }
3125
+ function sameDeviceName(served, wanted) {
3126
+ return wanted !== undefined && served.replaceAll("+", " ") === wanted.replaceAll("+", " ");
3127
+ }
3079
3128
  function normalizeWsUrl(wsUrl, metroUrl) {
3080
3129
  const ws = new URL(wsUrl);
3081
3130
  const metro = new URL(metroUrl);
@@ -3094,7 +3143,11 @@ function selectTarget(targets, query, metroUrl) {
3094
3143
  (t) => t.reactNative?.logicalDeviceId === query.logicalDeviceId || urlDevice(t) === query.logicalDeviceId,
3095
3144
  "logicalDeviceId"
3096
3145
  ],
3097
- [query.deviceName, (t) => t.deviceName === query.deviceName, "deviceName"]
3146
+ [
3147
+ query.deviceName,
3148
+ (t) => t.deviceName !== undefined && sameDeviceName(t.deviceName, query.deviceName),
3149
+ "deviceName"
3150
+ ]
3098
3151
  ];
3099
3152
  for (const [value, matches, label] of wanted) {
3100
3153
  if (!value)
@@ -4390,7 +4443,9 @@ var DRIVER_CODES = new Set([
4390
4443
  "E_HID",
4391
4444
  "E_IO",
4392
4445
  "E_BAD_REQUEST",
4393
- "E_INTERNAL"
4446
+ "E_INTERNAL",
4447
+ "E_DEVICE_LOCKED",
4448
+ "E_REFUSED"
4394
4449
  ]);
4395
4450
  function isDriverCode(code) {
4396
4451
  return DRIVER_CODES.has(code);
@@ -4515,6 +4570,7 @@ function stdioTransport(opts) {
4515
4570
  };
4516
4571
  }
4517
4572
  function socketTransport(opts) {
4573
+ const label = opts.tcp ? `${opts.tcp.hostname}:${opts.tcp.port}` : opts.socketPath ?? "";
4518
4574
  return async (handlers) => {
4519
4575
  const splitter = createLineSplitter();
4520
4576
  const encoder = new TextEncoder;
@@ -4534,37 +4590,35 @@ function socketTransport(opts) {
4534
4590
  queued = written >= queued.length ? new Uint8Array(0) : queued.slice(Math.max(written, 0));
4535
4591
  };
4536
4592
  try {
4537
- const connected = await Bun.connect({
4538
- unix: opts.socketPath,
4539
- socket: {
4540
- open(s) {
4541
- socket = s;
4542
- },
4543
- data(_s, chunk) {
4544
- for (const line of splitter.push(chunk))
4545
- handlers.onLine(line);
4546
- },
4547
- drain() {
4548
- flush();
4549
- },
4550
- close() {
4551
- notifyClosed();
4552
- },
4553
- error() {
4554
- notifyClosed();
4555
- },
4556
- end() {
4557
- notifyClosed();
4558
- }
4593
+ const socketHandlers = {
4594
+ open(s) {
4595
+ socket = s;
4596
+ },
4597
+ data(_s, chunk) {
4598
+ for (const line of splitter.push(chunk))
4599
+ handlers.onLine(line);
4600
+ },
4601
+ drain() {
4602
+ flush();
4603
+ },
4604
+ close() {
4605
+ notifyClosed();
4606
+ },
4607
+ error() {
4608
+ notifyClosed();
4609
+ },
4610
+ end() {
4611
+ notifyClosed();
4559
4612
  }
4560
- });
4613
+ };
4614
+ const connected = opts.tcp ? await Bun.connect({ hostname: opts.tcp.hostname, port: opts.tcp.port, socket: socketHandlers }) : await Bun.connect({ unix: opts.socketPath, socket: socketHandlers });
4561
4615
  socket = connected;
4562
4616
  } catch (error) {
4563
4617
  const reason = error instanceof Error ? error.message : String(error);
4564
4618
  return {
4565
4619
  success: false,
4566
- error: driverError("E_HELPER_CRASHED", `cannot connect to warden-simd socket ${opts.socketPath}: ${reason}`, {
4567
- detail: { socketPath: opts.socketPath }
4620
+ error: driverError("E_HELPER_CRASHED", `cannot connect to warden-simd socket ${label}: ${reason}`, {
4621
+ detail: { socketPath: label }
4568
4622
  })
4569
4623
  };
4570
4624
  }
@@ -4914,6 +4968,22 @@ function isHidLost(error) {
4914
4968
  const hidInvalidated = /\bE_HID\b/.test(error.message) && /dtuhidd connection[\s\S]*invalidated/.test(error.message);
4915
4969
  return hidInvalidated || /\bE_NOT_BOOTED\b/.test(error.message);
4916
4970
  }
4971
+ function isRunnerLost(error) {
4972
+ if (!(error instanceof E2eError2) || error.code !== "ACTION_FAILED" && error.code !== "DRIVER")
4973
+ return false;
4974
+ return /\bE_HELPER_CRASHED\b/.test(error.message);
4975
+ }
4976
+ function isRecoverableLoss(error, deviceKind) {
4977
+ return deviceKind === "physical" ? isRunnerLost(error) : isHidLost(error);
4978
+ }
4979
+ var RUNNER_RESTART_MS = 180000;
4980
+ async function restartRunnerHost(deps, argv, udid) {
4981
+ if (argv === undefined || argv.length === 0)
4982
+ return err("WARDEN_IOS_RUNNER_RESTART is not set");
4983
+ const cmd = argv.map((arg) => arg.replaceAll("{udid}", udid));
4984
+ const res = await deps.exec(cmd, { timeoutMs: RUNNER_RESTART_MS });
4985
+ return res.exitCode === 0 ? ok(undefined) : err(execError(cmd, res));
4986
+ }
4917
4987
  async function rebootSim(deps, udid) {
4918
4988
  const down = ["xcrun", "simctl", "shutdown", udid];
4919
4989
  const shutdown = await deps.exec(down, { timeoutMs: SHUTDOWN_MS });
@@ -5042,8 +5112,328 @@ function traceDriver(driver, options) {
5042
5112
  return wrap(driver, "driver.");
5043
5113
  }
5044
5114
 
5045
- // ../../packages/e2e/src/env.ts
5115
+ // ../../packages/core/src/a11y/a11y.config.ts
5046
5116
  import { z as z7 } from "zod";
5117
+ var A11Y_RULE_IDS = [
5118
+ "missing-name",
5119
+ "image-no-name",
5120
+ "target-size",
5121
+ "target-size-platform",
5122
+ "duplicate-name",
5123
+ "role-in-name",
5124
+ "label-in-name",
5125
+ "hidden-focusable",
5126
+ "text-clipped",
5127
+ "native"
5128
+ ];
5129
+ var ruleId = z7.enum(A11Y_RULE_IDS);
5130
+ var a11yAuditConfigSchema = z7.object({
5131
+ ignore: z7.array(z7.object({ rule: ruleId, identifier: z7.string().min(1).optional() }).strict()).optional(),
5132
+ minSeverity: z7.enum(["error", "warning"]).optional(),
5133
+ rules: z7.array(ruleId).min(1).optional()
5134
+ }).strict();
5135
+ function parseA11yAuditEnv(raw) {
5136
+ let json;
5137
+ try {
5138
+ json = JSON.parse(raw);
5139
+ } catch {
5140
+ return err("must be JSON");
5141
+ }
5142
+ const parsed = a11yAuditConfigSchema.safeParse(json);
5143
+ if (parsed.success)
5144
+ return ok(parsed.data);
5145
+ return err(parsed.error.issues.map((i) => `${i.path.join(".") || "a11y"}: ${i.message}`).join("; "));
5146
+ }
5147
+
5148
+ // ../../packages/core/src/conditions/conditions.config.ts
5149
+ import { z as z8 } from "zod";
5150
+
5151
+ // ../../packages/core/src/duration.ts
5152
+ var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
5153
+ function parseDuration(input) {
5154
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(input.trim());
5155
+ if (!match?.[1])
5156
+ return { success: false, error: `invalid duration "${input}" (use e.g. 30s, 10m, 2h)` };
5157
+ return { success: true, data: Math.round(Number(match[1]) * (UNIT_MS[match[2] ?? "s"] ?? 1000)) };
5158
+ }
5159
+
5160
+ // ../../packages/core/src/conditions/conditions.types.ts
5161
+ var PERMISSION_NAMES = [
5162
+ "location",
5163
+ "locationAlways",
5164
+ "camera",
5165
+ "photos",
5166
+ "photosAdd",
5167
+ "contacts",
5168
+ "contactsLimited",
5169
+ "notifications",
5170
+ "microphone",
5171
+ "calendar",
5172
+ "reminders",
5173
+ "motion",
5174
+ "mediaLibrary",
5175
+ "siri"
5176
+ ];
5177
+ var TEXT_SIZE_NAMES = [
5178
+ "extra-small",
5179
+ "small",
5180
+ "medium",
5181
+ "large",
5182
+ "extra-large",
5183
+ "extra-extra-large",
5184
+ "extra-extra-extra-large",
5185
+ "accessibility-medium",
5186
+ "accessibility-large",
5187
+ "accessibility-extra-large",
5188
+ "accessibility-extra-extra-large",
5189
+ "accessibility-extra-extra-extra-large"
5190
+ ];
5191
+ var CAMERA_FACINGS = ["back", "front"];
5192
+ var CONDITION_AREAS = [
5193
+ "network",
5194
+ "location",
5195
+ "clock",
5196
+ "permission",
5197
+ "locale",
5198
+ "a11y",
5199
+ "battery",
5200
+ "camera"
5201
+ ];
5202
+
5203
+ // ../../packages/core/src/conditions/network.presets.ts
5204
+ var NETWORK_PRESET_NAMES = ["edge", "3g", "4g", "lte", "wifi", "slow", "lossy"];
5205
+ var NETWORK_PRESETS = {
5206
+ edge: {
5207
+ emu: { speed: "200:240", delay: "175:225" },
5208
+ shaping: { latencyMs: 400, jitterMs: 25, downKbps: 240, upKbps: 200, lossPct: 0 }
5209
+ },
5210
+ "3g": {
5211
+ emu: { speed: "330:780", delay: "40:60" },
5212
+ shaping: { latencyMs: 100, jitterMs: 10, downKbps: 780, upKbps: 330, lossPct: 0 }
5213
+ },
5214
+ "4g": {
5215
+ emu: { speed: "9000:9000", delay: "70:100" },
5216
+ shaping: { latencyMs: 170, jitterMs: 15, downKbps: 9000, upKbps: 9000, lossPct: 0 }
5217
+ },
5218
+ lte: {
5219
+ emu: { speed: "10000:50000", delay: "20:30" },
5220
+ shaping: { latencyMs: 50, jitterMs: 5, downKbps: 50000, upKbps: 1e4, lossPct: 0 }
5221
+ },
5222
+ wifi: {
5223
+ emu: { speed: "33000:40000", delay: "0:1" },
5224
+ shaping: { latencyMs: 2, jitterMs: 0, downKbps: 40000, upKbps: 33000, lossPct: 0 }
5225
+ },
5226
+ slow: {
5227
+ emu: { speed: "400:400", delay: "900:1100" },
5228
+ shaping: { latencyMs: 2000, jitterMs: 100, downKbps: 400, upKbps: 400, lossPct: 0 }
5229
+ },
5230
+ lossy: {
5231
+ emu: { speed: "1000:1000", delay: "200:300" },
5232
+ shaping: { latencyMs: 500, jitterMs: 50, downKbps: 1000, upKbps: 1000, lossPct: 10 }
5233
+ }
5234
+ };
5235
+ var UNSHAPED = { latencyMs: 0, jitterMs: 0, downKbps: 0, upKbps: 0, lossPct: 0 };
5236
+ var UNSHAPED_EMU = { speed: "full", delay: "none" };
5237
+ var nonNegative = (value) => value === undefined || !Number.isFinite(value) ? 0 : Math.max(0, value);
5238
+ function customEmu(shaping) {
5239
+ const { downKbps, upKbps, latencyMs, jitterMs } = shaping;
5240
+ const speed = downKbps === 0 && upKbps === 0 ? UNSHAPED_EMU.speed : `${upKbps || downKbps}:${downKbps || upKbps}`;
5241
+ const half = latencyMs / 2;
5242
+ const min = Math.max(0, Math.round(half - jitterMs));
5243
+ const max = Math.round(half + jitterMs);
5244
+ const delay = max === 0 ? UNSHAPED_EMU.delay : `${min}:${max}`;
5245
+ return { speed, delay };
5246
+ }
5247
+ function resolveNetwork(condition) {
5248
+ switch (condition.kind) {
5249
+ case "offline":
5250
+ return { online: false };
5251
+ case "online":
5252
+ return { online: true, shaping: UNSHAPED, emu: UNSHAPED_EMU };
5253
+ case "preset":
5254
+ return { online: true, ...NETWORK_PRESETS[condition.preset] };
5255
+ case "flap":
5256
+ return resolveNetwork(condition.base ?? { kind: "online" });
5257
+ case "custom": {
5258
+ const shaping = {
5259
+ latencyMs: nonNegative(condition.latencyMs),
5260
+ jitterMs: nonNegative(condition.jitterMs),
5261
+ downKbps: nonNegative(condition.downKbps),
5262
+ upKbps: nonNegative(condition.upKbps),
5263
+ lossPct: Math.min(100, nonNegative(condition.lossPct))
5264
+ };
5265
+ return { online: true, shaping, emu: customEmu(shaping) };
5266
+ }
5267
+ }
5268
+ }
5269
+
5270
+ // ../../packages/core/src/conditions/text-size.ts
5271
+ var TEXT_SIZE_SCALE = {
5272
+ "extra-small": 0.82,
5273
+ small: 0.88,
5274
+ medium: 0.94,
5275
+ large: 1,
5276
+ "extra-large": 1.12,
5277
+ "extra-extra-large": 1.24,
5278
+ "extra-extra-extra-large": 1.35,
5279
+ "accessibility-medium": 1.6,
5280
+ "accessibility-large": 1.9,
5281
+ "accessibility-extra-large": 2.35,
5282
+ "accessibility-extra-extra-large": 2.75,
5283
+ "accessibility-extra-extra-extra-large": 3.1
5284
+ };
5285
+ var ANDROID_MAX_FONT_SCALE = 2;
5286
+ var ANDROID_MIN_FONT_SCALE = 0.5;
5287
+ var isName = (v) => TEXT_SIZE_NAMES.includes(v);
5288
+ function resolveTextSize(size) {
5289
+ if (typeof size === "number") {
5290
+ if (!Number.isFinite(size) || size <= 0)
5291
+ return err(`text size must be a positive scale, got ${String(size)}`);
5292
+ let best = TEXT_SIZE_NAMES[0];
5293
+ for (const name of TEXT_SIZE_NAMES) {
5294
+ if (Math.abs(TEXT_SIZE_SCALE[name] - size) < Math.abs(TEXT_SIZE_SCALE[best] - size))
5295
+ best = name;
5296
+ }
5297
+ return ok({ category: best, scale: size });
5298
+ }
5299
+ if (!isName(size))
5300
+ return err(`unknown text size "${String(size)}" (use ${TEXT_SIZE_NAMES.join(", ")} or a scale like 1.3)`);
5301
+ return ok({ category: size, scale: TEXT_SIZE_SCALE[size] });
5302
+ }
5303
+ var androidFontScale = (scale) => Math.min(ANDROID_MAX_FONT_SCALE, Math.max(ANDROID_MIN_FONT_SCALE, Math.round(scale * 100) / 100));
5304
+ function appliedA11y(op, applied) {
5305
+ return Object.fromEntries(Object.entries(op).filter(([key]) => applied.includes(key)));
5306
+ }
5307
+
5308
+ // ../../packages/core/src/conditions/conditions.config.ts
5309
+ var NETWORK_WORDS = ["online", "offline", ...NETWORK_PRESET_NAMES];
5310
+ var nonNegative2 = z8.number().nonnegative().finite();
5311
+ var customNetworkSchema = z8.object({
5312
+ latencyMs: nonNegative2.optional(),
5313
+ jitterMs: nonNegative2.optional(),
5314
+ downKbps: nonNegative2.optional(),
5315
+ upKbps: nonNegative2.optional(),
5316
+ lossPct: z8.number().min(0).max(100).optional()
5317
+ }).strict();
5318
+ var isoDate = z8.string().refine((v) => !Number.isNaN(Date.parse(v)), { message: "invalid date (use ISO 8601)" });
5319
+ var offsetDuration = z8.string().refine((v) => parseDuration(v).success, { message: "invalid duration (use e.g. 2d)" });
5320
+ var textSize = z8.union([z8.enum(TEXT_SIZE_NAMES), z8.number().positive().finite()]);
5321
+ var a11yConfigSchema = z8.object({
5322
+ textSize: textSize.optional(),
5323
+ boldText: z8.boolean().optional(),
5324
+ contrast: z8.enum(["increased", "normal"]).optional(),
5325
+ reduceMotion: z8.boolean().optional()
5326
+ }).strict();
5327
+ var conditionsConfigSchema = z8.object({
5328
+ network: z8.union([z8.enum(NETWORK_WORDS), customNetworkSchema]).optional(),
5329
+ location: z8.union([
5330
+ z8.enum(["city-run", "city-bike", "freeway"]),
5331
+ z8.object({
5332
+ lat: z8.number().min(-90).max(90),
5333
+ lon: z8.number().min(-180).max(180),
5334
+ altM: z8.number().finite().optional()
5335
+ }).strict()
5336
+ ]).optional(),
5337
+ clock: z8.union([
5338
+ isoDate,
5339
+ z8.object({ at: isoDate, frozen: z8.boolean().optional() }).strict(),
5340
+ z8.object({ offset: offsetDuration }).strict()
5341
+ ]).optional(),
5342
+ permissions: z8.partialRecord(z8.enum(PERMISSION_NAMES), z8.enum(["granted", "denied", "unset"])).optional(),
5343
+ locale: z8.string().min(1).optional(),
5344
+ timezone: z8.string().min(1).optional(),
5345
+ a11y: a11yConfigSchema.optional()
5346
+ }).strict();
5347
+ var cameraFeedInput = z8.union([
5348
+ z8.string().min(1),
5349
+ z8.object({ path: z8.string().min(1), loop: z8.boolean().optional() }).strict()
5350
+ ]);
5351
+ var cameraConfigSchema = z8.object({ back: cameraFeedInput.optional(), front: cameraFeedInput.optional() }).strict().refine((v) => v.back !== undefined || v.front !== undefined, { message: "camera needs a back or a front asset" });
5352
+ var cameraFeedsSchema = z8.object({
5353
+ back: z8.object({ path: z8.string().min(1), loop: z8.boolean() }).strict().optional(),
5354
+ front: z8.object({ path: z8.string().min(1), loop: z8.boolean() }).strict().optional()
5355
+ }).strict();
5356
+ function parseCameraFeedsEnv(raw) {
5357
+ let json;
5358
+ try {
5359
+ json = JSON.parse(raw);
5360
+ } catch {
5361
+ return err("must be JSON");
5362
+ }
5363
+ const parsed = cameraFeedsSchema.safeParse(json);
5364
+ return parsed.success ? ok(parsed.data) : err(parsed.error.issues.map((i) => i.message).join("; "));
5365
+ }
5366
+ var isWord = (v) => NETWORK_WORDS.includes(v);
5367
+ function toNetworkCondition(input) {
5368
+ if (typeof input === "string") {
5369
+ if (!isWord(input))
5370
+ return err(`unknown network "${input}" (use ${NETWORK_WORDS.join(", ")})`);
5371
+ if (input === "online" || input === "offline")
5372
+ return ok({ kind: input });
5373
+ return ok({ kind: "preset", preset: input });
5374
+ }
5375
+ if ("kind" in input)
5376
+ return ok(input);
5377
+ return ok({ kind: "custom", ...input });
5378
+ }
5379
+ function toLocation(input) {
5380
+ if (typeof input === "string")
5381
+ return { kind: "scenario", name: input };
5382
+ return { kind: "point", lat: input.lat, lon: input.lon, ...input.altM === undefined ? {} : { altM: input.altM } };
5383
+ }
5384
+ function toClock(input) {
5385
+ if (typeof input === "string")
5386
+ return { kind: "fixed", at: new Date(input).toISOString() };
5387
+ if ("offset" in input) {
5388
+ const ms = parseDuration(input.offset);
5389
+ return { kind: "offset", ms: ms.success ? ms.data : 0 };
5390
+ }
5391
+ const at = new Date(input.at).toISOString();
5392
+ return input.frozen === true ? { kind: "frozen", at } : { kind: "fixed", at };
5393
+ }
5394
+ function normalizeConditions(config) {
5395
+ const conditions = {};
5396
+ if (config.network !== undefined) {
5397
+ const network = toNetworkCondition(config.network);
5398
+ if (!network.success)
5399
+ return network;
5400
+ conditions.network = network.data;
5401
+ }
5402
+ if (config.location !== undefined)
5403
+ conditions.location = toLocation(config.location);
5404
+ if (config.clock !== undefined)
5405
+ conditions.clock = toClock(config.clock);
5406
+ if (config.permissions !== undefined)
5407
+ conditions.permissions = config.permissions;
5408
+ if (config.locale !== undefined)
5409
+ conditions.locale = config.locale;
5410
+ if (config.timezone !== undefined)
5411
+ conditions.timezone = config.timezone;
5412
+ if (config.a11y !== undefined) {
5413
+ const a11y = { ...config.a11y };
5414
+ if (config.a11y.textSize !== undefined) {
5415
+ const size = resolveTextSize(config.a11y.textSize);
5416
+ if (!size.success)
5417
+ return size;
5418
+ a11y.fontScale = size.data.scale;
5419
+ }
5420
+ conditions.a11y = a11y;
5421
+ }
5422
+ return ok({ conditions });
5423
+ }
5424
+ function parseConditionsEnv(raw) {
5425
+ let json;
5426
+ try {
5427
+ json = JSON.parse(raw);
5428
+ } catch {
5429
+ return err("must be JSON");
5430
+ }
5431
+ const parsed = conditionsConfigSchema.safeParse(json);
5432
+ return parsed.success ? ok(parsed.data) : err(parsed.error.issues.map((i) => i.message).join("; "));
5433
+ }
5434
+
5435
+ // ../../packages/e2e/src/env.ts
5436
+ import { z as z9 } from "zod";
5047
5437
  var ENV_NAMES = {
5048
5438
  udid: "WARDEN_UDID",
5049
5439
  deviceName: "WARDEN_DEVICE_NAME",
@@ -5052,6 +5442,7 @@ var ENV_NAMES = {
5052
5442
  appPath: "WARDEN_APP_PATH",
5053
5443
  metroUrl: "WARDEN_METRO_URL",
5054
5444
  launchArgs: "WARDEN_LAUNCH_ARGS",
5445
+ devClientScheme: "WARDEN_DEV_CLIENT_SCHEME",
5055
5446
  out: "WARDEN_E2E_OUT",
5056
5447
  timeout: "WARDEN_E2E_TIMEOUT",
5057
5448
  actionTimeout: "WARDEN_E2E_ACTION_TIMEOUT",
@@ -5060,57 +5451,98 @@ var ENV_NAMES = {
5060
5451
  helperDir: "WARDEN_HELPER_DIR",
5061
5452
  simd: "WARDEN_SIMD",
5062
5453
  simdSocket: "WARDEN_SIMD_SOCKET",
5454
+ iosRunnerSocket: "WARDEN_IOS_RUNNER_SOCKET",
5063
5455
  logTimestamps: "WARDEN_LOG_TIMESTAMPS",
5456
+ deviceKind: "WARDEN_DEVICE_KIND",
5457
+ iosRunnerRestart: "WARDEN_IOS_RUNNER_RESTART",
5064
5458
  afterReboot: "WARDEN_AFTER_REBOOT",
5065
5459
  version: "WARDEN_VERSION",
5066
5460
  bin: "WARDEN_BIN",
5067
5461
  perf: "WARDEN_PERF",
5068
- perfBudgets: "WARDEN_PERF_BUDGETS"
5462
+ perfBudgets: "WARDEN_PERF_BUDGETS",
5463
+ conditions: "WARDEN_CONDITIONS",
5464
+ leaseId: "WARDEN_LEASE_ID",
5465
+ netproxyCommand: "WARDEN_NETPROXY_COMMAND",
5466
+ biometrics: "WARDEN_BIOMETRICS",
5467
+ a11yAudit: "WARDEN_A11Y_AUDIT",
5468
+ a11y: "WARDEN_A11Y",
5469
+ cameraFeeds: "WARDEN_CAMERA_FEEDS"
5069
5470
  };
5070
5471
  var DEFAULT_TIMEOUT_MS4 = 120000;
5071
5472
  var DEFAULT_ACTION_TIMEOUT_MS = 30000;
5072
5473
  var DEFAULT_ASSERTION_TIMEOUT_MS = 5000;
5073
- var toggle = z7.enum(["1", "true", "0", "false"]).transform((v) => v === "1" || v === "true");
5074
- var millis = z7.string().regex(/^\d+$/, "must be a whole number of milliseconds").transform(Number).pipe(z7.number().int().positive());
5075
- var launchArgs = z7.string().transform((raw, ctx) => {
5474
+ var toggle = z9.enum(["1", "true", "0", "false"]).transform((v) => v === "1" || v === "true");
5475
+ var millis = z9.string().regex(/^\d+$/, "must be a whole number of milliseconds").transform(Number).pipe(z9.number().int().positive());
5476
+ var launchArgs = z9.string().transform((raw, ctx) => {
5076
5477
  try {
5077
- const parsed = z7.array(z7.string()).safeParse(JSON.parse(raw));
5478
+ const parsed = z9.array(z9.string()).safeParse(JSON.parse(raw));
5078
5479
  if (parsed.success)
5079
5480
  return parsed.data;
5080
5481
  } catch {}
5081
5482
  ctx.addIssue({ code: "custom", message: "must be a JSON array of strings" });
5082
- return z7.NEVER;
5483
+ return z9.NEVER;
5484
+ });
5485
+ var conditions = z9.string().transform((raw, ctx) => {
5486
+ const parsed = parseConditionsEnv(raw);
5487
+ if (parsed.success)
5488
+ return parsed.data;
5489
+ ctx.addIssue({ code: "custom", message: parsed.error });
5490
+ return z9.NEVER;
5491
+ });
5492
+ var a11yAudit = z9.string().transform((raw, ctx) => {
5493
+ const parsed = parseA11yAuditEnv(raw);
5494
+ if (parsed.success)
5495
+ return parsed.data;
5496
+ ctx.addIssue({ code: "custom", message: parsed.error });
5497
+ return z9.NEVER;
5498
+ });
5499
+ var cameraFeeds = z9.string().transform((raw, ctx) => {
5500
+ const parsed = parseCameraFeedsEnv(raw);
5501
+ if (parsed.success)
5502
+ return parsed.data;
5503
+ ctx.addIssue({ code: "custom", message: parsed.error });
5504
+ return z9.NEVER;
5083
5505
  });
5084
5506
  var REQUIRED = "required when WARDEN_UDID is set";
5085
- var schema = z7.object({
5086
- udid: z7.string().min(1),
5087
- deviceName: z7.string().optional(),
5088
- platform: z7.enum(["ios", "android"]).default("ios"),
5089
- bundleId: z7.string({ error: REQUIRED }).min(1, REQUIRED),
5090
- appPath: z7.string().optional(),
5091
- metroUrl: z7.string().optional(),
5507
+ var schema = z9.object({
5508
+ udid: z9.string().min(1),
5509
+ deviceName: z9.string().optional(),
5510
+ platform: z9.enum(["ios", "android"]).default("ios"),
5511
+ bundleId: z9.string({ error: REQUIRED }).min(1, REQUIRED),
5512
+ appPath: z9.string().optional(),
5513
+ metroUrl: z9.string().optional(),
5514
+ devClientScheme: z9.string().min(1).optional(),
5092
5515
  launchArgs: launchArgs.default([]),
5093
- out: z7.string().optional(),
5516
+ out: z9.string().optional(),
5094
5517
  timeout: millis.default(DEFAULT_TIMEOUT_MS4),
5095
5518
  actionTimeout: millis.default(DEFAULT_ACTION_TIMEOUT_MS),
5096
5519
  assertionTimeout: millis.default(DEFAULT_ASSERTION_TIMEOUT_MS),
5097
- specsDir: z7.string().optional(),
5098
- helperDir: z7.string().optional(),
5099
- simd: z7.string().optional(),
5100
- simdSocket: z7.string().optional(),
5520
+ specsDir: z9.string().optional(),
5521
+ helperDir: z9.string().optional(),
5522
+ simd: z9.string().optional(),
5523
+ simdSocket: z9.string().optional(),
5524
+ iosRunnerSocket: z9.string().optional(),
5101
5525
  logTimestamps: toggle.default(false),
5526
+ deviceKind: z9.enum(["physical"]).optional(),
5527
+ iosRunnerRestart: launchArgs.optional(),
5102
5528
  afterReboot: launchArgs.optional(),
5103
- version: z7.string().optional(),
5104
- bin: z7.string().optional(),
5105
- perf: z7.string().optional(),
5106
- perfBudgets: z7.string().optional()
5529
+ version: z9.string().optional(),
5530
+ bin: z9.string().optional(),
5531
+ perf: z9.string().optional(),
5532
+ perfBudgets: z9.string().optional(),
5533
+ conditions: conditions.optional(),
5534
+ leaseId: z9.string().min(1).optional(),
5535
+ netproxyCommand: launchArgs.pipe(z9.array(z9.string()).min(1)).optional(),
5536
+ biometrics: toggle.optional(),
5537
+ a11yAudit: a11yAudit.optional(),
5538
+ cameraFeeds: cameraFeeds.optional()
5107
5539
  });
5108
5540
  function defined2(values) {
5109
5541
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
5110
5542
  }
5111
5543
  function parseArgv(raw) {
5112
5544
  try {
5113
- const parsed = z7.array(z7.string()).min(1).safeParse(JSON.parse(raw));
5545
+ const parsed = z9.array(z9.string()).min(1).safeParse(JSON.parse(raw));
5114
5546
  if (parsed.success)
5115
5547
  return parsed.data;
5116
5548
  } catch {}
@@ -5131,13 +5563,23 @@ function toEnv(v, now) {
5131
5563
  deviceName: v.deviceName,
5132
5564
  appPath: v.appPath,
5133
5565
  metroUrl: v.metroUrl,
5566
+ devClientScheme: v.devClientScheme,
5134
5567
  specsDir: v.specsDir,
5135
5568
  helperDir: v.helperDir,
5136
5569
  simd: v.simd,
5137
5570
  simdSocket: v.simdSocket,
5571
+ iosRunnerSocket: v.iosRunnerSocket,
5138
5572
  wardenVersion: v.version,
5139
5573
  perfBudgets: v.perfBudgets
5140
5574
  }),
5575
+ ...v.deviceKind === undefined ? {} : { deviceKind: v.deviceKind },
5576
+ ...v.iosRunnerRestart === undefined ? {} : { iosRunnerRestart: v.iosRunnerRestart },
5577
+ ...v.conditions === undefined ? {} : { conditions: v.conditions },
5578
+ ...v.leaseId === undefined ? {} : { leaseId: v.leaseId },
5579
+ ...v.netproxyCommand === undefined ? {} : { netproxyCommand: v.netproxyCommand },
5580
+ ...v.biometrics === true ? { biometrics: true } : {},
5581
+ ...v.a11yAudit === undefined ? {} : { a11yAudit: v.a11yAudit },
5582
+ ...v.cameraFeeds === undefined ? {} : { cameraFeeds: v.cameraFeeds },
5141
5583
  ...v.afterReboot === undefined ? {} : { afterReboot: v.afterReboot },
5142
5584
  ...v.bin === undefined ? {} : { wardenBin: parseArgv(v.bin) },
5143
5585
  ...v.perf === undefined ? {} : {
@@ -5170,25 +5612,25 @@ function readEnv(env, opts = {}) {
5170
5612
  // ../../packages/e2e/src/runner/results.ts
5171
5613
  import { appendFileSync, mkdirSync } from "fs";
5172
5614
  import { dirname as dirname2 } from "path";
5173
- import { z as z8 } from "zod";
5174
- var testStatusSchema = z8.enum(["passed", "failed", "skipped", "timed-out"]);
5175
- var testResultSchema = z8.object({
5176
- file: z8.string(),
5177
- title: z8.array(z8.string()),
5615
+ import { z as z10 } from "zod";
5616
+ var testStatusSchema = z10.enum(["passed", "failed", "skipped", "timed-out"]);
5617
+ var testResultSchema = z10.object({
5618
+ file: z10.string(),
5619
+ title: z10.array(z10.string()),
5178
5620
  status: testStatusSchema,
5179
- durationMs: z8.number(),
5180
- startedAt: z8.number(),
5181
- only: z8.boolean().optional(),
5182
- error: z8.object({
5183
- message: z8.string(),
5184
- code: z8.string().optional(),
5185
- stack: z8.string().optional()
5621
+ durationMs: z10.number(),
5622
+ startedAt: z10.number(),
5623
+ only: z10.boolean().optional(),
5624
+ error: z10.object({
5625
+ message: z10.string(),
5626
+ code: z10.string().optional(),
5627
+ stack: z10.string().optional()
5186
5628
  }).optional(),
5187
- artifacts: z8.object({ dir: z8.string(), files: z8.array(z8.string()) }).optional(),
5188
- device: z8.object({
5189
- udid: z8.string(),
5190
- platform: z8.enum(["ios", "android"]),
5191
- name: z8.string().optional()
5629
+ artifacts: z10.object({ dir: z10.string(), files: z10.array(z10.string()) }).optional(),
5630
+ device: z10.object({
5631
+ udid: z10.string(),
5632
+ platform: z10.enum(["ios", "android"]),
5633
+ name: z10.string().optional()
5192
5634
  })
5193
5635
  });
5194
5636
  function openResults(path) {
@@ -5260,6 +5702,8 @@ async function runStep(attempt, kind, label, fn, now = Date.now) {
5260
5702
  }
5261
5703
 
5262
5704
  // ../../packages/e2e/src/session.ts
5705
+ var ENROLL_TRIES = 5;
5706
+ var ENROLL_RETRY_MS = 500;
5263
5707
  function devtoolsTarget(env) {
5264
5708
  return { appId: env.bundleId, ...env.deviceName === undefined ? {} : { deviceName: env.deviceName } };
5265
5709
  }
@@ -5366,10 +5810,27 @@ function createSession(opts = {}) {
5366
5810
  await sleep(WARM_INTERVAL_MS);
5367
5811
  }
5368
5812
  }
5813
+ async function recoverRunner(deps, name) {
5814
+ if (hidRecovered) {
5815
+ note(`iPhone ${name}: the runner connection was lost again after a restart; check the phone and retry`);
5816
+ return false;
5817
+ }
5818
+ hidRecovered = true;
5819
+ await resetDevice();
5820
+ const restarted = await restartRunnerHost(deps, resolved.iosRunnerRestart, resolved.udid);
5821
+ if (!restarted.success) {
5822
+ note(`iPhone ${name}: runner connection lost, restart failed: ${restarted.error}`);
5823
+ return false;
5824
+ }
5825
+ note(`iPhone ${name}: runner connection lost, runner host restarted; running the test again`);
5826
+ return true;
5827
+ }
5369
5828
  async function recoverHid() {
5370
5829
  if (hid === undefined || closing !== undefined)
5371
5830
  return false;
5372
5831
  const name = resolved.deviceName ?? resolved.udid;
5832
+ if (resolved.deviceKind === "physical")
5833
+ return recoverRunner(hid, name);
5373
5834
  if (hidRecovered) {
5374
5835
  note(`sim ${name}: HID connection lost again after a reboot; \`warden release <id> --shutdown\` and retry`);
5375
5836
  return false;
@@ -5391,6 +5852,24 @@ function createSession(opts = {}) {
5391
5852
  note(`sim ${name}: HID connection lost (Device Hub quit?), rebooted; running the test again`);
5392
5853
  return true;
5393
5854
  }
5855
+ async function reenroll(driver) {
5856
+ if (resolved.biometrics !== true || resolved.platform !== "ios" || resolved.deviceKind === "physical")
5857
+ return driver;
5858
+ const sleep = hid?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
5859
+ for (let attempt = 1;attempt <= ENROLL_TRIES; attempt += 1) {
5860
+ const state = await driver.biometrics("enroll");
5861
+ if (!state.success) {
5862
+ note(`biometrics enroll failed: ${state.error.code}: ${state.error.message}`);
5863
+ return driver;
5864
+ }
5865
+ if (state.data.enrolled)
5866
+ return driver;
5867
+ if (attempt < ENROLL_TRIES)
5868
+ await sleep(ENROLL_RETRY_MS);
5869
+ }
5870
+ note(`biometrics enroll did not take after ${ENROLL_TRIES} attempts`);
5871
+ return driver;
5872
+ }
5394
5873
  const session = {
5395
5874
  env: resolved,
5396
5875
  out,
@@ -5423,7 +5902,7 @@ function createSession(opts = {}) {
5423
5902
  throw new E2eError2("DRIVER", `${code}: ${message}`);
5424
5903
  }
5425
5904
  current = cacheObservations(traceDriver(result.data, { trace: () => attempt?.trace }));
5426
- return current;
5905
+ return reenroll(current);
5427
5906
  }, (cause) => {
5428
5907
  opened = undefined;
5429
5908
  throw new E2eError2("DRIVER", cause instanceof Error ? cause.message : String(cause));
@@ -5579,133 +6058,2823 @@ function getSession(opts = {}) {
5579
6058
  return session;
5580
6059
  }
5581
6060
 
5582
- // ../../packages/e2e/src/perf/device-perf.ts
5583
- import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync } from "fs";
5584
- import { join as join2 } from "path";
5585
-
5586
- // ../../packages/e2e/src/perf/budgets.ts
5587
- var BUDGET_KEYS = ["startupColdMs", "jankPct", "maxCommitMs", "p95CpuPct", "peakMemoryMb"];
5588
- function measured(report, metric) {
5589
- const m = report.metrics;
5590
- switch (metric) {
5591
- case "startupColdMs":
5592
- return m.startupColdMs;
5593
- case "jankPct":
5594
- return m.jankPct;
5595
- case "maxCommitMs":
5596
- return m.topCommitMs;
5597
- case "p95CpuPct":
5598
- return m.cpuP95Pct;
5599
- case "peakMemoryMb": {
5600
- const kb = Math.max(m.peakPssKb ?? 0, m.peakRssKb ?? 0);
5601
- return kb > 0 ? kb / 1024 : undefined;
5602
- }
5603
- }
6061
+ // ../../packages/e2e/src/a11y/speech.ts
6062
+ var ACTIONABLE_ROLES = new Set([
6063
+ "button",
6064
+ "switch",
6065
+ "checkbox",
6066
+ "slider",
6067
+ "link",
6068
+ "tab",
6069
+ "textfield",
6070
+ "securefield",
6071
+ "searchfield"
6072
+ ]);
6073
+ var INPUT_ROLES = new Set(["textfield", "securefield", "searchfield"]);
6074
+ var ON_VALUES = new Set(["1", "on", "true", "checked"]);
6075
+ var OFF_VALUES = new Set(["0", "off", "false", "unchecked"]);
6076
+ function clean(s) {
6077
+ const t = s?.trim();
6078
+ return t ? t : undefined;
6079
+ }
6080
+ function has(el, trait) {
6081
+ return el.traits.includes(trait);
6082
+ }
6083
+ function isActionable(el, platform) {
6084
+ if (ACTIONABLE_ROLES.has(el.role))
6085
+ return true;
6086
+ if (platform === "ios")
6087
+ return has(el, "button") || has(el, "link") || has(el, "adjustable");
6088
+ return has(el, "clickable") || has(el, "long-clickable") || has(el, "checkable");
6089
+ }
6090
+ function isFocusTarget(el, platform) {
6091
+ return el.a11y?.focusable === true || isActionable(el, platform);
6092
+ }
6093
+ function isInput(el) {
6094
+ return INPUT_ROLES.has(el.role);
6095
+ }
6096
+ function ownName(el, platform) {
6097
+ const label = clean(el.label);
6098
+ if (label)
6099
+ return label;
6100
+ const text = clean(el.text);
6101
+ if (text)
6102
+ return text;
6103
+ if (isInput(el)) {
6104
+ return platform === "android" ? clean(el.value) ?? clean(el.placeholder) : undefined;
6105
+ }
6106
+ if (el.role === "switch" || el.role === "checkbox")
6107
+ return;
6108
+ return platform === "android" ? clean(el.value) : undefined;
5604
6109
  }
5605
- function checkBudgets(report, base, overrides) {
5606
- const budgets = { ...base, ...overrides };
5607
- const breaches = [];
5608
- for (const metric of BUDGET_KEYS) {
5609
- const budget = budgets[metric];
5610
- if (budget === undefined)
6110
+ function descendantTexts(el, platform, out) {
6111
+ for (const child of el.children) {
6112
+ if (child.hidden)
5611
6113
  continue;
5612
- const value = measured(report, metric);
5613
- if (value !== undefined && value > budget)
5614
- breaches.push({ metric, value, budget });
6114
+ if (child.a11y?.important === false && child.children.length === 0)
6115
+ continue;
6116
+ const name = clean(child.label) ?? clean(child.text) ?? (child.role === "text" ? clean(child.value) : undefined);
6117
+ if (name)
6118
+ out.push(name);
6119
+ else
6120
+ descendantTexts(child, platform, out);
5615
6121
  }
5616
- return breaches;
5617
6122
  }
5618
- var round = (n) => Math.round(n * 10) / 10;
5619
- function describeBreaches(breaches, reportPath) {
5620
- const lines = breaches.map((b) => ` ${b.metric}: ${round(b.value)} > budget ${b.budget}`);
5621
- return [
5622
- `perf_budget: ${breaches.length} metric${breaches.length === 1 ? "" : "s"} over budget`,
5623
- ...lines,
5624
- ...reportPath === undefined ? [] : [` report: ${reportPath}`]
5625
- ].join(`
5626
- `);
6123
+ function hasActionableDescendant(el, platform) {
6124
+ return el.children.some((c) => !c.hidden && (isActionable(c, platform) || hasActionableDescendant(c, platform)));
5627
6125
  }
5628
- function parseBudgets(raw) {
5629
- if (raw === undefined || raw === "")
5630
- return {};
5631
- let json;
5632
- try {
5633
- json = JSON.parse(raw);
5634
- } catch {
5635
- return {};
6126
+ function accessibleName(el, platform) {
6127
+ const own = ownName(el, platform);
6128
+ if (own)
6129
+ return own;
6130
+ if (!isFocusTarget(el, platform) && el.role !== "text")
6131
+ return;
6132
+ if (el.children.length === 0 || hasActionableDescendant(el, platform))
6133
+ return;
6134
+ const parts = [];
6135
+ descendantTexts(el, platform, parts);
6136
+ return parts.length ? parts.join(platform === "ios" ? " " : ", ") : undefined;
6137
+ }
6138
+ function switchState(el) {
6139
+ const v = clean(el.value)?.toLowerCase();
6140
+ if (v && ON_VALUES.has(v))
6141
+ return "on";
6142
+ if (v && OFF_VALUES.has(v))
6143
+ return "off";
6144
+ if (el.checked !== undefined)
6145
+ return el.checked ? "on" : "off";
6146
+ if (has(el, "checked"))
6147
+ return "on";
6148
+ return;
6149
+ }
6150
+ function iosRoleWord(el) {
6151
+ if (has(el, "header"))
6152
+ return "heading";
6153
+ if (has(el, "adjustable") || el.role === "slider")
6154
+ return "adjustable";
6155
+ if (has(el, "link") || el.role === "link")
6156
+ return "link";
6157
+ switch (el.role) {
6158
+ case "switch":
6159
+ return "switch button";
6160
+ case "checkbox":
6161
+ return "checkbox";
6162
+ case "tab":
6163
+ return "tab";
6164
+ case "textfield":
6165
+ return "text field";
6166
+ case "securefield":
6167
+ return "secure text field";
6168
+ case "searchfield":
6169
+ return "search field";
6170
+ case "image":
6171
+ return "image";
6172
+ case "button":
6173
+ return "button";
6174
+ default:
5636
6175
  }
5637
- if (typeof json !== "object" || json === null)
5638
- return {};
5639
- const out = {};
5640
- for (const key of BUDGET_KEYS) {
5641
- const v = json[key];
5642
- if (typeof v === "number" && Number.isFinite(v) && v >= 0)
5643
- out[key] = v;
6176
+ if (has(el, "button"))
6177
+ return "button";
6178
+ if (has(el, "image"))
6179
+ return "image";
6180
+ return;
6181
+ }
6182
+ function androidRoleWord(el) {
6183
+ if (has(el, "heading") || has(el, "header"))
6184
+ return "heading";
6185
+ if (has(el, "link") || el.role === "link")
6186
+ return "link";
6187
+ switch (el.role) {
6188
+ case "switch":
6189
+ return "switch";
6190
+ case "checkbox":
6191
+ return "checkbox";
6192
+ case "textfield":
6193
+ case "securefield":
6194
+ case "searchfield":
6195
+ return "edit box";
6196
+ case "image":
6197
+ return "image";
6198
+ case "button":
6199
+ return "button";
6200
+ case "tab":
6201
+ return "tab";
6202
+ case "slider":
6203
+ return "seek control";
6204
+ default:
5644
6205
  }
5645
- return out;
6206
+ if (has(el, "checkable"))
6207
+ return "checkbox";
6208
+ return;
6209
+ }
6210
+ function expandedState(el) {
6211
+ if (has(el, "expanded"))
6212
+ return "expanded";
6213
+ if (has(el, "collapsed"))
6214
+ return "collapsed";
6215
+ return;
6216
+ }
6217
+ function join2(parts) {
6218
+ return parts.filter((p) => !!p).join(", ");
6219
+ }
6220
+ function announceIos(el, opts) {
6221
+ const label = accessibleName(el, "ios");
6222
+ const toggle = el.role === "switch" || el.role === "checkbox" ? switchState(el) : undefined;
6223
+ const rawValue = clean(el.value);
6224
+ let value;
6225
+ if (el.role === "securefield")
6226
+ value = undefined;
6227
+ else if (isInput(el))
6228
+ value = rawValue ?? clean(el.placeholder);
6229
+ else if (!toggle && el.role !== "text" && rawValue && rawValue !== label)
6230
+ value = rawValue;
6231
+ const role = iosRoleWord(el);
6232
+ const state = [];
6233
+ if (toggle)
6234
+ state.push(toggle);
6235
+ if (el.selected || has(el, "selected"))
6236
+ state.push("selected");
6237
+ const expanded = expandedState(el);
6238
+ if (expanded)
6239
+ state.push(expanded);
6240
+ if (!el.enabled || has(el, "not-enabled"))
6241
+ state.push("dimmed");
6242
+ const hint = opts.hints === false ? undefined : clean(el.a11y?.hint);
6243
+ return build(el, "ios", { label, value, role, state, hint }, join2([label, value, role, ...state, hint]));
6244
+ }
6245
+ function announceAndroid(el, opts) {
6246
+ const name = accessibleName(el, "android");
6247
+ const state = [];
6248
+ const toggle = switchState(el);
6249
+ if (el.role === "switch") {
6250
+ if (toggle)
6251
+ state.push(toggle);
6252
+ } else if (el.role === "checkbox" || has(el, "checkable")) {
6253
+ const checked = el.checked ?? has(el, "checked");
6254
+ state.push(checked ? "checked" : "not checked");
6255
+ }
6256
+ if (el.selected || has(el, "selected"))
6257
+ state.push("selected");
6258
+ const expanded = expandedState(el);
6259
+ if (expanded)
6260
+ state.push(expanded);
6261
+ const role = androidRoleWord(el);
6262
+ const disabled = !el.enabled;
6263
+ const hint = opts.hints === false ? undefined : clean(el.a11y?.hint);
6264
+ const text = join2([...state, name, role, disabled ? "disabled" : undefined, hint]);
6265
+ return build(el, "android", { label: name, role, state: disabled ? [...state, "disabled"] : state, hint }, text);
6266
+ }
6267
+ function build(el, platform, parts, text) {
6268
+ const { label, value, role, state, hint } = parts;
6269
+ return {
6270
+ id: el.id,
6271
+ ...el.identifier ? { identifier: el.identifier } : {},
6272
+ text,
6273
+ role: el.role,
6274
+ platform,
6275
+ source: "simulated",
6276
+ parts: {
6277
+ ...label ? { label } : {},
6278
+ ...value ? { value } : {},
6279
+ ...role ? { role } : {},
6280
+ state,
6281
+ ...hint ? { hint } : {}
6282
+ }
6283
+ };
6284
+ }
6285
+ function announce(el, platform, opts = {}) {
6286
+ return platform === "ios" ? announceIos(el, opts) : announceAndroid(el, opts);
5646
6287
  }
5647
6288
 
5648
- // ../../packages/e2e/src/perf/device-perf.ts
5649
- var PERF_DIR = "perf";
5650
- function parseEnvelope2(stdout) {
5651
- try {
5652
- const json = JSON.parse(stdout.trim().split(`
5653
- `).at(-1) ?? "");
5654
- if (typeof json !== "object" || json === null || !("ok" in json))
5655
- return;
5656
- return json;
5657
- } catch {
6289
+ // ../../packages/e2e/src/a11y/order.ts
6290
+ function onScreen(el, screen) {
6291
+ const f = el.frame;
6292
+ if (f.width <= 0 || f.height <= 0)
6293
+ return false;
6294
+ return f.x < screen.width && f.y < screen.height && f.x + f.width > 0 && f.y + f.height > 0;
6295
+ }
6296
+ function verticalOverlap(a, b) {
6297
+ const top = Math.max(a.frame.y, b.frame.y);
6298
+ const bottom = Math.min(a.frame.y + a.frame.height, b.frame.y + b.frame.height);
6299
+ return bottom - top;
6300
+ }
6301
+ function rowMajor(items) {
6302
+ const byTop = [...items].sort((a, b) => a.frame.y - b.frame.y || a.frame.x - b.frame.x);
6303
+ const rows = [];
6304
+ for (const el of byTop) {
6305
+ const row = rows.find((r) => {
6306
+ const anchor = r[0];
6307
+ return anchor !== undefined && verticalOverlap(anchor, el) > 0.5 * Math.min(anchor.frame.height, el.frame.height);
6308
+ });
6309
+ if (row)
6310
+ row.push(el);
6311
+ else
6312
+ rows.push([el]);
6313
+ }
6314
+ return rows.flatMap((r) => r.sort((a, b) => a.frame.x - b.frame.x));
6315
+ }
6316
+ function orderSiblings(children, platform) {
6317
+ if (platform === "android")
6318
+ return children;
6319
+ const explicit = children.filter((c) => c.a11y?.order !== undefined);
6320
+ if (explicit.length > 0) {
6321
+ const rest = children.filter((c) => c.a11y?.order === undefined);
6322
+ return [...explicit.sort((a, b) => (a.a11y?.order ?? 0) - (b.a11y?.order ?? 0)), ...rowMajor(rest)];
6323
+ }
6324
+ return rowMajor(children);
6325
+ }
6326
+ function hasActionableDescendant2(el, platform) {
6327
+ return el.children.some((c) => !c.hidden && (isActionable(c, platform) || hasActionableDescendant2(c, platform)));
6328
+ }
6329
+ function hasSpokenDescendant(el, platform) {
6330
+ return el.children.some((c) => !c.hidden && (isActionable(c, platform) || accessibleName(c, platform) !== undefined || hasSpokenDescendant(c, platform)));
6331
+ }
6332
+ var CONTAINERS = new Set([
6333
+ "application",
6334
+ "window",
6335
+ "toolbar",
6336
+ "navbar",
6337
+ "tabbar",
6338
+ "list",
6339
+ "scrollview"
6340
+ ]);
6341
+ function swallows(el, platform, actionable) {
6342
+ if (actionable)
6343
+ return !hasActionableDescendant2(el, platform);
6344
+ return el.a11y?.focusable === true || !hasSpokenDescendant(el, platform);
6345
+ }
6346
+ function walk(el, platform, screen, opts, out) {
6347
+ if (el.hidden)
5658
6348
  return;
6349
+ const visible = el.a11y?.important !== false && onScreen(el, screen);
6350
+ if (visible) {
6351
+ const actionable = isActionable(el, platform);
6352
+ const container = CONTAINERS.has(el.role) && !actionable;
6353
+ const named = accessibleName(el, platform) !== undefined;
6354
+ if (!container && (actionable || named)) {
6355
+ if (swallows(el, platform, actionable)) {
6356
+ out.push({ el, announcement: announce(el, platform, opts) });
6357
+ return;
6358
+ }
6359
+ if (named)
6360
+ out.push({ el, announcement: announce(el, platform, opts) });
6361
+ }
5659
6362
  }
6363
+ for (const child of orderSiblings(el.children, platform))
6364
+ walk(child, platform, screen, opts, out);
5660
6365
  }
5661
- var flagsOf = (o) => [
5662
- ...o.js ? ["--js"] : [],
5663
- ...o.react ? ["--react"] : [],
5664
- ...typeof o.native === "string" ? [`--native=${o.native}`] : o.native ? ["--native"] : [],
5665
- ...o.metrics ? ["--metrics"] : [],
5666
- ...o.frames ? ["--frames"] : [],
5667
- ...o.logs ? ["--logs"] : [],
5668
- ...o.all ? ["--all"] : [],
5669
- ...o.relaunch ? ["--relaunch"] : [],
5670
- ...o.reset ? ["--reset"] : [],
5671
- ...o.maxDurationMs === undefined ? [] : ["--max-duration", `${Math.ceil(o.maxDurationMs / 1000)}s`]
5672
- ];
5673
- var isRecord = (v) => typeof v === "object" && v !== null;
5674
- function saveReport(deps, result, report, sessionId) {
5675
- const sessionReport = typeof result.reportPath === "string" ? result.reportPath : undefined;
5676
- const attempt = deps.attemptDir();
5677
- if (attempt === undefined)
5678
- return sessionReport;
5679
- const out = join2(attempt, PERF_DIR);
5680
- mkdirSync3(out, { recursive: true });
5681
- const sessionDir = typeof result.dir === "string" ? result.dir : undefined;
5682
- const write = (name, from, fallback) => {
5683
- if (from !== undefined && existsSync3(from))
5684
- copyFileSync(from, join2(out, name));
5685
- else if (fallback !== undefined)
5686
- writeFileSync(join2(out, name), fallback);
5687
- else
5688
- return;
5689
- deps.addFile?.(`${PERF_DIR}/${name}`);
5690
- };
5691
- write("report.json", sessionReport, `${JSON.stringify(report, null, 2)}
5692
- `);
5693
- write("report.md", sessionDir === undefined ? undefined : join2(sessionDir, "report.md"));
5694
- write("session.json", undefined, `${JSON.stringify({ sessionId, dir: sessionDir }, null, 2)}
5695
- `);
5696
- return join2(out, "report.json");
6366
+ function readingOrder(snapshot, platform, opts = {}) {
6367
+ const out = [];
6368
+ for (const el of orderSiblings(snapshot.elements, platform))
6369
+ walk(el, platform, snapshot.screen, opts, out);
6370
+ return out.map((e) => e.announcement);
5697
6371
  }
5698
- function createDevicePerf(deps) {
5699
- const run = async (sub, extra) => {
5700
- if (deps.unavailable !== undefined)
5701
- throw new E2eError2("SESSION", `device.perf unavailable: ${deps.unavailable}`);
5702
- const { code, stdout, stderr } = await deps.exec(["perf", sub, "--json", "--udid", deps.udid, ...extra]);
6372
+
6373
+ // ../../packages/e2e/src/a11y/rules/rule.context.ts
6374
+ function unitsPerPoint(snapshot) {
6375
+ return snapshot.platform === "android" ? snapshot.screen.scale || 1 : 1;
6376
+ }
6377
+ function isAncestor(a, b, parents) {
6378
+ for (let p = parents.get(b);p; p = parents.get(p))
6379
+ if (p === a)
6380
+ return true;
6381
+ return false;
6382
+ }
6383
+ function related(a, b, parents) {
6384
+ return a === b || isAncestor(a, b, parents) || isAncestor(b, a, parents);
6385
+ }
6386
+ function issue(rule, severity, el, message, wcag) {
6387
+ return {
6388
+ rule,
6389
+ severity,
6390
+ source: "warden",
6391
+ elementId: el.id,
6392
+ message,
6393
+ ...wcag === undefined ? {} : { wcag },
6394
+ ...el.identifier === undefined ? {} : { identifier: el.identifier }
6395
+ };
6396
+ }
6397
+ function describe(el) {
6398
+ const what = el.identifier ?? el.label ?? el.text ?? el.rawType;
6399
+ return `${el.role} "${what}"`;
6400
+ }
6401
+
6402
+ // ../../packages/e2e/src/a11y/rules/duplicate-name.ts
6403
+ var key = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
6404
+ var sameFrame2 = (a, b) => a.frame.x === b.frame.x && a.frame.y === b.frame.y && a.frame.width === b.frame.width && a.frame.height === b.frame.height;
6405
+ var duplicateName = (_snapshot, ctx) => {
6406
+ const named = ctx.reachable.flatMap((el) => {
6407
+ if (!isActionable(el, ctx.platform))
6408
+ return [];
6409
+ const name = accessibleName(el, ctx.platform);
6410
+ return name === undefined ? [] : [{ el, name: key(name), raw: name }];
6411
+ });
6412
+ const out = [];
6413
+ const kept = [];
6414
+ for (const n of named) {
6415
+ const first = kept.find((k) => k.name === n.name && !sameFrame2(k.el, n.el) && !related(k.el, n.el, ctx.parents) && !(k.el.identifier !== undefined && k.el.identifier === n.el.identifier));
6416
+ if (first) {
6417
+ out.push(issue("duplicate-name", "warning", n.el, `${describe(n.el)} is announced "${n.raw}", the same as ${describe(first.el)}`, "2.4.6"));
6418
+ } else
6419
+ kept.push(n);
6420
+ }
6421
+ return out;
6422
+ };
6423
+
6424
+ // ../../packages/e2e/src/a11y/rules/hidden-focusable.ts
6425
+ function offScreen(el, screen) {
6426
+ const f = el.frame;
6427
+ return f.x >= screen.width || f.y >= screen.height || f.x + f.width <= 0 || f.y + f.height <= 0;
6428
+ }
6429
+ var hiddenFocusable = (snapshot) => snapshot.flat.filter((el) => el.a11y?.focusable === true && el.a11y.important !== false).flatMap((el) => {
6430
+ const why = el.hidden ? "hidden" : el.frame.width <= 0 || el.frame.height <= 0 ? "zero-size" : offScreen(el, snapshot.screen) ? "off screen" : undefined;
6431
+ return why ? [issue("hidden-focusable", "warning", el, `${describe(el)} is ${why} but still focusable`)] : [];
6432
+ });
6433
+
6434
+ // ../../packages/e2e/src/a11y/rules/image-no-name.ts
6435
+ var imageNoName = (_snapshot, ctx) => ctx.reachable.filter((el) => el.role === "image" && accessibleName(el, ctx.platform) === undefined).map((el) => issue("image-no-name", "error", el, `${describe(el)} has no name (add a label, or mark it decorative)`, "1.1.1"));
6436
+
6437
+ // ../../packages/e2e/src/a11y/rules/label-in-name.ts
6438
+ var norm = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
6439
+ function visibleText(el, platform) {
6440
+ const own = platform === "android" ? el.text?.trim() : undefined;
6441
+ if (own)
6442
+ return own;
6443
+ const parts = [];
6444
+ const walk = (e) => {
6445
+ for (const c of e.children) {
6446
+ if (c.hidden)
6447
+ continue;
6448
+ if (c.role === "text") {
6449
+ const t = (c.text ?? c.label ?? c.value)?.trim();
6450
+ if (t)
6451
+ parts.push(t);
6452
+ } else
6453
+ walk(c);
6454
+ }
6455
+ };
6456
+ walk(el);
6457
+ return parts.join(" ");
6458
+ }
6459
+ var labelInName = (_snapshot, ctx) => ctx.reachable.flatMap((el) => {
6460
+ if (!isActionable(el, ctx.platform) || !el.label?.trim())
6461
+ return [];
6462
+ const visible = visibleText(el, ctx.platform);
6463
+ const name = accessibleName(el, ctx.platform);
6464
+ if (!visible || !name || norm(name).includes(norm(visible)))
6465
+ return [];
6466
+ return [issue("label-in-name", "error", el, `${describe(el)} shows "${visible}" but is named "${name}"`, "2.5.3")];
6467
+ });
6468
+
6469
+ // ../../packages/e2e/src/a11y/rules/missing-name.ts
6470
+ function isScrollContainer(el, platform) {
6471
+ const scrolls = el.role === "scrollview" || el.role === "list" || el.traits.includes("scrollable");
6472
+ return scrolls && !isActionable(el, platform);
6473
+ }
6474
+ var missingName = (_snapshot, ctx) => ctx.reachable.filter((el) => isFocusTarget(el, ctx.platform) && !isScrollContainer(el, ctx.platform) && accessibleName(el, ctx.platform) === undefined).map((el) => issue("missing-name", "error", el, `${describe(el)} has no accessible name`, "4.1.2"));
6475
+
6476
+ // ../../packages/e2e/src/a11y/rules/role-in-name.ts
6477
+ var WORDS = {
6478
+ button: /\b(button|btn)\b/i,
6479
+ image: /\b(image|icon)\b/i
6480
+ };
6481
+ var roleInName = (_snapshot, ctx) => ctx.reachable.flatMap((el) => {
6482
+ const pattern = WORDS[el.role];
6483
+ const name = pattern ? accessibleName(el, ctx.platform) : undefined;
6484
+ const hit = name ? pattern?.exec(name)?.[0] : undefined;
6485
+ return hit ? [
6486
+ issue("role-in-name", "warning", el, `${describe(el)} name "${name}" contains its role word "${hit}", announced twice`)
6487
+ ] : [];
6488
+ });
6489
+
6490
+ // ../../packages/e2e/src/a11y/rules/target-size.ts
6491
+ var WCAG_MIN = 24;
6492
+ var IOS_MIN = 44;
6493
+ var ANDROID_MIN = 48;
6494
+ var RADIUS = WCAG_MIN / 2;
6495
+ function targets(snapshot, ctx) {
6496
+ const k = unitsPerPoint(snapshot);
6497
+ return ctx.reachable.filter((el) => el.enabled && isActionable(el, ctx.platform) && el.frame.width > 0 && el.frame.height > 0).map((el) => ({ el, x: el.frame.x / k, y: el.frame.y / k, w: el.frame.width / k, h: el.frame.height / k }));
6498
+ }
6499
+ function centre(b) {
6500
+ return { x: b.x + b.w / 2, y: b.y + b.h / 2 };
6501
+ }
6502
+ function distanceToBox(p, b) {
6503
+ const dx = Math.max(b.x - p.x, 0, p.x - (b.x + b.w));
6504
+ const dy = Math.max(b.y - p.y, 0, p.y - (b.y + b.h));
6505
+ return Math.hypot(dx, dy);
6506
+ }
6507
+ var undersized = (b) => b.w < WCAG_MIN || b.h < WCAG_MIN;
6508
+ function wcagFailures(snapshot, ctx) {
6509
+ const all = targets(snapshot, ctx);
6510
+ return all.filter((b) => {
6511
+ if (!undersized(b))
6512
+ return false;
6513
+ const c = centre(b);
6514
+ return all.some((o) => {
6515
+ if (related(b.el, o.el, ctx.parents))
6516
+ return false;
6517
+ if (undersized(o))
6518
+ return Math.hypot(centre(o).x - c.x, centre(o).y - c.y) < RADIUS * 2;
6519
+ return distanceToBox(c, o) < RADIUS;
6520
+ });
6521
+ });
6522
+ }
6523
+ var fmt = (n) => String(Math.round(n * 10) / 10);
6524
+ var targetSize = (snapshot, ctx) => {
6525
+ const unit = ctx.platform === "ios" ? "pt" : "dp";
6526
+ return wcagFailures(snapshot, ctx).map((b) => issue("target-size", "error", b.el, `${describe(b.el)} is ${fmt(b.w)}x${fmt(b.h)}${unit}, under ${WCAG_MIN}x${WCAG_MIN}${unit} with another target within ${WCAG_MIN}${unit}`, "2.5.8"));
6527
+ };
6528
+ var targetSizePlatform = (snapshot, ctx) => {
6529
+ const min = ctx.platform === "ios" ? IOS_MIN : ANDROID_MIN;
6530
+ const unit = ctx.platform === "ios" ? "pt" : "dp";
6531
+ const failing = new Set(wcagFailures(snapshot, ctx).map((b) => b.el));
6532
+ return targets(snapshot, ctx).filter((b) => (b.w < min || b.h < min) && !failing.has(b.el)).map((b) => issue("target-size-platform", "warning", b.el, `${describe(b.el)} is ${fmt(b.w)}x${fmt(b.h)}${unit}, under the platform minimum ${min}${unit}`));
6533
+ };
6534
+
6535
+ // ../../packages/e2e/src/a11y/rules/text-clipped.ts
6536
+ var CLIPPING_MIN_SCALE = 1.6;
6537
+ var ELLIPSIS = /(\u2026|\.\.\.)$/;
6538
+ var TOLERANCE = 1;
6539
+ function overflows(el, parent) {
6540
+ const f = el.frame;
6541
+ const p = parent.frame;
6542
+ return f.x < p.x - TOLERANCE || f.y < p.y - TOLERANCE || f.x + f.width > p.x + p.width + TOLERANCE || f.y + f.height > p.y + p.height + TOLERANCE;
6543
+ }
6544
+ var textClipped = (snapshot, ctx) => {
6545
+ if (ctx.textScale === undefined || ctx.textScale < CLIPPING_MIN_SCALE)
6546
+ return [];
6547
+ return ctx.reachable.flatMap((el) => {
6548
+ if (el.role !== "text")
6549
+ return [];
6550
+ const content = (el.text ?? el.label ?? el.value)?.trim();
6551
+ if (content && ELLIPSIS.test(content))
6552
+ return [issue("text-clipped", "warning", el, `${describe(el)} is truncated ("${content}")`, "1.4.4")];
6553
+ const parent = ctx.parents.get(el);
6554
+ if (!parent || parent.role === "scrollview" || parent.role === "list" || parent.traits.includes("scrollable"))
6555
+ return [];
6556
+ if (parent.frame.width >= snapshot.screen.width && parent.frame.height >= snapshot.screen.height)
6557
+ return [];
6558
+ return overflows(el, parent) ? [issue("text-clipped", "warning", el, `${describe(el)} overflows its container`, "1.4.4")] : [];
6559
+ });
6560
+ };
6561
+
6562
+ // ../../packages/e2e/src/a11y/audit.ts
6563
+ var WARDEN_RULES = [
6564
+ { id: "missing-name", run: missingName },
6565
+ { id: "image-no-name", run: imageNoName },
6566
+ { id: "label-in-name", run: labelInName },
6567
+ { id: "target-size", run: targetSize },
6568
+ { id: "target-size-platform", run: targetSizePlatform },
6569
+ { id: "duplicate-name", run: duplicateName },
6570
+ { id: "role-in-name", run: roleInName },
6571
+ { id: "hidden-focusable", run: hiddenFocusable },
6572
+ { id: "text-clipped", run: textClipped }
6573
+ ];
6574
+ function reachableNodes(snapshot) {
6575
+ const reachable = [];
6576
+ const parents = new Map;
6577
+ const walk = (el, parent) => {
6578
+ if (parent)
6579
+ parents.set(el, parent);
6580
+ if (el.hidden)
6581
+ return;
6582
+ if (el.a11y?.important !== false)
6583
+ reachable.push(el);
6584
+ for (const c of el.children)
6585
+ walk(c, el);
6586
+ };
6587
+ for (const el of snapshot.elements)
6588
+ walk(el, undefined);
6589
+ return { reachable, parents };
6590
+ }
6591
+ var SEVERITY_RANK = { warning: 0, error: 1 };
6592
+ var ruleRank = (rule) => {
6593
+ const i = WARDEN_RULES.findIndex((r) => r.id === rule);
6594
+ return i === -1 ? WARDEN_RULES.length : i;
6595
+ };
6596
+ function positions(snapshot, platform) {
6597
+ const listed = new Map;
6598
+ readingOrder(snapshot, platform).forEach((a, i) => {
6599
+ listed.set(a.id, i);
6600
+ });
6601
+ const pos = new Map;
6602
+ let last = -1;
6603
+ snapshot.flat.forEach((el, i) => {
6604
+ const at = listed.get(el.id);
6605
+ if (at !== undefined) {
6606
+ pos.set(el.id, at);
6607
+ last = at;
6608
+ } else
6609
+ pos.set(el.id, last + 0.5 + i / 1e6);
6610
+ });
6611
+ return pos;
6612
+ }
6613
+ function runAudit(snapshot, platform, opts = {}) {
6614
+ const { reachable, parents } = reachableNodes(snapshot);
6615
+ const resolved = opts.textSize === undefined ? undefined : resolveTextSize(opts.textSize);
6616
+ const ctx = {
6617
+ platform,
6618
+ reachable,
6619
+ parents,
6620
+ ...resolved?.success ? { textScale: resolved.data.scale } : {}
6621
+ };
6622
+ const wanted = opts.rules === undefined ? undefined : new Set(opts.rules);
6623
+ const found = WARDEN_RULES.filter((r) => wanted === undefined || wanted.has(r.id)).flatMap((r) => r.run(snapshot, ctx));
6624
+ const ignore = opts.ignore ?? [];
6625
+ const kept = found.filter((i) => !ignore.some((g) => g.rule === i.rule && (g.identifier === undefined || g.identifier === i.identifier)));
6626
+ const seen = new Set;
6627
+ const unique = kept.filter((i) => {
6628
+ const k = `${i.rule}\x00${i.elementId ?? ""}\x00${i.message}`;
6629
+ if (seen.has(k))
6630
+ return false;
6631
+ seen.add(k);
6632
+ return true;
6633
+ });
6634
+ const pos = positions(snapshot, platform);
6635
+ const at = (i) => i.elementId === undefined ? Number.MAX_SAFE_INTEGER : pos.get(i.elementId) ?? 0;
6636
+ const issues = unique.sort((a, b) => at(a) - at(b) || ruleRank(a.rule) - ruleRank(b.rule));
6637
+ return { issues, checked: reachable.length, sources: ["warden"], skipped: [] };
6638
+ }
6639
+ function failing(result, minSeverity = "error") {
6640
+ return result.issues.filter((i) => SEVERITY_RANK[i.severity] >= SEVERITY_RANK[minSeverity]);
6641
+ }
6642
+ function elementKey(i) {
6643
+ return i.identifier ?? i.elementId;
6644
+ }
6645
+ function mergeIssues(base, extra, skipped = []) {
6646
+ const issues = [...base.issues];
6647
+ const added = [];
6648
+ for (const e of extra) {
6649
+ const key = elementKey(e);
6650
+ const idx = issues.findIndex((i) => key !== undefined && elementKey(i) === key && (e.wcag !== undefined ? i.wcag === e.wcag : i.rule === e.rule && i.message === e.message));
6651
+ const hit = idx === -1 ? undefined : issues[idx];
6652
+ if (hit === undefined) {
6653
+ added.push(e);
6654
+ } else if (hit.source !== e.source && !hit.message.includes(`also reported by ${e.source}`)) {
6655
+ issues[idx] = { ...hit, message: `${hit.message} (also reported by ${e.source})` };
6656
+ }
6657
+ }
6658
+ const sources = [...new Set([...base.sources, ...extra.map((e) => e.source)])];
6659
+ return {
6660
+ issues: [...issues, ...added],
6661
+ checked: base.checked,
6662
+ sources,
6663
+ skipped: [...base.skipped, ...skipped]
6664
+ };
6665
+ }
6666
+ function formatIssues(result, minSeverity = "warning") {
6667
+ const shown = failing(result, minSeverity);
6668
+ const head = `a11y audit: ${shown.length} issue${shown.length === 1 ? "" : "s"} in ${result.checked} elements (${result.sources.join(", ")})`;
6669
+ const skipped = result.skipped.map((s) => `skipped ${s.source}: ${s.reason}`);
6670
+ if (shown.length === 0)
6671
+ return [head, ...skipped].join(`
6672
+ `);
6673
+ const rows = shown.map((i) => [i.severity, i.rule, i.wcag ?? "-", i.identifier ?? i.elementId ?? "-", i.message]);
6674
+ const widths = [0, 1, 2, 3].map((c) => Math.max(...rows.map((r) => r[c]?.length ?? 0)));
6675
+ const lines = rows.map((r) => `${r.map((cell, c) => c < 4 ? cell.padEnd(widths[c] ?? 0) : cell).join(" ")}`);
6676
+ return [head, ...lines, ...skipped].join(`
6677
+ `);
6678
+ }
6679
+
6680
+ // ../../packages/e2e/src/driver/unwrap.ts
6681
+ var TIMEOUTS = new Set(["E_TIMEOUT", "E_APP_NOT_RESPONDING"]);
6682
+ var INFRASTRUCTURE = new Set([
6683
+ "E_HELPER_MISSING",
6684
+ "E_HELPER_CRASHED",
6685
+ "E_NOT_BOOTED",
6686
+ "E_UNKNOWN_DEVICE",
6687
+ "E_AX_UNAVAILABLE",
6688
+ "E_APP_UNAVAILABLE",
6689
+ "E_INTERNAL",
6690
+ "E_DEVICE_LOCKED"
6691
+ ]);
6692
+ function codeFor(code) {
6693
+ if (TIMEOUTS.has(code))
6694
+ return "TIMEOUT";
6695
+ if (INFRASTRUCTURE.has(code))
6696
+ return "DRIVER";
6697
+ return "ACTION_FAILED";
6698
+ }
6699
+ function toE2eError(error, action) {
6700
+ return new E2eError2(codeFor(error.code), `${action} failed: ${error.code}: ${error.message}`);
6701
+ }
6702
+ function aborted2(action, signal) {
6703
+ const reason = signal.reason;
6704
+ const detail = reason instanceof Error ? `: ${reason.message}` : "";
6705
+ return new E2eError2("TIMEOUT", `${action} aborted (test deadline reached or cancelled)${detail}`);
6706
+ }
6707
+ async function settled(call, action, signal) {
6708
+ if (signal === undefined)
6709
+ return call();
6710
+ if (signal.aborted)
6711
+ throw aborted2(action, signal);
6712
+ let onAbort;
6713
+ const abort = new Promise((_, reject) => {
6714
+ onAbort = () => reject(aborted2(action, signal));
6715
+ signal.addEventListener("abort", onAbort, { once: true });
6716
+ });
6717
+ try {
6718
+ return await Promise.race([call(), abort]);
6719
+ } finally {
6720
+ if (onAbort !== undefined)
6721
+ signal.removeEventListener("abort", onAbort);
6722
+ }
6723
+ }
6724
+ async function unwrap(call, action, signal) {
6725
+ const result = await settled(call, action, signal);
6726
+ if (!result.success)
6727
+ throw toE2eError(result.error, action);
6728
+ return result.data;
6729
+ }
6730
+
6731
+ // ../../packages/e2e/src/driver/a11y-events.ts
6732
+ function canReadA11yEvents(driver) {
6733
+ return typeof driver.a11yEvents === "function";
6734
+ }
6735
+ var KINDS = new Set([
6736
+ "announcement",
6737
+ "live-region",
6738
+ "screen-changed",
6739
+ "layout-changed"
6740
+ ]);
6741
+ function isRecord(value) {
6742
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6743
+ }
6744
+ var SYSTEM_ANNOUNCEMENT = /^AXAnnouncementType/;
6745
+ function parseA11yEvent(value, source) {
6746
+ if (!isRecord(value) || typeof value.kind !== "string" || !KINDS.has(value.kind))
6747
+ return;
6748
+ if (typeof value.at !== "number" || !Number.isFinite(value.at))
6749
+ return;
6750
+ if (value.text !== undefined && typeof value.text !== "string")
6751
+ return;
6752
+ return {
6753
+ kind: value.kind,
6754
+ ...typeof value.text === "string" && value.text !== "" ? { text: value.text } : {},
6755
+ at: value.at,
6756
+ source
6757
+ };
6758
+ }
6759
+ function parseUiadA11yEvents(data) {
6760
+ if (!isRecord(data) || !Array.isArray(data.events) || typeof data.cursor !== "number") {
6761
+ return { success: false, error: "warden-uiad answered a11yEvents with an unexpected shape" };
6762
+ }
6763
+ const events = [];
6764
+ for (const [index, raw] of data.events.entries()) {
6765
+ const event = parseA11yEvent(raw, "uiad");
6766
+ if (!event)
6767
+ return { success: false, error: `warden-uiad sent a malformed a11y event at ${index}` };
6768
+ events.push(event);
6769
+ }
6770
+ return { success: true, data: { events, cursor: data.cursor, ...data.lost === true ? { lost: true } : {} } };
6771
+ }
6772
+ function parseInjectEvents(text, since = 0) {
6773
+ const bytes = Buffer.from(text, "utf8");
6774
+ const start = since > bytes.length || since < 0 ? 0 : since;
6775
+ const end = bytes.lastIndexOf(10) + 1;
6776
+ if (end <= start)
6777
+ return { events: [], cursor: start };
6778
+ const events = [];
6779
+ for (const line of bytes.subarray(start, end).toString("utf8").split(`
6780
+ `)) {
6781
+ if (line.trim() === "")
6782
+ continue;
6783
+ try {
6784
+ const event = parseA11yEvent(JSON.parse(line), "inject");
6785
+ if (event && !(event.text !== undefined && SYSTEM_ANNOUNCEMENT.test(event.text)))
6786
+ events.push(event);
6787
+ } catch {}
6788
+ }
6789
+ return { events, cursor: end };
6790
+ }
6791
+
6792
+ // ../../packages/e2e/src/a11y/announcements.ts
6793
+ var ANNOUNCEMENT_TIMEOUT_MS = 5000;
6794
+ var ANNOUNCEMENT_POLL_MS = 100;
6795
+ var ANNOUNCING = new Set(["announcement", "live-region"]);
6796
+ function matches(event, match) {
6797
+ if (!ANNOUNCING.has(event.kind) || event.text === undefined)
6798
+ return false;
6799
+ if (typeof match === "string")
6800
+ return event.text.includes(match);
6801
+ match.lastIndex = 0;
6802
+ return match.test(event.text);
6803
+ }
6804
+ function withDefaults(opts) {
6805
+ return {
6806
+ now: opts.now ?? Date.now,
6807
+ sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
6808
+ timeoutMs: opts.timeoutMs ?? ANNOUNCEMENT_TIMEOUT_MS,
6809
+ pollMs: opts.pollMs ?? ANNOUNCEMENT_POLL_MS
6810
+ };
6811
+ }
6812
+ async function waitForAnnouncement(driver, match, opts = {}) {
6813
+ if (!canReadA11yEvents(driver)) {
6814
+ return {
6815
+ success: false,
6816
+ error: driverError("E_UNSUPPORTED", "this driver cannot read announcements (no a11yEvents capability)")
6817
+ };
6818
+ }
6819
+ const { now, sleep, timeoutMs, pollMs } = withDefaults(opts);
6820
+ const deadline = now() + timeoutMs;
6821
+ let cursor = opts.since;
6822
+ const seen = [];
6823
+ for (;; ) {
6824
+ const read = await driver.a11yEvents(cursor);
6825
+ if (!read.success)
6826
+ return read;
6827
+ cursor = read.data.cursor;
6828
+ const hit = read.data.events.find((event) => matches(event, match));
6829
+ if (hit)
6830
+ return { success: true, data: { event: hit, cursor: read.data.cursor } };
6831
+ for (const event of read.data.events)
6832
+ if (event.text !== undefined)
6833
+ seen.push(event.text);
6834
+ if (now() >= deadline) {
6835
+ return {
6836
+ success: false,
6837
+ error: driverError("E_TIMEOUT", `no announcement matched ${String(match)} within ${timeoutMs} ms`, {
6838
+ detail: { seen: seen.slice(-10), cursor }
6839
+ })
6840
+ };
6841
+ }
6842
+ await sleep(pollMs);
6843
+ }
6844
+ }
6845
+
6846
+ // ../../packages/e2e/src/driver/a11y-read.ts
6847
+ function canReadA11y(driver) {
6848
+ return typeof driver.readA11y === "function";
6849
+ }
6850
+
6851
+ // ../../packages/e2e/src/driver/native-audit.ts
6852
+ function canAuditNatively(driver) {
6853
+ return typeof driver.auditNatively === "function";
6854
+ }
6855
+
6856
+ // ../../packages/e2e/src/driver/screen-reader.ts
6857
+ function canDriveScreenReader(driver) {
6858
+ return typeof driver.screenReaderNext === "function";
6859
+ }
6860
+
6861
+ // ../../packages/e2e/src/device-a11y.ts
6862
+ var TALKBACK_HINT = "turn TalkBack on with device.a11y({ screenReader: true }) on an Android emulator";
6863
+ function unsupported(action, why) {
6864
+ return toE2eError(driverError("E_UNSUPPORTED", why), action);
6865
+ }
6866
+ async function withA11yRead(driver, snapshot) {
6867
+ if (!canReadA11y(driver))
6868
+ return { success: true, data: snapshot };
6869
+ const read = await driver.readA11y(snapshot);
6870
+ if (read.success)
6871
+ return read;
6872
+ if (read.error.code === "E_UNSUPPORTED")
6873
+ return { success: true, data: snapshot };
6874
+ return read;
6875
+ }
6876
+ var A11Y_EXCERPT_LINES = 40;
6877
+ function renderA11yScreen(snapshot, maxLines = A11Y_EXCERPT_LINES) {
6878
+ const rendered = renderSnapshot(snapshot, { interactiveOnly: true, refs: new Map, a11y: true, maxLines });
6879
+ return [renderHeader({ snapshot, rendered, interactiveOnly: true }), ...rendered.lines].join(`
6880
+ `);
6881
+ }
6882
+ function ignored(issue, ignore) {
6883
+ return ignore.some((g) => g.rule === issue.rule && (g.identifier === undefined || g.identifier === issue.identifier));
6884
+ }
6885
+ async function auditSnapshot(driver, snapshot, options = {}) {
6886
+ const { platform } = driver;
6887
+ const { rules, minSeverity } = options;
6888
+ const ignore = options.ignore ?? [];
6889
+ const own = runAudit(snapshot, platform, {
6890
+ ...rules === undefined ? {} : { rules },
6891
+ ignore,
6892
+ ...minSeverity === undefined ? {} : { minSeverity },
6893
+ ...options.textSize === undefined ? {} : { textSize: options.textSize }
6894
+ });
6895
+ if (options.native === false || rules !== undefined && !rules.includes("native"))
6896
+ return own;
6897
+ const source = platform === "ios" ? "xcuitest" : "atf";
6898
+ if (!canAuditNatively(driver)) {
6899
+ return mergeIssues(own, [], [{ source, reason: "this driver has no native accessibility audit" }]);
6900
+ }
6901
+ const native = await driver.auditNatively(options.bundleId === undefined ? {} : { bundleId: options.bundleId });
6902
+ return mergeIssues(own, native.issues.filter((issue) => !ignored(issue, ignore)), native.skipped === undefined ? [] : [native.skipped]);
6903
+ }
6904
+ function createDeviceA11y(deps) {
6905
+ const { driver, env } = deps;
6906
+ const signal = () => deps.signal();
6907
+ const { platform } = driver;
6908
+ const readSnapshot = async () => {
6909
+ const snapshot = await unwrap(() => driver.snapshot(), "snapshot", signal());
6910
+ const merged = await withA11yRead(driver, snapshot);
6911
+ if (!merged.success)
6912
+ throw toE2eError(merged.error, "read accessibility facts");
6913
+ return merged.data;
6914
+ };
6915
+ const audit = async (options = {}) => {
6916
+ const config = env.a11yAudit ?? {};
6917
+ const rules = options.rules ?? config.rules;
6918
+ const ignore = options.ignore ?? config.ignore ?? [];
6919
+ const minSeverity = options.minSeverity ?? config.minSeverity;
6920
+ return auditSnapshot(driver, await readSnapshot(), {
6921
+ ...rules === undefined ? {} : { rules },
6922
+ ignore,
6923
+ ...minSeverity === undefined ? {} : { minSeverity },
6924
+ ...options.textSize === undefined ? {} : { textSize: options.textSize },
6925
+ ...options.native === undefined ? {} : { native: options.native },
6926
+ ...env.bundleId === undefined ? {} : { bundleId: env.bundleId }
6927
+ });
6928
+ };
6929
+ const announcements = async (options = {}) => {
6930
+ if (!canReadA11yEvents(driver)) {
6931
+ throw unsupported("device.announcements", "this driver cannot read announcements (no a11yEvents capability)");
6932
+ }
6933
+ const { since } = options;
6934
+ const read = await unwrap(() => driver.a11yEvents(since), "read announcements", signal());
6935
+ const announced = read.events.filter((e) => (e.kind === "announcement" || e.kind === "live-region") && e.text !== undefined);
6936
+ return {
6937
+ announcements: announced.map((e) => ({ kind: e.kind, text: e.text, at: e.at, source: e.source })),
6938
+ cursor: read.cursor,
6939
+ ...read.lost === true ? { lost: true } : {}
6940
+ };
6941
+ };
6942
+ const reader = (action) => {
6943
+ if (!canDriveScreenReader(driver))
6944
+ throw unsupported(action, TALKBACK_HINT);
6945
+ return driver;
6946
+ };
6947
+ const spoken = (el) => el === undefined ? undefined : announce(el, platform);
6948
+ const screenReader = {
6949
+ async order() {
6950
+ return readingOrder(await readSnapshot(), platform);
6951
+ },
6952
+ async state() {
6953
+ const d = reader("device.screenReader.state");
6954
+ return unwrap(() => d.screenReaderState(), "screen reader state", signal());
6955
+ },
6956
+ async next() {
6957
+ const d = reader("device.screenReader.next");
6958
+ return spoken(await unwrap(() => d.screenReaderNext(), "screen reader next", signal()));
6959
+ },
6960
+ async prev() {
6961
+ const d = reader("device.screenReader.prev");
6962
+ return spoken(await unwrap(() => d.screenReaderPrev(), "screen reader prev", signal()));
6963
+ },
6964
+ async activate() {
6965
+ const d = reader("device.screenReader.activate");
6966
+ await unwrap(() => d.screenReaderActivate(), "screen reader activate", signal());
6967
+ },
6968
+ async focused() {
6969
+ const d = reader("device.screenReader.focused");
6970
+ return spoken(await unwrap(() => d.screenReaderFocused(), "screen reader focused", signal()));
6971
+ },
6972
+ async spoken(options = {}) {
6973
+ const d = reader("device.screenReader.spoken");
6974
+ const read = await unwrap(() => d.screenReaderSpoken(options.since), "screen reader spoken", signal());
6975
+ return { lines: read.lines, cursor: read.cursor };
6976
+ }
6977
+ };
6978
+ const target = {
6979
+ audit: (options) => audit(options),
6980
+ waitForAnnouncement(match, options) {
6981
+ return waitForAnnouncement(driver, match, {
6982
+ timeoutMs: options.timeoutMs,
6983
+ ...options.since === undefined ? {} : { since: options.since }
6984
+ });
6985
+ },
6986
+ async a11yScreen() {
6987
+ const taken = await settled(() => driver.snapshot(), "snapshot", signal());
6988
+ if (!taken.success)
6989
+ return;
6990
+ const merged = await withA11yRead(driver, taken.data);
6991
+ return renderA11yScreen(merged.success ? merged.data : taken.data);
6992
+ },
6993
+ assertionTimeout: () => env.assertionTimeoutMs,
6994
+ step: (label, fn) => deps.step === undefined ? fn() : deps.step(label, fn)
6995
+ };
6996
+ return { a11yAudit: audit, screenReader, announcements, target };
6997
+ }
6998
+
6999
+ // ../../packages/e2e/src/driver/ios/device/capabilities.ts
7000
+ var IOS_DEVICE_CAPABILITIES = {
7001
+ pinch: false,
7002
+ maxPointers: 0,
7003
+ unicodeText: true,
7004
+ nativeSettle: false,
7005
+ alerts: true,
7006
+ reachability: true,
7007
+ cheapHitTest: false
7008
+ };
7009
+
7010
+ // ../../packages/e2e/src/conditions/device-conditions.ts
7011
+ import { dirname as dirname5, resolve as resolve3 } from "path";
7012
+
7013
+ // ../../packages/core/src/conditions/camera-media.ts
7014
+ import { createHash } from "crypto";
7015
+ import { closeSync, copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readFileSync, readSync, rmSync } from "fs";
7016
+ import { join as join3 } from "path";
7017
+ var SUPPORTED = "png, jpg, heic, mp4, mov";
7018
+ var FTYP_BRANDS = {
7019
+ heic: { kind: "image", ext: "heic" },
7020
+ heix: { kind: "image", ext: "heic" },
7021
+ mif1: { kind: "image", ext: "heic" },
7022
+ "qt ": { kind: "video", ext: "mov" },
7023
+ isom: { kind: "video", ext: "mp4" },
7024
+ iso2: { kind: "video", ext: "mp4" },
7025
+ mp41: { kind: "video", ext: "mp4" },
7026
+ mp42: { kind: "video", ext: "mp4" },
7027
+ avc1: { kind: "video", ext: "mp4" },
7028
+ M4V: { kind: "video", ext: "mp4" },
7029
+ "M4V ": { kind: "video", ext: "mp4" }
7030
+ };
7031
+ function head2(path, n) {
7032
+ const fd = openSync(path, "r");
7033
+ try {
7034
+ const buf = Buffer.alloc(n);
7035
+ const read = readSync(fd, buf, 0, n, 0);
7036
+ return buf.subarray(0, read);
7037
+ } finally {
7038
+ closeSync(fd);
7039
+ }
7040
+ }
7041
+ function sniffCameraMedia(path) {
7042
+ if (!existsSync3(path)) {
7043
+ return { success: false, error: { code: "E_CAMERA_NOT_FOUND", message: `camera media not found: ${path}` } };
7044
+ }
7045
+ const b = head2(path, 12);
7046
+ if (b.length >= 8 && b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
7047
+ return ok({ kind: "image", ext: "png" });
7048
+ }
7049
+ if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255)
7050
+ return ok({ kind: "image", ext: "jpg" });
7051
+ if (b.length >= 12 && b.subarray(4, 8).toString("latin1") === "ftyp") {
7052
+ const brand = FTYP_BRANDS[b.subarray(8, 12).toString("latin1")];
7053
+ if (brand)
7054
+ return ok(brand);
7055
+ }
7056
+ return {
7057
+ success: false,
7058
+ error: { code: "E_CAMERA_MEDIA", message: `unsupported camera media ${path}: use ${SUPPORTED}` }
7059
+ };
7060
+ }
7061
+ function cameraFeedDir(wardenHome, udid) {
7062
+ return join3(wardenHome, "camera", udid);
7063
+ }
7064
+ function stageCameraFeed(input) {
7065
+ const sniffed = sniffCameraMedia(input.source);
7066
+ if (!sniffed.success)
7067
+ return sniffed;
7068
+ const hash = createHash("sha256").update(readFileSync(input.source)).digest("hex").slice(0, 12);
7069
+ const dir = cameraFeedDir(input.wardenHome, input.udid);
7070
+ mkdirSync3(dir, { recursive: true });
7071
+ const path = join3(dir, `${input.facing}-${hash}.${sniffed.data.ext}`);
7072
+ if (!existsSync3(path))
7073
+ copyFileSync(input.source, path);
7074
+ return ok(sniffed.data.kind === "image" ? { kind: "image", path } : { kind: "video", path, loop: input.loop });
7075
+ }
7076
+ function clearCameraFeeds(wardenHome, udid) {
7077
+ rmSync(cameraFeedDir(wardenHome, udid), { recursive: true, force: true });
7078
+ }
7079
+
7080
+ // ../../packages/core/src/store.ts
7081
+ import { Database } from "bun:sqlite";
7082
+ import { randomUUID } from "crypto";
7083
+ import { mkdirSync as mkdirSync4 } from "fs";
7084
+ import { dirname as dirname3, join as join4 } from "path";
7085
+
7086
+ // ../../packages/core/src/lease-events.ts
7087
+ var LEASE_EVENT_TYPES = [
7088
+ "requested",
7089
+ "queued",
7090
+ "granted",
7091
+ "heartbeat_lost",
7092
+ "released",
7093
+ "expired",
7094
+ "evicted",
7095
+ "quarantined",
7096
+ "unquarantined",
7097
+ "shutdown",
7098
+ "device_unhealthy",
7099
+ "device_recovered"
7100
+ ];
7101
+ function insertLeaseEvent(db, event) {
7102
+ db.query(`INSERT INTO lease_events (at, type, lease_id, resource_key, owner_key, label, claim_id, data)
7103
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(event.at, event.type, event.leaseId ?? null, event.resourceKey ?? null, event.ownerKey ?? null, event.label ?? null, event.claimId ?? null, JSON.stringify(event.data));
7104
+ }
7105
+ function isEventType(value) {
7106
+ return LEASE_EVENT_TYPES.includes(value);
7107
+ }
7108
+ function toEvent(row) {
7109
+ if (!isEventType(row.type))
7110
+ return;
7111
+ const base = {
7112
+ id: row.id,
7113
+ at: row.at,
7114
+ type: row.type,
7115
+ data: JSON.parse(row.data),
7116
+ ...row.lease_id !== null ? { leaseId: row.lease_id } : {},
7117
+ ...row.resource_key !== null ? { resourceKey: row.resource_key } : {},
7118
+ ...row.owner_key !== null ? { ownerKey: row.owner_key } : {},
7119
+ ...row.label !== null ? { label: row.label } : {},
7120
+ ...row.claim_id !== null ? { claimId: row.claim_id } : {}
7121
+ };
7122
+ return base;
7123
+ }
7124
+ function queryLeaseEvents(db, filter = {}) {
7125
+ const where = [];
7126
+ const params = [];
7127
+ if (filter.since !== undefined) {
7128
+ where.push("at >= ?");
7129
+ params.push(filter.since);
7130
+ }
7131
+ if (filter.leaseId !== undefined) {
7132
+ where.push("lease_id = ?");
7133
+ params.push(filter.leaseId);
7134
+ }
7135
+ if (filter.owner !== undefined) {
7136
+ where.push("instr(owner_key, ?) > 0");
7137
+ params.push(filter.owner);
7138
+ }
7139
+ if (filter.afterId !== undefined) {
7140
+ where.push("id > ?");
7141
+ params.push(filter.afterId);
7142
+ }
7143
+ const sql = `SELECT * FROM lease_events${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`;
7144
+ return db.query(sql).all(...params).flatMap((row) => toEvent(row) ?? []);
7145
+ }
7146
+ function pruneLeaseEvents(db, before) {
7147
+ return db.query("DELETE FROM lease_events WHERE at < ?").run(before).changes;
7148
+ }
7149
+ var INTERNAL_LEASE_LABELS = ["capacity-evict", "gc-shutdown", "reclaiming"];
7150
+
7151
+ // ../../packages/core/src/liveness.ts
7152
+ var LOST_HEARTBEATS = 3;
7153
+ function isLeaseAlive(lease, now, pidAlive) {
7154
+ const supervised = lease.pid !== undefined && lease.heartbeatMs !== undefined;
7155
+ const fresh = supervised ? Math.min(lease.ttlMs, LOST_HEARTBEATS * (lease.heartbeatMs ?? 0)) : lease.ttlMs;
7156
+ if (now - lease.heartbeatAt <= fresh)
7157
+ return true;
7158
+ return lease.pid !== undefined && pidAlive(lease.pid);
7159
+ }
7160
+ function processAlive(pid) {
7161
+ try {
7162
+ process.kill(pid, 0);
7163
+ return true;
7164
+ } catch (error) {
7165
+ return error instanceof Error && "code" in error && error.code === "EPERM";
7166
+ }
7167
+ }
7168
+
7169
+ // ../../packages/core/src/queue.ts
7170
+ var AGING_MS = 10 * 60000;
7171
+ var DEVICE_WAIT_REASONS = ["device-limit", "requester-limit", "waiting-for-device"];
7172
+ var WAIT_REASONS = new Set([
7173
+ "behind-earlier",
7174
+ "ram-budget",
7175
+ "cpu-budget",
7176
+ "memory-pressure",
7177
+ "host-load",
7178
+ ...DEVICE_WAIT_REASONS
7179
+ ]);
7180
+ var isWaitReason = (s) => WAIT_REASONS.has(s);
7181
+
7182
+ // ../../packages/core/src/types.ts
7183
+ function resourceKey(resource) {
7184
+ switch (resource.kind) {
7185
+ case "device":
7186
+ return `device:${resource.platform}:${resource.id}`;
7187
+ case "port":
7188
+ return `port:${resource.port}`;
7189
+ case "build":
7190
+ return `build:${resource.key}`;
7191
+ }
7192
+ }
7193
+ function describeOwner(owner) {
7194
+ switch (owner.kind) {
7195
+ case "agent":
7196
+ return `agent ${owner.sessionId}`;
7197
+ case "user":
7198
+ return `user pid ${owner.pid}`;
7199
+ case "ci":
7200
+ return `ci ${owner.runId}`;
7201
+ }
7202
+ }
7203
+ var PHYSICAL_IOS_ID = /^(?:[0-9A-F]{8}-[0-9A-F]{16}|[0-9a-f]{40})$/i;
7204
+ function isPhysicalIosId(id) {
7205
+ return PHYSICAL_IOS_ID.test(id);
7206
+ }
7207
+
7208
+ // ../../packages/core/src/store.ts
7209
+ function wardenHome(env = process.env) {
7210
+ if (env.WARDEN_HOME)
7211
+ return env.WARDEN_HOME;
7212
+ return join4(env.HOME ?? "~", ".warden");
7213
+ }
7214
+ function defaultDbPath(env = process.env) {
7215
+ return join4(wardenHome(env), "warden.db");
7216
+ }
7217
+ var MIGRATIONS = [
7218
+ `CREATE TABLE leases (
7219
+ id TEXT PRIMARY KEY,
7220
+ resource_key TEXT NOT NULL UNIQUE,
7221
+ resource TEXT NOT NULL,
7222
+ owner_key TEXT NOT NULL,
7223
+ owner TEXT NOT NULL,
7224
+ label TEXT,
7225
+ acquired_at INTEGER NOT NULL,
7226
+ heartbeat_at INTEGER NOT NULL,
7227
+ ttl_ms INTEGER NOT NULL,
7228
+ pid INTEGER
7229
+ );
7230
+ CREATE INDEX leases_owner ON leases(owner_key);
7231
+ CREATE TABLE devices (
7232
+ platform TEXT NOT NULL,
7233
+ id TEXT NOT NULL,
7234
+ name TEXT NOT NULL,
7235
+ profile TEXT,
7236
+ runtime TEXT,
7237
+ created_at INTEGER NOT NULL,
7238
+ last_used_at INTEGER NOT NULL,
7239
+ PRIMARY KEY (platform, id)
7240
+ );`,
7241
+ `CREATE TABLE builds (
7242
+ project_key TEXT NOT NULL,
7243
+ platform TEXT NOT NULL,
7244
+ profile TEXT NOT NULL,
7245
+ hash TEXT NOT NULL,
7246
+ path TEXT NOT NULL,
7247
+ source TEXT NOT NULL,
7248
+ size INTEGER NOT NULL,
7249
+ created_at INTEGER NOT NULL,
7250
+ last_used_at INTEGER NOT NULL,
7251
+ PRIMARY KEY (project_key, platform, hash)
7252
+ );
7253
+ CREATE TABLE installs (
7254
+ platform TEXT NOT NULL,
7255
+ device_id TEXT NOT NULL,
7256
+ bundle_id TEXT NOT NULL,
7257
+ hash TEXT NOT NULL,
7258
+ installed_at INTEGER NOT NULL,
7259
+ PRIMARY KEY (platform, device_id, bundle_id)
7260
+ );`,
7261
+ `ALTER TABLE leases ADD COLUMN booted_by_owner INTEGER;
7262
+ CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`,
7263
+ `ALTER TABLE devices ADD COLUMN quarantined_at INTEGER;
7264
+ ALTER TABLE devices ADD COLUMN quarantine_reason TEXT;
7265
+ ALTER TABLE devices ADD COLUMN slimmed_at INTEGER;`,
7266
+ `ALTER TABLE leases ADD COLUMN heartbeat_ms INTEGER;`,
7267
+ `CREATE TABLE fingerprints (
7268
+ root TEXT NOT NULL,
7269
+ platform TEXT NOT NULL,
7270
+ variant TEXT NOT NULL,
7271
+ command TEXT NOT NULL,
7272
+ signature TEXT NOT NULL,
7273
+ native TEXT NOT NULL,
7274
+ computed_at INTEGER NOT NULL,
7275
+ PRIMARY KEY (root, platform, variant, command)
7276
+ );`,
7277
+ `ALTER TABLE installs ADD COLUMN container TEXT;
7278
+ ALTER TABLE installs ADD COLUMN exe_stamp TEXT;`,
7279
+ `CREATE TABLE boot_queue (
7280
+ id TEXT PRIMARY KEY,
7281
+ platform TEXT NOT NULL,
7282
+ profile TEXT NOT NULL,
7283
+ units REAL NOT NULL,
7284
+ priority INTEGER NOT NULL,
7285
+ enqueued_at INTEGER NOT NULL,
7286
+ heartbeat_at INTEGER NOT NULL,
7287
+ owner_key TEXT NOT NULL,
7288
+ owner TEXT NOT NULL,
7289
+ label TEXT,
7290
+ pid INTEGER
7291
+ );
7292
+ CREATE INDEX boot_queue_order ON boot_queue(priority, enqueued_at);`,
7293
+ `ALTER TABLE leases ADD COLUMN shared INTEGER;`,
7294
+ `ALTER TABLE builds ADD COLUMN kind TEXT NOT NULL DEFAULT 'legacy';
7295
+ ALTER TABLE builds ADD COLUMN base_hash TEXT;
7296
+ ALTER TABLE builds ADD COLUMN js_key TEXT;
7297
+ ALTER TABLE builds ADD COLUMN worktree TEXT;
7298
+ ALTER TABLE builds ADD COLUMN branch TEXT;`,
7299
+ `CREATE TABLE lease_conditions (
7300
+ lease_id TEXT NOT NULL REFERENCES leases(id) ON DELETE CASCADE,
7301
+ area TEXT NOT NULL,
7302
+ json TEXT NOT NULL,
7303
+ owned TEXT NOT NULL DEFAULT '[]',
7304
+ set_at INTEGER NOT NULL,
7305
+ PRIMARY KEY (lease_id, area)
7306
+ );`,
7307
+ `CREATE TABLE orphan_conditions (
7308
+ platform TEXT NOT NULL,
7309
+ device_id TEXT NOT NULL,
7310
+ area TEXT NOT NULL,
7311
+ lease_id TEXT NOT NULL,
7312
+ json TEXT NOT NULL,
7313
+ owned TEXT NOT NULL DEFAULT '[]',
7314
+ set_at INTEGER NOT NULL,
7315
+ orphaned_at INTEGER NOT NULL,
7316
+ PRIMARY KEY (platform, device_id, area)
7317
+ );
7318
+ CREATE TRIGGER leases_orphan_conditions BEFORE DELETE ON leases
7319
+ WHEN json_extract(OLD.resource, '$.kind') = 'device' AND json_extract(OLD.resource, '$.id') NOT LIKE 'pending:%'
7320
+ BEGIN
7321
+ INSERT OR REPLACE INTO orphan_conditions (platform, device_id, area, lease_id, json, owned, set_at, orphaned_at)
7322
+ SELECT json_extract(OLD.resource, '$.platform'), json_extract(OLD.resource, '$.id'), c.area, OLD.id, c.json, c.owned, c.set_at,
7323
+ CAST(strftime('%s', 'now') AS INTEGER) * 1000
7324
+ FROM lease_conditions c WHERE c.lease_id = OLD.id;
7325
+ END;`,
7326
+ `ALTER TABLE devices ADD COLUMN persistent_at INTEGER;`,
7327
+ `ALTER TABLE boot_queue ADD COLUMN reason TEXT;`,
7328
+ `ALTER TABLE leases ADD COLUMN claim_id TEXT;
7329
+ ALTER TABLE boot_queue ADD COLUMN claim_id TEXT;`,
7330
+ `CREATE TABLE lease_events (
7331
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7332
+ at INTEGER NOT NULL,
7333
+ type TEXT NOT NULL,
7334
+ lease_id TEXT,
7335
+ resource_key TEXT,
7336
+ owner_key TEXT,
7337
+ label TEXT,
7338
+ claim_id TEXT,
7339
+ data TEXT NOT NULL DEFAULT '{}'
7340
+ );
7341
+ CREATE INDEX lease_events_at ON lease_events(at);
7342
+ CREATE INDEX lease_events_lease ON lease_events(lease_id);
7343
+ CREATE INDEX lease_events_owner ON lease_events(owner_key, at);
7344
+ CREATE INDEX lease_events_claim ON lease_events(claim_id);`,
7345
+ `CREATE TABLE claim_keys (
7346
+ owner_key TEXT NOT NULL,
7347
+ key TEXT NOT NULL,
7348
+ claim_id TEXT NOT NULL,
7349
+ platform TEXT NOT NULL,
7350
+ profile TEXT NOT NULL,
7351
+ count INTEGER NOT NULL,
7352
+ ready INTEGER NOT NULL DEFAULT 0,
7353
+ created_at INTEGER NOT NULL,
7354
+ PRIMARY KEY (owner_key, key)
7355
+ );
7356
+ CREATE INDEX claim_keys_claim ON claim_keys(claim_id);`
7357
+ ];
7358
+ var QUEUE_TTL_MS = 60000;
7359
+ function ownerKey(owner) {
7360
+ switch (owner.kind) {
7361
+ case "agent":
7362
+ return `agent:${owner.sessionId}`;
7363
+ case "user":
7364
+ return `user:${owner.pid}`;
7365
+ case "ci":
7366
+ return `ci:${owner.runId}`;
7367
+ }
7368
+ }
7369
+ function toQueueEntry(row) {
7370
+ const entry = {
7371
+ id: row.id,
7372
+ platform: row.platform,
7373
+ profile: row.profile,
7374
+ units: row.units,
7375
+ priority: row.priority,
7376
+ enqueuedAt: row.enqueued_at,
7377
+ heartbeatAt: row.heartbeat_at,
7378
+ owner: JSON.parse(row.owner)
7379
+ };
7380
+ if (row.label !== null)
7381
+ entry.label = row.label;
7382
+ if (row.pid !== null)
7383
+ entry.pid = row.pid;
7384
+ if (row.reason !== null && isWaitReason(row.reason))
7385
+ entry.reason = row.reason;
7386
+ if (row.claim_id !== null)
7387
+ entry.claimId = row.claim_id;
7388
+ return entry;
7389
+ }
7390
+ function toLease(row) {
7391
+ const lease = {
7392
+ id: row.id,
7393
+ resource: JSON.parse(row.resource),
7394
+ owner: JSON.parse(row.owner),
7395
+ acquiredAt: row.acquired_at,
7396
+ heartbeatAt: row.heartbeat_at,
7397
+ ttlMs: row.ttl_ms
7398
+ };
7399
+ if (row.label !== null)
7400
+ lease.label = row.label;
7401
+ if (row.pid !== null)
7402
+ lease.pid = row.pid;
7403
+ if (row.booted_by_owner)
7404
+ lease.bootedByOwner = true;
7405
+ if (row.heartbeat_ms !== null)
7406
+ lease.heartbeatMs = row.heartbeat_ms;
7407
+ if (row.shared)
7408
+ lease.shared = true;
7409
+ if (row.claim_id !== null)
7410
+ lease.claimId = row.claim_id;
7411
+ return lease;
7412
+ }
7413
+ function toDevice(row) {
7414
+ const device = {
7415
+ platform: row.platform,
7416
+ id: row.id,
7417
+ name: row.name,
7418
+ createdAt: row.created_at,
7419
+ lastUsedAt: row.last_used_at
7420
+ };
7421
+ if (row.profile !== null)
7422
+ device.profile = row.profile;
7423
+ if (row.runtime !== null)
7424
+ device.runtime = row.runtime;
7425
+ if (row.quarantined_at !== null)
7426
+ device.quarantinedAt = row.quarantined_at;
7427
+ if (row.quarantine_reason !== null)
7428
+ device.quarantineReason = row.quarantine_reason;
7429
+ if (row.slimmed_at !== null)
7430
+ device.slimmedAt = row.slimmed_at;
7431
+ if (row.persistent_at !== null)
7432
+ device.persistentAt = row.persistent_at;
7433
+ return device;
7434
+ }
7435
+ function newLeaseId() {
7436
+ return `l_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
7437
+ }
7438
+
7439
+ class Store {
7440
+ db;
7441
+ constructor(db) {
7442
+ this.db = db;
7443
+ }
7444
+ close() {
7445
+ this.db.close();
7446
+ }
7447
+ transaction(fn) {
7448
+ if (this.db.inTransaction)
7449
+ return fn();
7450
+ return this.db.transaction(fn).immediate();
7451
+ }
7452
+ insertLease(input, now) {
7453
+ const lease = {
7454
+ id: newLeaseId(),
7455
+ resource: input.resource,
7456
+ owner: input.owner,
7457
+ acquiredAt: now,
7458
+ heartbeatAt: now,
7459
+ ttlMs: input.ttlMs
7460
+ };
7461
+ if (input.label !== undefined)
7462
+ lease.label = input.label;
7463
+ if (input.pid !== undefined)
7464
+ lease.pid = input.pid;
7465
+ if (input.bootedByOwner)
7466
+ lease.bootedByOwner = true;
7467
+ if (input.shared)
7468
+ lease.shared = true;
7469
+ if (input.claimId !== undefined)
7470
+ lease.claimId = input.claimId;
7471
+ this.db.query(`INSERT INTO leases (id, resource_key, resource, owner_key, owner, label, acquired_at, heartbeat_at, ttl_ms, pid, booted_by_owner, shared, claim_id)
7472
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(lease.id, resourceKey(lease.resource), JSON.stringify(lease.resource), ownerKey(lease.owner), JSON.stringify(lease.owner), lease.label ?? null, now, now, lease.ttlMs, lease.pid ?? null, lease.bootedByOwner ? 1 : null, lease.shared ? 1 : null, lease.claimId ?? null);
7473
+ return lease;
7474
+ }
7475
+ markShared(ids) {
7476
+ if (ids.length === 0)
7477
+ return;
7478
+ const stmt = this.db.query("UPDATE leases SET shared = 1 WHERE id = ?");
7479
+ this.transaction(() => {
7480
+ for (const id of ids)
7481
+ stmt.run(id);
7482
+ });
7483
+ }
7484
+ markBootedByOwner(id) {
7485
+ this.db.query("UPDATE leases SET booted_by_owner = 1 WHERE id = ?").run(id);
7486
+ }
7487
+ setLeasePid(id, pid) {
7488
+ this.db.query("UPDATE leases SET pid = ? WHERE id = ?").run(pid, id);
7489
+ }
7490
+ getMeta(key) {
7491
+ return this.db.query("SELECT value FROM meta WHERE key = ?").get(key)?.value;
7492
+ }
7493
+ setMeta(key, value) {
7494
+ this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value").run(key, value);
7495
+ }
7496
+ deleteMeta(key) {
7497
+ this.db.query("DELETE FROM meta WHERE key = ?").run(key);
7498
+ }
7499
+ getLease(id) {
7500
+ const row = this.db.query("SELECT * FROM leases WHERE id = ?").get(id);
7501
+ return row ? toLease(row) : undefined;
7502
+ }
7503
+ listLeases() {
7504
+ return this.db.query("SELECT * FROM leases ORDER BY acquired_at, id").all().map(toLease);
7505
+ }
7506
+ listLeasesByOwner(owner) {
7507
+ return this.db.query("SELECT * FROM leases WHERE owner_key = ? ORDER BY acquired_at, id").all(ownerKey(owner)).map(toLease);
7508
+ }
7509
+ findLeaseByResource(resource) {
7510
+ const row = this.db.query("SELECT * FROM leases WHERE resource_key = ?").get(resourceKey(resource));
7511
+ return row ? toLease(row) : undefined;
7512
+ }
7513
+ deleteLeases(ids, end) {
7514
+ if (ids.length === 0)
7515
+ return 0;
7516
+ const stmt = this.db.query("DELETE FROM leases WHERE id = ?");
7517
+ return this.transaction(() => ids.reduce((n, id) => {
7518
+ const lease = end ? this.getLease(id) : undefined;
7519
+ const changes = stmt.run(id).changes;
7520
+ if (changes > 0 && lease && end)
7521
+ this.recordLeaseEnd(lease, end);
7522
+ return n + changes;
7523
+ }, 0));
7524
+ }
7525
+ recordLeaseEnd(lease, end) {
7526
+ if (lease.resource.kind !== "device" || lease.resource.id.startsWith("pending:"))
7527
+ return;
7528
+ if (lease.label !== undefined && INTERNAL_LEASE_LABELS.includes(lease.label))
7529
+ return;
7530
+ const granted = this.db.query("SELECT at FROM lease_events WHERE lease_id = ? AND type = 'granted' LIMIT 1").get(lease.id);
7531
+ if (!granted)
7532
+ return;
7533
+ this.recordEvent({
7534
+ at: end.now,
7535
+ type: end.type,
7536
+ data: { heldMs: Math.max(0, end.now - granted.at) },
7537
+ leaseId: lease.id,
7538
+ resourceKey: resourceKey(lease.resource),
7539
+ ownerKey: ownerKey(lease.owner),
7540
+ ...lease.label !== undefined ? { label: lease.label } : {},
7541
+ ...lease.claimId !== undefined ? { claimId: lease.claimId } : {}
7542
+ });
7543
+ }
7544
+ recordEvent(event) {
7545
+ insertLeaseEvent(this.db, event);
7546
+ }
7547
+ recordQueuedOnce(event) {
7548
+ return this.transaction(() => {
7549
+ const seen = this.db.query("SELECT 1 AS n FROM lease_events WHERE type = 'queued' AND claim_id IS ?1 AND json_extract(data, '$.queueId') = ?2 LIMIT 1").get(event.claimId ?? null, event.data.queueId);
7550
+ if (seen)
7551
+ return false;
7552
+ this.recordEvent(event);
7553
+ return true;
7554
+ });
7555
+ }
7556
+ recordHeartbeatLost(ids, now) {
7557
+ return this.transaction(() => {
7558
+ let logged = 0;
7559
+ for (const id of ids) {
7560
+ const event = this.getLease(id) ? undefined : this.heartbeatLostEvent(id, now);
7561
+ if (!event)
7562
+ continue;
7563
+ this.recordEvent(event);
7564
+ logged++;
7565
+ }
7566
+ return logged;
7567
+ });
7568
+ }
7569
+ heartbeatLostEvent(leaseId, at) {
7570
+ const granted = this.db.query("SELECT resource_key, owner_key, label, claim_id FROM lease_events WHERE lease_id = ? AND type = 'granted' LIMIT 1").get(leaseId);
7571
+ if (!granted)
7572
+ return;
7573
+ return {
7574
+ at,
7575
+ type: "heartbeat_lost",
7576
+ data: {},
7577
+ leaseId,
7578
+ ...granted.resource_key ? { resourceKey: granted.resource_key } : {},
7579
+ ...granted.owner_key ? { ownerKey: granted.owner_key } : {},
7580
+ ...granted.label ? { label: granted.label } : {},
7581
+ ...granted.claim_id ? { claimId: granted.claim_id } : {}
7582
+ };
7583
+ }
7584
+ listEvents(filter = {}) {
7585
+ return queryLeaseEvents(this.db, filter);
7586
+ }
7587
+ pruneEvents(before) {
7588
+ return pruneLeaseEvents(this.db, before);
7589
+ }
7590
+ heartbeat(ids, now, periodMs) {
7591
+ if (ids.length === 0)
7592
+ return 0;
7593
+ const stmt = this.db.query("UPDATE leases SET heartbeat_at = ?, heartbeat_ms = COALESCE(?, heartbeat_ms) WHERE id = ?");
7594
+ return this.transaction(() => ids.reduce((n, id) => n + stmt.run(now, periodMs ?? null, id).changes, 0));
7595
+ }
7596
+ reclaimStale(now, pidAlive) {
7597
+ return this.transaction(() => {
7598
+ const stale = this.listLeases().filter((l) => !isLeaseAlive(l, now, pidAlive)).map((l) => l.id);
7599
+ this.deleteLeases(stale, { type: "expired", now });
7600
+ return stale;
7601
+ });
7602
+ }
7603
+ enqueue(entry) {
7604
+ return this.transaction(() => {
7605
+ this.db.query(`INSERT INTO boot_queue (id, platform, profile, units, priority, enqueued_at, heartbeat_at, owner_key, owner, label, pid, reason, claim_id)
7606
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7607
+ ON CONFLICT (id) DO UPDATE SET platform = excluded.platform, profile = excluded.profile, units = excluded.units,
7608
+ heartbeat_at = excluded.heartbeat_at, label = excluded.label, pid = excluded.pid, reason = excluded.reason, claim_id = excluded.claim_id`).run(entry.id, entry.platform, entry.profile, entry.units, entry.priority, entry.enqueuedAt, entry.heartbeatAt, ownerKey(entry.owner), JSON.stringify(entry.owner), entry.label ?? null, entry.pid ?? null, entry.reason ?? null, entry.claimId ?? null);
7609
+ const row = this.db.query("SELECT * FROM boot_queue WHERE id = ?").get(entry.id);
7610
+ if (!row)
7611
+ throw new Error(`boot_queue row ${entry.id} vanished`);
7612
+ return toQueueEntry(row);
7613
+ });
7614
+ }
7615
+ refreshQueue(id, now, reason) {
7616
+ return this.db.query("UPDATE boot_queue SET heartbeat_at = ?, reason = COALESCE(?, reason) WHERE id = ?").run(now, reason ?? null, id).changes > 0;
7617
+ }
7618
+ getClaimKey(ownerKey, key) {
7619
+ const row = this.db.query("SELECT * FROM claim_keys WHERE owner_key = ? AND key = ?").get(ownerKey, key);
7620
+ if (!row)
7621
+ return;
7622
+ return {
7623
+ ownerKey: row.owner_key,
7624
+ key: row.key,
7625
+ claimId: row.claim_id,
7626
+ platform: row.platform,
7627
+ profile: row.profile,
7628
+ count: row.count,
7629
+ ready: row.ready === 1
7630
+ };
7631
+ }
7632
+ setClaimKey(entry, now) {
7633
+ this.db.query("INSERT OR REPLACE INTO claim_keys (owner_key, key, claim_id, platform, profile, count, ready, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?)").run(entry.ownerKey, entry.key, entry.claimId, entry.platform, entry.profile, entry.count, now);
7634
+ }
7635
+ markClaimKeyReady(claimId) {
7636
+ this.db.query("UPDATE claim_keys SET ready = 1 WHERE claim_id = ?").run(claimId);
7637
+ }
7638
+ deleteClaimKey(ownerKey, key) {
7639
+ this.db.query("DELETE FROM claim_keys WHERE owner_key = ? AND key = ?").run(ownerKey, key);
7640
+ }
7641
+ deleteClaimKeysOf(claimId) {
7642
+ this.db.query("DELETE FROM claim_keys WHERE claim_id = ?").run(claimId);
7643
+ }
7644
+ dequeue(id) {
7645
+ return this.db.query("DELETE FROM boot_queue WHERE id = ?").run(id).changes > 0;
7646
+ }
7647
+ listQueue() {
7648
+ return this.db.query("SELECT * FROM boot_queue ORDER BY enqueued_at, id").all().map(toQueueEntry);
7649
+ }
7650
+ reclaimStaleQueue(now, pidAlive) {
7651
+ return this.transaction(() => {
7652
+ const stale = this.listQueue().filter((e) => now - e.heartbeatAt > QUEUE_TTL_MS || e.pid !== undefined && !pidAlive(e.pid)).map((e) => e.id);
7653
+ const stmt = this.db.query("DELETE FROM boot_queue WHERE id = ?");
7654
+ for (const id of stale)
7655
+ stmt.run(id);
7656
+ return stale;
7657
+ });
7658
+ }
7659
+ recordDevice(device, now) {
7660
+ this.db.query(`INSERT INTO devices (platform, id, name, profile, runtime, created_at, last_used_at) VALUES (?, ?, ?, ?, ?, ?, ?)
7661
+ ON CONFLICT (platform, id) DO UPDATE SET name = excluded.name, profile = excluded.profile, runtime = excluded.runtime, last_used_at = excluded.last_used_at`).run(device.platform, device.id, device.name, device.profile ?? null, device.runtime ?? null, now, now);
7662
+ }
7663
+ listDevices(platform) {
7664
+ const rows = platform === undefined ? this.db.query("SELECT * FROM devices ORDER BY platform, name").all() : this.db.query("SELECT * FROM devices WHERE platform = ? ORDER BY name").all(platform);
7665
+ return rows.map(toDevice);
7666
+ }
7667
+ touchDevice(platform, id, now) {
7668
+ this.db.query("UPDATE devices SET last_used_at = ? WHERE platform = ? AND id = ?").run(now, platform, id);
7669
+ }
7670
+ quarantineDevice(device, reason, now) {
7671
+ this.transaction(() => {
7672
+ this.db.query(`INSERT INTO devices (platform, id, name, profile, runtime, created_at, last_used_at, quarantined_at, quarantine_reason)
7673
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
7674
+ ON CONFLICT (platform, id) DO UPDATE SET quarantined_at = excluded.quarantined_at, quarantine_reason = excluded.quarantine_reason`).run(device.platform, device.id, device.name, device.profile ?? null, device.runtime ?? null, now, now, now, reason ?? null);
7675
+ this.recordEvent({
7676
+ at: now,
7677
+ type: "quarantined",
7678
+ data: {
7679
+ platform: device.platform,
7680
+ deviceId: device.id,
7681
+ deviceName: device.name,
7682
+ ...reason !== undefined ? { reason } : {}
7683
+ },
7684
+ resourceKey: `device:${device.platform}:${device.id}`
7685
+ });
7686
+ });
7687
+ }
7688
+ unquarantineDevice(id, now) {
7689
+ return this.transaction(() => {
7690
+ const cleared = this.db.query("SELECT platform FROM devices WHERE id = ? AND quarantined_at IS NOT NULL").all(id);
7691
+ this.db.query("UPDATE devices SET quarantined_at = NULL, quarantine_reason = NULL WHERE id = ? AND quarantined_at IS NOT NULL").run(id);
7692
+ for (const { platform } of cleared) {
7693
+ this.recordEvent({
7694
+ at: now,
7695
+ type: "unquarantined",
7696
+ data: { platform, deviceId: id },
7697
+ resourceKey: `device:${platform}:${id}`
7698
+ });
7699
+ }
7700
+ return cleared.length;
7701
+ });
7702
+ }
7703
+ setSlimmed(platform, id, at) {
7704
+ this.db.query("UPDATE devices SET slimmed_at = ? WHERE platform = ? AND id = ?").run(at ?? null, platform, id);
7705
+ }
7706
+ markPersistent(platform, id, now) {
7707
+ this.db.query("UPDATE devices SET persistent_at = ? WHERE platform = ? AND id = ?").run(now, platform, id);
7708
+ }
7709
+ forgetDevice(platform, id) {
7710
+ this.db.query("DELETE FROM devices WHERE platform = ? AND id = ?").run(platform, id);
7711
+ }
7712
+ gcWork(now, idleMs, pidAlive) {
7713
+ const staleLeases = this.listLeases().filter((l) => !isLeaseAlive(l, now, pidAlive)).length;
7714
+ const lastGc = Number(this.getMeta("last_gc_at") ?? 0);
7715
+ const row = this.db.query(`SELECT COUNT(*) AS n FROM devices d
7716
+ WHERE d.last_used_at <= ?1 - ?2 AND d.last_used_at + ?2 > ?3
7717
+ AND NOT EXISTS (SELECT 1 FROM leases l WHERE l.resource_key = 'device:' || d.platform || ':' || d.id)`).get(now, idleMs, lastGc);
7718
+ return { staleLeases, idleDevices: row?.n ?? 0 };
7719
+ }
7720
+ }
7721
+ function migrate(db) {
7722
+ const row = db.query("PRAGMA user_version").get();
7723
+ const current = row?.user_version ?? 0;
7724
+ if (current >= MIGRATIONS.length)
7725
+ return;
7726
+ db.transaction(() => {
7727
+ const again = db.query("PRAGMA user_version").get()?.user_version ?? 0;
7728
+ for (let v = again;v < MIGRATIONS.length; v++) {
7729
+ const sql = MIGRATIONS[v];
7730
+ if (sql !== undefined)
7731
+ db.exec(sql);
7732
+ }
7733
+ db.exec(`PRAGMA user_version = ${MIGRATIONS.length}`);
7734
+ }).immediate();
7735
+ }
7736
+ function openStore(path = defaultDbPath()) {
7737
+ mkdirSync4(dirname3(path), { recursive: true });
7738
+ const db = new Database(path, { create: true, strict: true });
7739
+ db.exec("PRAGMA busy_timeout = 15000");
7740
+ db.exec("PRAGMA journal_mode = WAL");
7741
+ db.exec("PRAGMA synchronous = NORMAL");
7742
+ db.exec("PRAGMA foreign_keys = ON");
7743
+ migrate(db);
7744
+ return new Store(db);
7745
+ }
7746
+
7747
+ // ../../packages/e2e/src/runner/callsite.ts
7748
+ import { join as join5 } from "path";
7749
+ var OWN_FILES = new Set(["callsite.ts", "scope.ts", "test.ts", "attempt.ts"].map((name) => join5(import.meta.dir, name)).concat(join5(import.meta.dir, "..", "index.ts")));
7750
+ var FRAME = /(?:\(|\s)((?:file:\/\/)?\/[^\s()]+?):\d+(?::\d+)?\)?\s*$/;
7751
+ function callerFile(stack = new Error().stack, extraOwn = () => false) {
7752
+ for (const line of (stack ?? "").split(`
7753
+ `)) {
7754
+ const raw = FRAME.exec(line)?.[1];
7755
+ if (raw === undefined)
7756
+ continue;
7757
+ const path = raw.startsWith("file://") ? raw.slice("file://".length) : raw;
7758
+ if (OWN_FILES.has(path) || extraOwn(path) || path.includes("/node_modules/"))
7759
+ continue;
7760
+ return path;
7761
+ }
7762
+ return "unknown";
7763
+ }
7764
+
7765
+ // ../../packages/e2e/src/conditions/condition-events.ts
7766
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
7767
+ import { dirname as dirname4 } from "path";
7768
+ function openConditionSink(path) {
7769
+ let ready = false;
7770
+ return (event) => {
7771
+ if (!ready) {
7772
+ mkdirSync5(dirname4(path), { recursive: true });
7773
+ ready = true;
7774
+ }
7775
+ appendFileSync2(path, `${JSON.stringify(event)}
7776
+ `);
7777
+ };
7778
+ }
7779
+ var KNOWN_AREAS = new Set([
7780
+ "network",
7781
+ "location",
7782
+ "clock",
7783
+ "faults",
7784
+ "permission",
7785
+ "locale",
7786
+ "a11y",
7787
+ "battery",
7788
+ "camera"
7789
+ ]);
7790
+
7791
+ // ../../packages/e2e/src/conditions/geo.ts
7792
+ var EARTH_RADIUS_M = 6371008.8;
7793
+ var rad = (deg) => deg * Math.PI / 180;
7794
+ var deg = (r) => r * 180 / Math.PI;
7795
+ function haversineM(a, b) {
7796
+ const dLat = rad(b.lat - a.lat);
7797
+ const dLon = rad(b.lon - a.lon);
7798
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2;
7799
+ return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
7800
+ }
7801
+ function interpolate(a, b, f) {
7802
+ if (f <= 0)
7803
+ return a;
7804
+ if (f >= 1)
7805
+ return b;
7806
+ const delta = haversineM(a, b) / EARTH_RADIUS_M;
7807
+ if (delta < 0.000000000001)
7808
+ return a;
7809
+ const sinDelta = Math.sin(delta);
7810
+ const wa = Math.sin((1 - f) * delta) / sinDelta;
7811
+ const wb = Math.sin(f * delta) / sinDelta;
7812
+ const lat1 = rad(a.lat);
7813
+ const lon1 = rad(a.lon);
7814
+ const lat2 = rad(b.lat);
7815
+ const lon2 = rad(b.lon);
7816
+ const x = wa * Math.cos(lat1) * Math.cos(lon1) + wb * Math.cos(lat2) * Math.cos(lon2);
7817
+ const y = wa * Math.cos(lat1) * Math.sin(lon1) + wb * Math.cos(lat2) * Math.sin(lon2);
7818
+ const z = wa * Math.sin(lat1) + wb * Math.sin(lat2);
7819
+ return { lat: deg(Math.atan2(z, Math.hypot(x, y))), lon: deg(Math.atan2(y, x)) };
7820
+ }
7821
+ function routeSteps(points, speedMps, intervalS) {
7822
+ const first = points[0];
7823
+ if (!first)
7824
+ return [];
7825
+ if (points.length === 1)
7826
+ return [first];
7827
+ const stride = Math.max(speedMps * intervalS, 0.01);
7828
+ const steps = [first];
7829
+ let carried = 0;
7830
+ for (let i = 1;i < points.length; i++) {
7831
+ const from = points[i - 1];
7832
+ const to = points[i];
7833
+ const leg = haversineM(from, to);
7834
+ let at = stride - carried;
7835
+ while (at < leg - 0.000001) {
7836
+ steps.push(interpolate(from, to, at / leg));
7837
+ at += stride;
7838
+ }
7839
+ carried = leg - (at - stride);
7840
+ }
7841
+ const last = points[points.length - 1];
7842
+ if (haversineM(steps[steps.length - 1], last) > 0.001)
7843
+ steps.push(last);
7844
+ else
7845
+ steps[steps.length - 1] = last;
7846
+ return steps;
7847
+ }
7848
+ function formatCoord(n) {
7849
+ const fixed = n.toFixed(7);
7850
+ return fixed.includes(".") ? fixed.replace(/\.?0+$/, "") : fixed;
7851
+ }
7852
+
7853
+ // ../../packages/e2e/src/conditions/route-ticker.ts
7854
+ var DEFAULT_SPEED_MPS = 20;
7855
+ var DEFAULT_INTERVAL_S = 1;
7856
+ var MAX_CONSECUTIVE_FAILURES = 3;
7857
+ var realTickerClock = {
7858
+ now: () => Date.now(),
7859
+ sleep: (ms, signal) => new Promise((resolve) => {
7860
+ if (signal.aborted)
7861
+ return resolve();
7862
+ const done = () => {
7863
+ clearTimeout(timer);
7864
+ signal.removeEventListener("abort", done);
7865
+ resolve();
7866
+ };
7867
+ const timer = setTimeout(done, ms);
7868
+ signal.addEventListener("abort", done);
7869
+ })
7870
+ };
7871
+ function startRouteTicker(route, deps) {
7872
+ const controller = new AbortController;
7873
+ const { signal } = controller;
7874
+ const startedAt = deps.now();
7875
+ const intervalMs = (route.intervalS ?? DEFAULT_INTERVAL_S) * 1000;
7876
+ const steps = routeSteps(route.points, route.speedMps ?? DEFAULT_SPEED_MPS, route.intervalS ?? DEFAULT_INTERVAL_S);
7877
+ function* playlist() {
7878
+ do
7879
+ yield* steps;
7880
+ while (route.loop && steps.length > 0);
7881
+ }
7882
+ const total = steps.length;
7883
+ const pace = async (done) => {
7884
+ if (!route.loop && done >= total)
7885
+ return false;
7886
+ await deps.sleep(Math.max(0, startedAt + done * intervalMs - deps.now()), signal);
7887
+ return true;
7888
+ };
7889
+ const run = async () => {
7890
+ const count = { ticks: 0, failures: 0, index: 0 };
7891
+ let lastError = "";
7892
+ const sendOne = async (step) => {
7893
+ const failure = await deps.fix(step).catch((e) => e instanceof Error ? e.message : String(e));
7894
+ if (!failure) {
7895
+ count.failures = 0;
7896
+ count.ticks++;
7897
+ return true;
7898
+ }
7899
+ lastError = failure;
7900
+ count.failures++;
7901
+ return count.failures < MAX_CONSECUTIVE_FAILURES;
7902
+ };
7903
+ for (const step of playlist()) {
7904
+ if (signal.aborted)
7905
+ return { ticks: count.ticks, stopped: "aborted" };
7906
+ if (!await sendOne(step))
7907
+ return { ticks: count.ticks, stopped: "failed", error: lastError };
7908
+ count.index++;
7909
+ if (!await pace(count.index))
7910
+ break;
7911
+ }
7912
+ return { ticks: count.ticks, stopped: signal.aborted ? "aborted" : "finished" };
7913
+ };
7914
+ return { stop: () => controller.abort(), done: run() };
7915
+ }
7916
+
7917
+ // ../../packages/e2e/src/conditions/scheduler.ts
7918
+ var MAX_CONSECUTIVE_FAILURES2 = 3;
7919
+ var ONLINE = { kind: "online" };
7920
+ var OFFLINE = { kind: "offline" };
7921
+ function seededRng(seed) {
7922
+ let a = seed >>> 0;
7923
+ return () => {
7924
+ a = a + 1831565813 >>> 0;
7925
+ let t = a;
7926
+ t = Math.imul(t ^ t >>> 15, t | 1);
7927
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
7928
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
7929
+ };
7930
+ }
7931
+ var messageOf = (e) => e instanceof Error ? e.message : String(e);
7932
+ async function attempt(run, what) {
7933
+ if (!run)
7934
+ return `target cannot apply ${what}`;
7935
+ try {
7936
+ const res = await run();
7937
+ return res.success ? undefined : res.error.message;
7938
+ } catch (e) {
7939
+ return messageOf(e);
7940
+ }
7941
+ }
7942
+ function core() {
7943
+ const controller = new AbortController;
7944
+ return { controller, signal: controller.signal, status: { state: "running", transitions: 0 }, transitions: 0 };
7945
+ }
7946
+ function invalid(message) {
7947
+ return {
7948
+ stop: () => {
7949
+ return;
7950
+ },
7951
+ done: Promise.resolve({ transitions: 0, stopped: "failed", error: message }),
7952
+ current: () => ({ state: "failed", transitions: 0, error: message })
7953
+ };
7954
+ }
7955
+ function conclude(state, end, error) {
7956
+ const { transitions } = state;
7957
+ if (error !== undefined) {
7958
+ state.status = { state: "failed", transitions, error };
7959
+ return { transitions, stopped: "failed", error };
7960
+ }
7961
+ state.status = { state: end === "finished" ? "finished" : "stopped", transitions };
7962
+ return { transitions, stopped: end };
7963
+ }
7964
+ function applier(target, deps, state) {
7965
+ const net = target.network?.bind(target);
7966
+ const loc = target.location?.bind(target);
7967
+ const tally = { failures: 0, lastError: "" };
7968
+ const record = async (run, what) => {
7969
+ const failure = await attempt(run, what);
7970
+ if (failure) {
7971
+ tally.failures++;
7972
+ tally.lastError = failure;
7973
+ return false;
7974
+ }
7975
+ tally.failures = 0;
7976
+ state.transitions++;
7977
+ return true;
7978
+ };
7979
+ return {
7980
+ tally,
7981
+ async network(condition) {
7982
+ const landed = await record(net && (() => net(condition)), "network");
7983
+ if (landed)
7984
+ deps.sink?.({ type: "condition", area: "network", value: condition, at: deps.now() });
7985
+ return landed;
7986
+ },
7987
+ async location(op) {
7988
+ const landed = await record(loc && (() => loc(op)), "location");
7989
+ if (landed)
7990
+ deps.sink?.({ type: "condition", area: "location", value: op, at: deps.now() });
7991
+ return landed;
7992
+ }
7993
+ };
7994
+ }
7995
+ function startFlap(target, options, deps) {
7996
+ if (!(options.upMs > 0 && options.downMs > 0 && Number.isFinite(options.upMs + options.downMs))) {
7997
+ return invalid("flap needs upMs and downMs above 0");
7998
+ }
7999
+ if (options.base?.kind === "flap")
8000
+ return invalid("flap base cannot itself be a flap");
8001
+ const base = options.base ?? ONLINE;
8002
+ const rng = deps.rng ?? seededRng(options.seed ?? 1);
8003
+ const jitter = (options.jitterPct ?? 0) / 100;
8004
+ const state = core();
8005
+ const { signal } = state;
8006
+ const apply = applier(target, deps, state);
8007
+ const stretch = (ms) => Math.max(1, ms * (1 + (rng() * 2 - 1) * jitter));
8008
+ const runPhase = async (phase, skipApply, deadline) => {
8009
+ const landed = skipApply || await apply.network(phase === "up" ? base : OFFLINE);
8010
+ state.status = { state: "running", transitions: state.transitions, phase };
8011
+ const next = deadline + stretch(phase === "up" ? options.upMs : options.downMs);
8012
+ await deps.sleep(Math.max(0, next - deps.now()), signal);
8013
+ return [landed && phase === "up", next];
8014
+ };
8015
+ const run = async () => {
8016
+ let phase = "up";
8017
+ let atBase = false;
8018
+ let deadline = deps.now();
8019
+ let skipApply = options.applyInitial === false;
8020
+ while (!signal.aborted && apply.tally.failures < MAX_CONSECUTIVE_FAILURES2) {
8021
+ [atBase, deadline] = await runPhase(phase, skipApply, deadline);
8022
+ skipApply = false;
8023
+ phase = phase === "up" ? "down" : "up";
8024
+ }
8025
+ const { failures, lastError } = apply.tally;
8026
+ if (!atBase)
8027
+ await apply.network(base);
8028
+ return conclude(state, "aborted", failures >= MAX_CONSECUTIVE_FAILURES2 ? lastError : undefined);
8029
+ };
8030
+ return { stop: () => state.controller.abort(), done: run(), current: () => state.status };
8031
+ }
8032
+ function startTimeline(target, steps, deps) {
8033
+ if (steps.some((s) => !(s.atMs >= 0) || !Number.isFinite(s.atMs)))
8034
+ return invalid("timeline atMs must be 0 or more");
8035
+ const ordered = [...steps].sort((a, b) => a.atMs - b.atMs);
8036
+ const startedAt = deps.now();
8037
+ const state = core();
8038
+ const { signal } = state;
8039
+ const apply = applier(target, deps, state);
8040
+ const runStep = async (step) => {
8041
+ const errors = [];
8042
+ if (step.network && !await apply.network(step.network))
8043
+ errors.push(apply.tally.lastError);
8044
+ if (step.location && !await apply.location(step.location))
8045
+ errors.push(apply.tally.lastError);
8046
+ return errors.length > 0 ? errors.join("; ") : undefined;
8047
+ };
8048
+ const run = async () => {
8049
+ let stepFailures = 0;
8050
+ let lastError = "";
8051
+ for (const [index, step] of ordered.entries()) {
8052
+ await deps.sleep(Math.max(0, startedAt + step.atMs - deps.now()), signal);
8053
+ if (signal.aborted)
8054
+ break;
8055
+ state.status = { state: "running", transitions: state.transitions, step: index };
8056
+ const problem = await runStep(step);
8057
+ stepFailures = problem === undefined ? 0 : stepFailures + 1;
8058
+ lastError = problem ?? lastError;
8059
+ if (stepFailures >= MAX_CONSECUTIVE_FAILURES2)
8060
+ return conclude(state, "aborted", lastError);
8061
+ }
8062
+ return conclude(state, signal.aborted ? "aborted" : "finished");
8063
+ };
8064
+ return { stop: () => state.controller.abort(), done: run(), current: () => state.status };
8065
+ }
8066
+
8067
+ // ../../packages/e2e/src/conditions/device-conditions.ts
8068
+ var ONLINE2 = { kind: "online" };
8069
+ var REAL_CLOCK = { kind: "real" };
8070
+ function a11yOpOf(config) {
8071
+ const { fontScale: _scale, ...op } = config;
8072
+ return op;
8073
+ }
8074
+ var stderrLog = (line) => {
8075
+ process.stderr.write(`${line}
8076
+ `);
8077
+ };
8078
+ function invalid2(message) {
8079
+ return new E2eError2("INVALID_ARGUMENT", message);
8080
+ }
8081
+ function durationMs(value, what) {
8082
+ if (typeof value === "number") {
8083
+ if (!Number.isFinite(value) || value < 0)
8084
+ throw invalid2(`${what} must be 0 or more, got ${String(value)}`);
8085
+ return value;
8086
+ }
8087
+ const parsed = parseDuration(value);
8088
+ if (!parsed.success)
8089
+ throw invalid2(`${what}: ${parsed.error}`);
8090
+ return parsed.data;
8091
+ }
8092
+ function network(input) {
8093
+ const condition = toNetworkCondition(input);
8094
+ if (!condition.success)
8095
+ throw invalid2(condition.error);
8096
+ return condition.data;
8097
+ }
8098
+ function instant(at) {
8099
+ const date = at instanceof Date ? new Date(at.getTime()) : new Date(at);
8100
+ if (Number.isNaN(date.getTime()))
8101
+ throw invalid2(`invalid time ${JSON.stringify(at)} (use an ISO 8601 string, a Date or epoch ms)`);
8102
+ return date;
8103
+ }
8104
+ function validatePoint(point) {
8105
+ const ok = Number.isFinite(point.lat) && Number.isFinite(point.lon) && Math.abs(point.lat) <= 90 && Math.abs(point.lon) <= 180;
8106
+ if (!ok)
8107
+ throw invalid2(`invalid location ${point.lat},${point.lon} (lat within 90, lon within 180)`);
8108
+ }
8109
+ var SCENARIOS = ["city-run", "city-bike", "freeway"];
8110
+ function locationOf(input) {
8111
+ if ("kind" in input)
8112
+ return input;
8113
+ validatePoint(input);
8114
+ return { kind: "point", lat: input.lat, lon: input.lon };
8115
+ }
8116
+ var PACKAGE_SRC = `${resolve3(import.meta.dir, "..")}/`;
8117
+ function callingSpecDir() {
8118
+ const file = callerFile(new Error().stack, (path) => path.startsWith(PACKAGE_SRC) && !path.endsWith(".test.ts"));
8119
+ return file === "unknown" ? undefined : dirname5(file);
8120
+ }
8121
+ var FACINGS = {
8122
+ back: ["back"],
8123
+ front: ["front"],
8124
+ both: ["back", "front"]
8125
+ };
8126
+ function facingsOf(choice) {
8127
+ const facings = FACINGS[choice ?? "back"];
8128
+ if (facings === undefined)
8129
+ throw invalid2(`unknown camera facing "${String(choice)}" (use back, front or both)`);
8130
+ return facings;
8131
+ }
8132
+ var registry2 = new WeakMap;
8133
+ function registerConditions(device, internals) {
8134
+ registry2.set(device, internals);
8135
+ }
8136
+ function conditionsOf(device) {
8137
+ return registry2.get(device);
8138
+ }
8139
+ function conditionSinkFor(session) {
8140
+ return session.results === undefined ? undefined : openConditionSink(session.results.path);
8141
+ }
8142
+ function createDeviceConditions(deps) {
8143
+ const { driver, env } = deps;
8144
+ const ticker = deps.tickerClock ?? realTickerClock;
8145
+ const log = deps.log ?? stderrLog;
8146
+ const normalized = normalizeConditions(env.conditions ?? {});
8147
+ if (!normalized.success)
8148
+ throw new E2eError2("SESSION", `invalid conditions: ${normalized.error}`);
8149
+ const baseline = normalized.data.conditions;
8150
+ let currentNetwork = ONLINE2;
8151
+ let currentFaults = [];
8152
+ let currentClock = REAL_CLOCK;
8153
+ let clockSetAt = ticker.now();
8154
+ let networkDirty = false;
8155
+ let locationDirty = false;
8156
+ let clockDirty = false;
8157
+ let permissionsDirty = false;
8158
+ let localeDirty = false;
8159
+ let a11yDirty = false;
8160
+ let batteryDirty = false;
8161
+ let cameraDirty = false;
8162
+ let currentCamera = {};
8163
+ const permissionStates = new Map;
8164
+ let permissionBundle;
8165
+ let currentLocale = {};
8166
+ let currentA11y = {};
8167
+ let flap;
8168
+ const runs = new Set;
8169
+ const recorder = deps.recorder;
8170
+ const emit = (event) => {
8171
+ deps.sink?.({ ...event, type: "condition", at: ticker.now() });
8172
+ };
8173
+ async function call(what, run) {
8174
+ const result = await settled(run, what, deps.signal());
8175
+ if (result.success)
8176
+ return result.data;
8177
+ const error = toE2eError(result.error, what);
8178
+ if (result.error.code === "E_NETWORK_RELAUNCH") {
8179
+ throw new E2eError2(error.code, `${error.message}
8180
+ the app runs without the warden network dylib: declare \`conditions.network\` for the suite so it launches with it, or pass { relaunch: true }`);
8181
+ }
8182
+ throw error;
8183
+ }
8184
+ const target = (options) => ({
8185
+ network: async (condition) => {
8186
+ const result = await driver.network(condition, options?.relaunch === undefined ? undefined : { relaunch: options.relaunch });
8187
+ if (result.success)
8188
+ recorder?.record("network", condition);
8189
+ return result;
8190
+ },
8191
+ location: async (op) => {
8192
+ const result = await driver.location(op);
8193
+ if (result.success)
8194
+ recorder?.record("location", op);
8195
+ return result;
8196
+ }
8197
+ });
8198
+ function track(scheduler) {
8199
+ const run = {
8200
+ async stop() {
8201
+ scheduler.stop();
8202
+ const outcome = await scheduler.done;
8203
+ runs.delete(run);
8204
+ if (flap?.run === run)
8205
+ flap = undefined;
8206
+ return outcome;
8207
+ },
8208
+ done: scheduler.done,
8209
+ status: () => scheduler.current()
8210
+ };
8211
+ runs.add(run);
8212
+ return run;
8213
+ }
8214
+ async function stopFlap() {
8215
+ const running = flap;
8216
+ if (running === undefined)
8217
+ return;
8218
+ await running.run.stop();
8219
+ }
8220
+ async function applyNetwork(condition, options) {
8221
+ await stopFlap();
8222
+ networkDirty = true;
8223
+ const opts = options?.relaunch === undefined ? undefined : { relaunch: options.relaunch };
8224
+ const state = await call("network", () => driver.network(condition, opts));
8225
+ currentNetwork = condition;
8226
+ recorder?.record("network", condition);
8227
+ emit({ area: "network", value: condition });
8228
+ return state;
8229
+ }
8230
+ async function startFlapRun(options, netOptions) {
8231
+ const upMs = durationMs(options.up, "flap up");
8232
+ const downMs = durationMs(options.down, "flap down");
8233
+ if (!(upMs > 0 && downMs > 0))
8234
+ throw invalid2("flap needs up and down above 0");
8235
+ const base = options.base === undefined ? ONLINE2 : network(options.base);
8236
+ if (base.kind === "flap")
8237
+ throw invalid2("flap base cannot itself be a flap");
8238
+ await applyNetwork(base, netOptions);
8239
+ const scheduler = startFlap(target(netOptions), {
8240
+ upMs,
8241
+ downMs,
8242
+ base,
8243
+ applyInitial: false,
8244
+ ...options.jitterPct === undefined ? {} : { jitterPct: options.jitterPct },
8245
+ ...options.seed === undefined ? {} : { seed: options.seed }
8246
+ }, { ...ticker, ...deps.sink === undefined ? {} : { sink: deps.sink } });
8247
+ const run = track(scheduler);
8248
+ flap = { run, options };
8249
+ currentNetwork = {
8250
+ kind: "flap",
8251
+ upMs,
8252
+ downMs,
8253
+ base,
8254
+ ...options.jitterPct === undefined ? {} : { jitterPct: options.jitterPct }
8255
+ };
8256
+ emit({ area: "network", value: currentNetwork });
8257
+ return run;
8258
+ }
8259
+ async function setNetwork(input, options) {
8260
+ const condition = network(input);
8261
+ if (condition.kind === "flap") {
8262
+ await startFlapRun({
8263
+ up: condition.upMs,
8264
+ down: condition.downMs,
8265
+ ...condition.jitterPct === undefined ? {} : { jitterPct: condition.jitterPct },
8266
+ ...condition.base === undefined ? {} : { base: condition.base }
8267
+ }, options);
8268
+ return { online: true, condition, needsProxy: [] };
8269
+ }
8270
+ return applyNetwork(condition, options);
8271
+ }
8272
+ async function restoreNetwork(previous, previousFlap, options) {
8273
+ if (previous.kind === "flap" && previousFlap !== undefined)
8274
+ await startFlapRun(previousFlap, options);
8275
+ else
8276
+ await applyNetwork(previous, options);
8277
+ }
8278
+ async function resetNetwork() {
8279
+ await stopFlap();
8280
+ if (currentFaults.length > 0)
8281
+ await setFaults([]);
8282
+ const goal = baseline.network ?? ONLINE2;
8283
+ await applyNetwork(goal);
8284
+ networkDirty = false;
8285
+ recorder?.settle("network", goal, goal.kind === "online");
8286
+ }
8287
+ async function setFaults(rules) {
8288
+ networkDirty = true;
8289
+ const result = await call("network faults", () => driver.networkFaults(rules));
8290
+ currentFaults = rules;
8291
+ recorder?.record("network", currentNetwork);
8292
+ emit({ area: "faults", value: { kind: "faults", rules } });
8293
+ return result;
8294
+ }
8295
+ const networkApi = {
8296
+ set: setNetwork,
8297
+ async during(condition, fn, options) {
8298
+ const previous = currentNetwork;
8299
+ const previousFlap = flap?.options;
8300
+ await setNetwork(condition, options);
8301
+ let result;
8302
+ try {
8303
+ result = await fn();
8304
+ } catch (error) {
8305
+ await restoreNetwork(previous, previousFlap, options).catch((restoreError) => log(`[warden-e2e] network.during: restoring the previous network failed: ${String(restoreError instanceof Error ? restoreError.message : restoreError)}`));
8306
+ throw error;
8307
+ }
8308
+ await restoreNetwork(previous, previousFlap, options);
8309
+ return result;
8310
+ },
8311
+ flap: (options) => startFlapRun(options),
8312
+ fault: (rules) => setFaults("match" in rules ? [rules] : rules),
8313
+ async status() {
8314
+ return { condition: currentNetwork, faults: currentFaults, flapping: flap !== undefined };
8315
+ },
8316
+ reset: resetNetwork
8317
+ };
8318
+ async function applyLocation(op) {
8319
+ locationDirty = true;
8320
+ await call("location", () => driver.location(op));
8321
+ recorder?.record("location", op);
8322
+ emit({ area: "location", value: op });
8323
+ }
8324
+ const locationApi = {
8325
+ async set(point) {
8326
+ validatePoint(point);
8327
+ await applyLocation({
8328
+ kind: "point",
8329
+ lat: point.lat,
8330
+ lon: point.lon,
8331
+ ...point.altM === undefined ? {} : { altM: point.altM }
8332
+ });
8333
+ },
8334
+ async route(points, options = {}) {
8335
+ const pace = {
8336
+ ...options.speedMps === undefined ? {} : { speedMps: options.speedMps }
8337
+ };
8338
+ if ("gpx" in points) {
8339
+ await applyLocation({ kind: "gpx", path: resolve3(points.gpx), ...pace });
8340
+ return;
8341
+ }
8342
+ if (points.length === 0)
8343
+ throw invalid2("a route needs at least one point");
8344
+ for (const point of points)
8345
+ validatePoint(point);
8346
+ await applyLocation({
8347
+ kind: "route",
8348
+ points,
8349
+ ...pace,
8350
+ ...options.intervalS === undefined ? {} : { intervalS: options.intervalS },
8351
+ ...options.loop === undefined ? {} : { loop: options.loop }
8352
+ });
8353
+ },
8354
+ async scenario(name) {
8355
+ if (!SCENARIOS.includes(name))
8356
+ throw invalid2(`unknown scenario "${String(name)}" (use ${SCENARIOS.join(", ")})`);
8357
+ await applyLocation({ kind: "scenario", name });
8358
+ },
8359
+ clear: () => applyLocation({ kind: "clear" })
8360
+ };
8361
+ async function applyClock(condition) {
8362
+ const op = driver.clock;
8363
+ clockDirty = true;
8364
+ if (typeof op !== "function") {
8365
+ throw toE2eError({ code: "E_UNSUPPORTED", message: "this driver has no clock condition", retryable: false }, "clock");
8366
+ }
8367
+ await call("clock", () => op.call(driver, condition));
8368
+ currentClock = condition;
8369
+ clockSetAt = ticker.now();
8370
+ emit({ area: "clock", value: condition });
8371
+ }
8372
+ function clockFor(at, frozen) {
8373
+ const iso = instant(at).toISOString();
8374
+ return frozen === true ? { kind: "frozen", at: iso } : { kind: "fixed", at: iso };
8375
+ }
8376
+ function advanced(by) {
8377
+ switch (currentClock.kind) {
8378
+ case "real":
8379
+ return { kind: "offset", ms: by };
8380
+ case "offset":
8381
+ return { kind: "offset", ms: currentClock.ms + by };
8382
+ case "frozen":
8383
+ return { kind: "frozen", at: new Date(Date.parse(currentClock.at) + by).toISOString() };
8384
+ case "fixed": {
8385
+ const now = Date.parse(currentClock.at) + (ticker.now() - clockSetAt);
8386
+ return { kind: "fixed", at: new Date(now + by).toISOString() };
8387
+ }
8388
+ }
8389
+ }
8390
+ const clockApi = {
8391
+ async set(at, options) {
8392
+ await applyClock(clockFor(at, options?.frozen));
8393
+ },
8394
+ async advance(by) {
8395
+ await applyClock(advanced(durationMs(by, "advance")));
8396
+ },
8397
+ async during(at, fn, options) {
8398
+ const previous = currentClock;
8399
+ await applyClock(clockFor(at, options?.frozen));
8400
+ let result;
8401
+ try {
8402
+ result = await fn();
8403
+ } catch (error) {
8404
+ await applyClock(previous).catch((restoreError) => log(`[warden-e2e] clock.during: restoring the previous clock failed: ${String(restoreError instanceof Error ? restoreError.message : restoreError)}`));
8405
+ throw error;
8406
+ }
8407
+ await applyClock(previous);
8408
+ return result;
8409
+ },
8410
+ async reset() {
8411
+ await applyClock(baseline.clock ?? REAL_CLOCK);
8412
+ clockDirty = false;
8413
+ },
8414
+ status: () => currentClock
8415
+ };
8416
+ function settingOp(name) {
8417
+ const op = driver[name];
8418
+ if (typeof op !== "function") {
8419
+ throw toE2eError({ code: "E_UNSUPPORTED", message: `this driver has no ${name} op`, retryable: false }, name);
8420
+ }
8421
+ return op.bind(driver);
8422
+ }
8423
+ const recordPermissions = (bundleId) => {
8424
+ const value = {
8425
+ grants: [...permissionStates].map(([permission, state]) => ({ bundleId, permission, state }))
8426
+ };
8427
+ recorder?.record("permission", value);
8428
+ emit({ area: "permission", value });
8429
+ };
8430
+ async function applyPermissions(grants, options) {
8431
+ permissionsDirty = true;
8432
+ const op = settingOp("permissions");
8433
+ const state = await call("permissions", () => op(grants, options ?? { bundleId: env.bundleId }));
8434
+ for (const { permission, state: applied } of state.applied)
8435
+ permissionStates.set(permission, applied);
8436
+ permissionBundle = state.bundleId;
8437
+ recordPermissions(state.bundleId);
8438
+ return state;
8439
+ }
8440
+ const permissionsApi = {
8441
+ set: applyPermissions,
8442
+ async reset(options) {
8443
+ const baselinePermissions = baseline.permissions ?? {};
8444
+ const back = {};
8445
+ for (const name of permissionStates.keys())
8446
+ back[name] = baselinePermissions[name] ?? "unset";
8447
+ if (Object.keys(back).length === 0)
8448
+ return;
8449
+ await applyPermissions(back, options ?? (permissionBundle ? { bundleId: permissionBundle } : undefined));
8450
+ permissionsDirty = false;
8451
+ permissionStates.clear();
8452
+ recorder?.settle("permission", { grants: [] }, true);
8453
+ }
8454
+ };
8455
+ function recordLocale() {
8456
+ const empty = currentLocale.locale === undefined && currentLocale.timezone === undefined;
8457
+ if (empty)
8458
+ recorder?.settle("locale", currentLocale, true);
8459
+ else
8460
+ recorder?.record("locale", currentLocale);
8461
+ emit({ area: "locale", value: currentLocale });
8462
+ }
8463
+ async function applyTimezone(tz) {
8464
+ localeDirty = true;
8465
+ const op = settingOp("timezone");
8466
+ const previous = currentLocale.previousTimezone;
8467
+ const state = await call("timezone", () => op(tz, previous === undefined ? undefined : { previous }));
8468
+ const { timezone: _gone, ...rest } = currentLocale;
8469
+ currentLocale = {
8470
+ ...rest,
8471
+ ...tz === null ? {} : { timezone: tz },
8472
+ ...previous ?? state.previous ? { previousTimezone: previous ?? state.previous } : {}
8473
+ };
8474
+ recordLocale();
8475
+ return state;
8476
+ }
8477
+ async function applyLocale(tag, options) {
8478
+ localeDirty = true;
8479
+ const op = settingOp("locale");
8480
+ const state = await call("locale", () => op(tag, options ?? { bundleId: env.bundleId }));
8481
+ const { locale: _gone, ...rest } = currentLocale;
8482
+ currentLocale = {
8483
+ ...rest,
8484
+ ...tag === null ? {} : { locale: tag },
8485
+ ...state.bundleId === undefined ? {} : { bundleId: state.bundleId }
8486
+ };
8487
+ recordLocale();
8488
+ return state;
8489
+ }
8490
+ function checkedA11y(op) {
8491
+ if (op === null)
8492
+ return;
8493
+ if (op.textSize !== undefined) {
8494
+ const size = resolveTextSize(op.textSize);
8495
+ if (!size.success)
8496
+ throw invalid2(size.error);
8497
+ }
8498
+ if (Object.keys(op).length === 0)
8499
+ throw invalid2("a11y needs at least one of textSize, boldText, contrast, reduceMotion");
8500
+ }
8501
+ async function applyA11y(op) {
8502
+ checkedA11y(op);
8503
+ a11yDirty = true;
8504
+ const run = settingOp("a11y");
8505
+ const state = await call("a11y", () => run(op));
8506
+ const resolved = op?.textSize === undefined ? undefined : resolveTextSize(op.textSize);
8507
+ currentA11y = op === null ? {} : {
8508
+ ...currentA11y,
8509
+ ...appliedA11y(op, state.applied),
8510
+ ...resolved?.success ? { fontScale: resolved.data.scale } : {}
8511
+ };
8512
+ if (op?.screenReader === false && currentA11y.screenReaderPrevious !== undefined) {
8513
+ const { screenReader: _off, screenReaderPrevious: _prior, ...rest } = currentA11y;
8514
+ currentA11y = rest;
8515
+ } else if (state.screenReaderPrevious !== undefined) {
8516
+ currentA11y = { ...currentA11y, screenReaderPrevious: state.screenReaderPrevious };
8517
+ }
8518
+ if (Object.keys(currentA11y).length === 0)
8519
+ recorder?.settle("a11y", currentA11y, true);
8520
+ else
8521
+ recorder?.record("a11y", currentA11y);
8522
+ emit({ area: "a11y", value: currentA11y });
8523
+ return state;
8524
+ }
8525
+ async function applyBattery(op) {
8526
+ batteryDirty = true;
8527
+ const run = settingOp("battery");
8528
+ const state = await call("battery", () => run(op));
8529
+ const value = op === null ? {} : { ...op, cosmetic: state.cosmetic };
8530
+ if (op === null)
8531
+ recorder?.settle("battery", value, true);
8532
+ else
8533
+ recorder?.record("battery", value);
8534
+ emit({ area: "battery", value });
8535
+ return state;
8536
+ }
8537
+ const home = deps.wardenHome ?? wardenHome(process.env);
8538
+ async function applyCamera(next) {
8539
+ cameraDirty = true;
8540
+ const run = settingOp("camera");
8541
+ const empty = Object.keys(next).length === 0;
8542
+ const state = await call("camera", () => run(empty ? null : next));
8543
+ currentCamera = state.feeds;
8544
+ if (Object.keys(currentCamera).length === 0)
8545
+ recorder?.settle("camera", currentCamera, true);
8546
+ else
8547
+ recorder?.record("camera", currentCamera);
8548
+ emit({ area: "camera", value: currentCamera });
8549
+ return state;
8550
+ }
8551
+ function stage(facing, source, loop) {
8552
+ const staged = stageCameraFeed({ wardenHome: home, udid: env.udid, facing, source, loop });
8553
+ if (!staged.success) {
8554
+ throw invalid2(`camera failed: ${staged.error.code}: ${staged.error.message}`);
8555
+ }
8556
+ return staged.data;
8557
+ }
8558
+ function specPath(path, dir) {
8559
+ if (typeof path !== "string" || path === "")
8560
+ throw invalid2("camera.set needs the path of an image or video");
8561
+ return resolve3(dir ?? (env.specsDir !== undefined ? resolve3(env.specsDir) : process.cwd()), path);
8562
+ }
8563
+ const cameraApi = {
8564
+ async set(path, options = {}) {
8565
+ const dir = (deps.specDir ?? callingSpecDir)();
8566
+ const source = specPath(path, dir);
8567
+ const facings = facingsOf(options.facing);
8568
+ const loop = options.loop ?? true;
8569
+ const next = { ...currentCamera };
8570
+ for (const facing of facings) {
8571
+ const feed = stage(facing, source, loop);
8572
+ if (feed !== undefined)
8573
+ next[facing] = feed;
8574
+ }
8575
+ return applyCamera(next);
8576
+ },
8577
+ async clear(facing = "both") {
8578
+ const next = { ...currentCamera };
8579
+ for (const f of facingsOf(facing))
8580
+ delete next[f];
8581
+ return applyCamera(next);
8582
+ },
8583
+ async status() {
8584
+ return { feeds: currentCamera };
8585
+ }
8586
+ };
8587
+ function baselineCamera() {
8588
+ const feeds = {};
8589
+ for (const facing of ["back", "front"]) {
8590
+ const configured = env.cameraFeeds?.[facing];
8591
+ if (configured === undefined)
8592
+ continue;
8593
+ const feed = stage(facing, configured.path, configured.loop);
8594
+ if (feed !== undefined)
8595
+ feeds[facing] = feed;
8596
+ }
8597
+ return feeds;
8598
+ }
8599
+ async function startTimelineRun(steps) {
8600
+ const converted = steps.map((step, index) => {
8601
+ const atMs = durationMs(step.at, `timeline step ${index} at`);
8602
+ const converted = { atMs };
8603
+ if (step.network !== undefined) {
8604
+ const condition = network(step.network);
8605
+ if (condition.kind === "flap")
8606
+ throw invalid2(`timeline step ${index}: use device.network.flap for a flap`);
8607
+ converted.network = condition;
8608
+ }
8609
+ if (step.location !== undefined)
8610
+ converted.location = locationOf(step.location);
8611
+ return converted;
8612
+ });
8613
+ networkDirty = true;
8614
+ locationDirty = true;
8615
+ const scheduler = startTimeline(target(), converted, {
8616
+ ...ticker,
8617
+ ...deps.sink === undefined ? {} : { sink: deps.sink }
8618
+ });
8619
+ const status = scheduler.current();
8620
+ if (status.state === "failed")
8621
+ throw invalid2(status.error);
8622
+ const run = track(scheduler);
8623
+ const last = converted.at(-1);
8624
+ if (last?.network !== undefined)
8625
+ currentNetwork = last.network;
8626
+ return run;
8627
+ }
8628
+ async function stopRuns() {
8629
+ const failures = [];
8630
+ for (const run of [...runs]) {
8631
+ const outcome = await run.stop();
8632
+ if (outcome.stopped === "failed")
8633
+ failures.push(outcome.error);
8634
+ }
8635
+ if (failures.length > 0) {
8636
+ throw new E2eError2("ACTION_FAILED", `a network flap or timeline stopped on its own after failing: ${failures[0]}`);
8637
+ }
8638
+ }
8639
+ const declared = baseline.network !== undefined || baseline.location !== undefined || baseline.clock !== undefined || baseline.permissions !== undefined || baseline.locale !== undefined || baseline.timezone !== undefined || baseline.a11y !== undefined || env.cameraFeeds !== undefined;
8640
+ const internals = {
8641
+ declared,
8642
+ dirty: () => networkDirty || locationDirty || clockDirty || permissionsDirty || localeDirty || a11yDirty || batteryDirty || cameraDirty || runs.size > 0,
8643
+ wantsInject: () => baseline.network !== undefined || baseline.clock !== undefined || currentNetwork.kind !== "online" || currentClock.kind !== "real",
8644
+ async applyBaseline() {
8645
+ if (baseline.permissions !== undefined) {
8646
+ for (const [name, state] of Object.entries(baseline.permissions))
8647
+ permissionStates.set(name, state);
8648
+ await applyPermissions(baseline.permissions);
8649
+ }
8650
+ if (baseline.timezone !== undefined)
8651
+ await applyTimezone(baseline.timezone);
8652
+ if (baseline.locale !== undefined)
8653
+ await applyLocale(baseline.locale);
8654
+ if (baseline.a11y !== undefined)
8655
+ await applyA11y(a11yOpOf(baseline.a11y));
8656
+ if (baseline.clock !== undefined)
8657
+ await applyClock(baseline.clock);
8658
+ if (baseline.location !== undefined)
8659
+ await applyLocation(baseline.location);
8660
+ if (baseline.network !== undefined)
8661
+ await applyNetwork(baseline.network, { relaunch: true });
8662
+ if (env.cameraFeeds !== undefined)
8663
+ await applyCamera(baselineCamera());
8664
+ cameraDirty = false;
8665
+ networkDirty = false;
8666
+ locationDirty = false;
8667
+ clockDirty = false;
8668
+ permissionsDirty = false;
8669
+ localeDirty = false;
8670
+ a11yDirty = false;
8671
+ batteryDirty = false;
8672
+ },
8673
+ async teardown() {
8674
+ const errors = [];
8675
+ const attempt = async (run) => {
8676
+ try {
8677
+ await run();
8678
+ } catch (error) {
8679
+ errors.push(error);
8680
+ }
8681
+ };
8682
+ await attempt(stopRuns);
8683
+ if (networkDirty)
8684
+ await attempt(resetNetwork);
8685
+ if (locationDirty) {
8686
+ await attempt(async () => {
8687
+ const goal = baseline.location ?? { kind: "clear" };
8688
+ await applyLocation(goal);
8689
+ locationDirty = false;
8690
+ recorder?.settle("location", goal, goal.kind === "clear");
8691
+ });
8692
+ }
8693
+ if (clockDirty)
8694
+ await attempt(clockApi.reset);
8695
+ if (permissionsDirty)
8696
+ await attempt(() => permissionsApi.reset());
8697
+ if (localeDirty) {
8698
+ await attempt(async () => {
8699
+ await applyTimezone(baseline.timezone ?? null);
8700
+ await applyLocale(baseline.locale ?? null);
8701
+ localeDirty = false;
8702
+ });
8703
+ }
8704
+ if (a11yDirty) {
8705
+ await attempt(async () => {
8706
+ await applyA11y(null);
8707
+ if (baseline.a11y !== undefined)
8708
+ await applyA11y(a11yOpOf(baseline.a11y));
8709
+ a11yDirty = false;
8710
+ });
8711
+ }
8712
+ if (batteryDirty) {
8713
+ await attempt(async () => {
8714
+ await applyBattery(null);
8715
+ batteryDirty = false;
8716
+ });
8717
+ }
8718
+ if (cameraDirty) {
8719
+ await attempt(async () => {
8720
+ await applyCamera(baselineCamera());
8721
+ cameraDirty = false;
8722
+ });
8723
+ }
8724
+ if (errors.length > 0)
8725
+ throw errors[0];
8726
+ }
8727
+ };
8728
+ return {
8729
+ network: networkApi,
8730
+ location: locationApi,
8731
+ clock: clockApi,
8732
+ permissions: permissionsApi,
8733
+ timezone: applyTimezone,
8734
+ locale: applyLocale,
8735
+ a11y: applyA11y,
8736
+ battery: applyBattery,
8737
+ camera: cameraApi,
8738
+ memoryWarning: async (options) => {
8739
+ const op = settingOp("memoryWarning");
8740
+ await call("memoryWarning", () => op(options ?? { bundleId: env.bundleId }));
8741
+ },
8742
+ push: async (payload, options) => {
8743
+ const op = settingOp("push");
8744
+ await call("push", () => op(payload, options ?? { bundleId: env.bundleId }));
8745
+ },
8746
+ timeline: startTimelineRun,
8747
+ internals
8748
+ };
8749
+ }
8750
+
8751
+ // ../../packages/e2e/src/perf/device-perf.ts
8752
+ import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync6, writeFileSync } from "fs";
8753
+ import { join as join6 } from "path";
8754
+
8755
+ // ../../packages/e2e/src/perf/budgets.ts
8756
+ var BUDGET_KEYS = ["startupColdMs", "jankPct", "maxCommitMs", "p95CpuPct", "peakMemoryMb"];
8757
+ function measured(report, metric) {
8758
+ const m = report.metrics;
8759
+ switch (metric) {
8760
+ case "startupColdMs":
8761
+ return m.startupColdMs;
8762
+ case "jankPct":
8763
+ return m.jankPct;
8764
+ case "maxCommitMs":
8765
+ return m.topCommitMs;
8766
+ case "p95CpuPct":
8767
+ return m.cpuP95Pct;
8768
+ case "peakMemoryMb": {
8769
+ const kb = Math.max(m.peakPssKb ?? 0, m.peakRssKb ?? 0);
8770
+ return kb > 0 ? kb / 1024 : undefined;
8771
+ }
8772
+ }
8773
+ }
8774
+ function checkBudgets(report, base, overrides) {
8775
+ const budgets = { ...base, ...overrides };
8776
+ const breaches = [];
8777
+ for (const metric of BUDGET_KEYS) {
8778
+ const budget = budgets[metric];
8779
+ if (budget === undefined)
8780
+ continue;
8781
+ const value = measured(report, metric);
8782
+ if (value !== undefined && value > budget)
8783
+ breaches.push({ metric, value, budget });
8784
+ }
8785
+ return breaches;
8786
+ }
8787
+ var round = (n) => Math.round(n * 10) / 10;
8788
+ function describeBreaches(breaches, reportPath) {
8789
+ const lines = breaches.map((b) => ` ${b.metric}: ${round(b.value)} > budget ${b.budget}`);
8790
+ return [
8791
+ `perf_budget: ${breaches.length} metric${breaches.length === 1 ? "" : "s"} over budget`,
8792
+ ...lines,
8793
+ ...reportPath === undefined ? [] : [` report: ${reportPath}`]
8794
+ ].join(`
8795
+ `);
8796
+ }
8797
+ function parseBudgets(raw) {
8798
+ if (raw === undefined || raw === "")
8799
+ return {};
8800
+ let json;
8801
+ try {
8802
+ json = JSON.parse(raw);
8803
+ } catch {
8804
+ return {};
8805
+ }
8806
+ if (typeof json !== "object" || json === null)
8807
+ return {};
8808
+ const out = {};
8809
+ for (const key of BUDGET_KEYS) {
8810
+ const v = json[key];
8811
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0)
8812
+ out[key] = v;
8813
+ }
8814
+ return out;
8815
+ }
8816
+
8817
+ // ../../packages/e2e/src/perf/device-perf.ts
8818
+ var PERF_DIR = "perf";
8819
+ function parseEnvelope2(stdout) {
8820
+ try {
8821
+ const json = JSON.parse(stdout.trim().split(`
8822
+ `).at(-1) ?? "");
8823
+ if (typeof json !== "object" || json === null || !("ok" in json))
8824
+ return;
8825
+ return json;
8826
+ } catch {
8827
+ return;
8828
+ }
8829
+ }
8830
+ var flagsOf = (o) => [
8831
+ ...o.js ? ["--js"] : [],
8832
+ ...o.react ? ["--react"] : [],
8833
+ ...typeof o.native === "string" ? [`--native=${o.native}`] : o.native ? ["--native"] : [],
8834
+ ...o.metrics ? ["--metrics"] : [],
8835
+ ...o.frames ? ["--frames"] : [],
8836
+ ...o.logs ? ["--logs"] : [],
8837
+ ...o.all ? ["--all"] : [],
8838
+ ...o.relaunch ? ["--relaunch"] : [],
8839
+ ...o.reset ? ["--reset"] : [],
8840
+ ...o.maxDurationMs === undefined ? [] : ["--max-duration", `${Math.ceil(o.maxDurationMs / 1000)}s`]
8841
+ ];
8842
+ var isRecord2 = (v) => typeof v === "object" && v !== null;
8843
+ function saveReport(deps, result, report, sessionId) {
8844
+ const sessionReport = typeof result.reportPath === "string" ? result.reportPath : undefined;
8845
+ const attempt = deps.attemptDir();
8846
+ if (attempt === undefined)
8847
+ return sessionReport;
8848
+ const out = join6(attempt, PERF_DIR);
8849
+ mkdirSync6(out, { recursive: true });
8850
+ const sessionDir = typeof result.dir === "string" ? result.dir : undefined;
8851
+ const write = (name, from, fallback) => {
8852
+ if (from !== undefined && existsSync4(from))
8853
+ copyFileSync2(from, join6(out, name));
8854
+ else if (fallback !== undefined)
8855
+ writeFileSync(join6(out, name), fallback);
8856
+ else
8857
+ return;
8858
+ deps.addFile?.(`${PERF_DIR}/${name}`);
8859
+ };
8860
+ write("report.json", sessionReport, `${JSON.stringify(report, null, 2)}
8861
+ `);
8862
+ write("report.md", sessionDir === undefined ? undefined : join6(sessionDir, "report.md"));
8863
+ write("session.json", undefined, `${JSON.stringify({ sessionId, dir: sessionDir }, null, 2)}
8864
+ `);
8865
+ return join6(out, "report.json");
8866
+ }
8867
+ function createDevicePerf(deps) {
8868
+ const run = async (sub, extra) => {
8869
+ if (deps.unavailable !== undefined)
8870
+ throw new E2eError2("SESSION", `device.perf unavailable: ${deps.unavailable}`);
8871
+ const { code, stdout, stderr } = await deps.exec(["perf", sub, "--json", "--udid", deps.udid, ...extra]);
5703
8872
  const env = parseEnvelope2(stdout);
5704
8873
  if (env === undefined || !env.ok) {
5705
8874
  const why = env === undefined ? stderr.trim() || `exit ${code}` : `${env.error.code}: ${env.error.message}`;
5706
8875
  throw new E2eError2("SESSION", `warden perf ${sub} failed: ${why}`);
5707
8876
  }
5708
- return isRecord(env.result) ? env.result : {};
8877
+ return isRecord2(env.result) ? env.result : {};
5709
8878
  };
5710
8879
  let active = false;
5711
8880
  return {
@@ -5727,7 +8896,7 @@ function createDevicePerf(deps) {
5727
8896
  const result = await run("stop", []);
5728
8897
  active = false;
5729
8898
  const report = result.report;
5730
- if (!isRecord(report))
8899
+ if (!isRecord2(report))
5731
8900
  throw new E2eError2("SESSION", "warden perf stop returned no report");
5732
8901
  const sessionId = typeof result.sessionId === "string" ? result.sessionId : "";
5733
8902
  const reportPath = saveReport(deps, result, report, sessionId);
@@ -5764,21 +8933,21 @@ function spawnWarden(bin) {
5764
8933
  }
5765
8934
 
5766
8935
  // ../../packages/e2e/src/runner/artifacts.ts
5767
- import { createHash } from "crypto";
5768
- import { basename, extname, join as join3, relative, sep } from "path";
8936
+ import { createHash as createHash2 } from "crypto";
8937
+ import { basename, extname, join as join7, relative, sep } from "path";
5769
8938
  var SLUG_MAX = 60;
5770
8939
  function slug(text, fallback = "test") {
5771
8940
  const slugged = text.normalize("NFKD").replace(/[\u0300-\u036F]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX).replace(/-+$/g, "");
5772
8941
  return slugged === "" ? fallback : slugged;
5773
8942
  }
5774
8943
  function hash6(...parts) {
5775
- return createHash("sha1").update(parts.join("\x00")).digest("hex").slice(0, 6);
8944
+ return createHash2("sha1").update(parts.join("\x00")).digest("hex").slice(0, 6);
5776
8945
  }
5777
8946
  function attemptDir(out, specId, title, hash) {
5778
- return join3(out, specId, `${slug(title.join(" "))}-${hash.slice(0, 6)}`);
8947
+ return join7(out, specId, `${slug(title.join(" "))}-${hash.slice(0, 6)}`);
5779
8948
  }
5780
8949
  function screenshotPath(dir, n, name) {
5781
- return join3(dir, "screenshots", `${String(n).padStart(3, "0")}-${slug(name, "screenshot")}.png`);
8950
+ return join7(dir, "screenshots", `${String(n).padStart(3, "0")}-${slug(name, "screenshot")}.png`);
5782
8951
  }
5783
8952
  var SPEC_EXT = /\.e2e$/;
5784
8953
  function specIdOf(file, specsDir) {
@@ -5795,9 +8964,9 @@ function specIdOf(file, specsDir) {
5795
8964
  }
5796
8965
 
5797
8966
  // ../../packages/e2e/src/devtools/native-log.ts
5798
- import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
8967
+ import { copyFileSync as copyFileSync3, mkdirSync as mkdirSync7, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
5799
8968
  import { homedir } from "os";
5800
- import { dirname as dirname3, join as join4 } from "path";
8969
+ import { dirname as dirname6, join as join8 } from "path";
5801
8970
  var NATIVE_LOG = "native.log";
5802
8971
  var DEFAULT_TIMEOUT_MS5 = 1e4;
5803
8972
  var SLACK_S = 5;
@@ -5806,23 +8975,23 @@ function realNativeFs() {
5806
8975
  return {
5807
8976
  listReports: (dir) => {
5808
8977
  try {
5809
- return readdirSync(dir).map((name) => ({ name, mtimeMs: statSync(join4(dir, name)).mtimeMs }));
8978
+ return readdirSync(dir).map((name) => ({ name, mtimeMs: statSync(join8(dir, name)).mtimeMs }));
5810
8979
  } catch {
5811
8980
  return [];
5812
8981
  }
5813
8982
  },
5814
8983
  copy: (from, to) => {
5815
- mkdirSync4(dirname3(to), { recursive: true });
5816
- copyFileSync2(from, to);
8984
+ mkdirSync7(dirname6(to), { recursive: true });
8985
+ copyFileSync3(from, to);
5817
8986
  },
5818
8987
  write: (path, text) => {
5819
- mkdirSync4(dirname3(path), { recursive: true });
8988
+ mkdirSync7(dirname6(path), { recursive: true });
5820
8989
  writeFileSync2(path, text);
5821
8990
  }
5822
8991
  };
5823
8992
  }
5824
8993
  function realNativeLogDeps(exec, adb) {
5825
- return { exec, adb, fs: realNativeFs(), reportsDir: join4(homedir(), "Library", "Logs", "DiagnosticReports") };
8994
+ return { exec, adb, fs: realNativeFs(), reportsDir: join8(homedir(), "Library", "Logs", "DiagnosticReports") };
5826
8995
  }
5827
8996
  function tail(text) {
5828
8997
  return text.length > MAX_BYTES ? text.slice(text.length - MAX_BYTES) : text;
@@ -5838,7 +9007,7 @@ async function iosProcessName(target, exec, timeoutMs) {
5838
9007
  const app = container.stdout.trim();
5839
9008
  if (container.exitCode !== 0 || !app)
5840
9009
  return { error: failed(`app container of ${target.bundleId}`, container) };
5841
- const name = await exec(["plutil", "-extract", "CFBundleExecutable", "raw", "-o", "-", join4(app, "Info.plist")], {
9010
+ const name = await exec(["plutil", "-extract", "CFBundleExecutable", "raw", "-o", "-", join8(app, "Info.plist")], {
5842
9011
  timeoutMs
5843
9012
  });
5844
9013
  const exe = name.stdout.trim();
@@ -5867,7 +9036,7 @@ async function iosNativeLog(target, deps, win, timeoutMs) {
5867
9036
  `process == "${proc.exe}"`
5868
9037
  ], { timeoutMs });
5869
9038
  if (show.exitCode === 0) {
5870
- deps.fs.write(join4(win.dir, NATIVE_LOG), tail(show.stdout));
9039
+ deps.fs.write(join8(win.dir, NATIVE_LOG), tail(show.stdout));
5871
9040
  capture.files.push(NATIVE_LOG);
5872
9041
  } else {
5873
9042
  capture.problems.push(`native log: ${failed("log show", show)}`);
@@ -5875,7 +9044,7 @@ async function iosNativeLog(target, deps, win, timeoutMs) {
5875
9044
  for (const report of deps.fs.listReports(deps.reportsDir)) {
5876
9045
  if (!report.name.startsWith(proc.exe) || !report.name.endsWith(".ips") || report.mtimeMs < win.sinceMs)
5877
9046
  continue;
5878
- deps.fs.copy(join4(deps.reportsDir, report.name), join4(win.dir, report.name));
9047
+ deps.fs.copy(join8(deps.reportsDir, report.name), join8(win.dir, report.name));
5879
9048
  capture.files.push(report.name);
5880
9049
  }
5881
9050
  return capture;
@@ -5897,7 +9066,7 @@ async function androidNativeLog(target, deps, win, timeoutMs) {
5897
9066
  crash.exitCode === 0 ? `--- crash buffer ---
5898
9067
  ${crash.stdout}` : ""
5899
9068
  ];
5900
- deps.fs.write(join4(win.dir, NATIVE_LOG), tail(sections.filter(Boolean).join(`
9069
+ deps.fs.write(join8(win.dir, NATIVE_LOG), tail(sections.filter(Boolean).join(`
5901
9070
  `)));
5902
9071
  capture.files.push(NATIVE_LOG);
5903
9072
  return capture;
@@ -5911,24 +9080,6 @@ async function captureNativeLog(target, deps, win) {
5911
9080
  }
5912
9081
  }
5913
9082
 
5914
- // ../../packages/e2e/src/runner/callsite.ts
5915
- import { join as join5 } from "path";
5916
- var OWN_FILES = new Set(["callsite.ts", "scope.ts", "test.ts", "attempt.ts"].map((name) => join5(import.meta.dir, name)).concat(join5(import.meta.dir, "..", "index.ts")));
5917
- var FRAME = /(?:\(|\s)((?:file:\/\/)?\/[^\s()]+?):\d+(?::\d+)?\)?\s*$/;
5918
- function callerFile(stack = new Error().stack) {
5919
- for (const line of (stack ?? "").split(`
5920
- `)) {
5921
- const raw = FRAME.exec(line)?.[1];
5922
- if (raw === undefined)
5923
- continue;
5924
- const path = raw.startsWith("file://") ? raw.slice("file://".length) : raw;
5925
- if (OWN_FILES.has(path) || path.includes("/node_modules/"))
5926
- continue;
5927
- return path;
5928
- }
5929
- return "unknown";
5930
- }
5931
-
5932
9083
  // ../../packages/e2e/src/runner/scope.ts
5933
9084
  var BUN_TIMEOUT_MARGIN_MS2 = 30000;
5934
9085
  var hookOwners = new WeakMap;
@@ -6005,7 +9156,8 @@ function registrationDefaults(env = process.env) {
6005
9156
  timeoutMs: parsed?.timeoutMs ?? DEFAULT_TIMEOUT_MS4,
6006
9157
  actionTimeoutMs: parsed?.actionTimeoutMs ?? DEFAULT_ACTION_TIMEOUT_MS,
6007
9158
  assertionTimeoutMs: parsed?.assertionTimeoutMs ?? DEFAULT_ASSERTION_TIMEOUT_MS,
6008
- platform: parsed?.platform
9159
+ platform: parsed?.platform,
9160
+ deviceKind: parsed?.deviceKind
6009
9161
  };
6010
9162
  }
6011
9163
  var ONLY_KEY = Symbol.for("@delacour/warden-e2e/only");
@@ -6018,6 +9170,6 @@ function onlyWasSeen() {
6018
9170
 
6019
9171
  // ../../packages/e2e/src/version.ts
6020
9172
  var WORKSPACE_VERSION = "0.0.0-workspace";
6021
- var WARDEN_E2E_VERSION2 = "0.6.0";
9173
+ var WARDEN_E2E_VERSION2 = "0.8.0";
6022
9174
 
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 };
9175
+ export { E2eError2, registerA11yTarget, a11yTargetOf, canActOnElements, 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, createLineSplitter, encodeRequest, bunSpawn, stdioTransport, socketTransport, createSimdClient, spawnSimd, connectSimd, isRecoverableLoss, CAMERA_FACINGS, CONDITION_AREAS, resolveNetwork, resolveTextSize, androidFontScale, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, accessibleName, announce, failing, formatIssues, toE2eError, settled, unwrap, parseUiadA11yEvents, parseInjectEvents, withA11yRead, renderA11yScreen, createDeviceA11y, IOS_DEVICE_CAPABILITIES, sniffCameraMedia, cameraFeedDir, clearCameraFeeds, isLeaseAlive, processAlive, describeOwner, isPhysicalIosId, wardenHome, defaultDbPath, ownerKey, openStore, callerFile, formatCoord, DEFAULT_SPEED_MPS, DEFAULT_INTERVAL_S, realTickerClock, startRouteTicker, startFlap, registerConditions, conditionsOf, conditionSinkFor, createDeviceConditions, parseBudgets, createDevicePerf, perfStartFor, spawnWarden, hash6, attemptDir, screenshotPath, specIdOf, realNativeLogDeps, captureNativeLog, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };