@delacour/warden-e2e 0.7.0 → 0.9.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 (94) hide show
  1. package/{chunk-empqr9n0.mjs → chunk-y0y7frfz.mjs} +2010 -169
  2. package/index.mjs +214 -13
  3. package/package.json +2 -2
  4. package/preload.mjs +3007 -800
  5. package/types/core/src/a11y/a11y.config.d.ts +47 -0
  6. package/types/core/src/affected/affected.schema.d.ts +49 -1
  7. package/types/core/src/android-keyguard.d.ts +2 -0
  8. package/types/core/src/batch/preset.schema.d.ts +12 -1
  9. package/types/core/src/builds/app-option.schema.d.ts +1 -1
  10. package/types/core/src/builds/config.schema.d.ts +93 -2
  11. package/types/core/src/builds/define-config.d.ts +55 -0
  12. package/types/core/src/builds/kinds.d.ts +6 -0
  13. package/types/core/src/camera-mode.d.ts +17 -0
  14. package/types/core/src/camera-placeholder.d.ts +9 -0
  15. package/types/core/src/camera-profile.d.ts +12 -0
  16. package/types/core/src/claim-errors.d.ts +34 -0
  17. package/types/core/src/conditions/a11y-keys.d.ts +45 -0
  18. package/types/core/src/conditions/camera-media.d.ts +30 -0
  19. package/types/core/src/conditions/conditions.config.d.ts +24 -0
  20. package/types/core/src/conditions/conditions.types.d.ts +30 -0
  21. package/types/core/src/conditions/inject-state.d.ts +1 -1
  22. package/types/core/src/ios-runner/build.d.ts +28 -0
  23. package/types/core/src/ios-runner/cache.d.ts +44 -0
  24. package/types/core/src/ios-runner/pack.d.ts +17 -0
  25. package/types/core/src/ios-runner/signing.d.ts +40 -0
  26. package/types/core/src/ios-runner/sim-build.d.ts +36 -0
  27. package/types/core/src/ios-runner/stage.d.ts +26 -0
  28. package/types/core/src/lease-events.d.ts +126 -0
  29. package/types/core/src/ownership.d.ts +44 -0
  30. package/types/core/src/providers/android.d.ts +10 -1
  31. package/types/core/src/providers/provider.types.d.ts +9 -0
  32. package/types/core/src/queue.d.ts +43 -0
  33. package/types/core/src/store.d.ts +61 -5
  34. package/types/core/src/types.d.ts +10 -0
  35. package/types/core/src/xcode.d.ts +22 -0
  36. package/types/e2e/src/a11y/a11y-target.d.ts +23 -0
  37. package/types/e2e/src/a11y/a11y.types.d.ts +81 -0
  38. package/types/e2e/src/a11y/announcements.d.ts +24 -0
  39. package/types/e2e/src/a11y/audit.d.ts +22 -0
  40. package/types/e2e/src/a11y/native/atf.d.ts +51 -0
  41. package/types/e2e/src/a11y/native/xcuitest.d.ts +60 -0
  42. package/types/e2e/src/a11y/order.d.ts +5 -0
  43. package/types/e2e/src/a11y/rules/duplicate-name.d.ts +6 -0
  44. package/types/e2e/src/a11y/rules/hidden-focusable.d.ts +3 -0
  45. package/types/e2e/src/a11y/rules/image-no-name.d.ts +6 -0
  46. package/types/e2e/src/a11y/rules/label-in-name.d.ts +7 -0
  47. package/types/e2e/src/a11y/rules/missing-name.d.ts +3 -0
  48. package/types/e2e/src/a11y/rules/role-in-name.d.ts +3 -0
  49. package/types/e2e/src/a11y/rules/rule.context.d.ts +22 -0
  50. package/types/e2e/src/a11y/rules/target-size.d.ts +21 -0
  51. package/types/e2e/src/a11y/rules/text-clipped.d.ts +9 -0
  52. package/types/e2e/src/a11y/speech.d.ts +26 -0
  53. package/types/e2e/src/a11y/talkback-log.d.ts +13 -0
  54. package/types/e2e/src/app.d.ts +0 -1
  55. package/types/e2e/src/conditions/condition-events.d.ts +6 -1
  56. package/types/e2e/src/conditions/device-conditions.d.ts +30 -2
  57. package/types/e2e/src/device-a11y.d.ts +97 -0
  58. package/types/e2e/src/device.d.ts +11 -2
  59. package/types/e2e/src/driver/a11y-events.d.ts +30 -0
  60. package/types/e2e/src/driver/a11y-read.d.ts +12 -0
  61. package/types/e2e/src/driver/android/android-animations.d.ts +7 -0
  62. package/types/e2e/src/driver/android/android-argv.d.ts +2 -0
  63. package/types/e2e/src/driver/android/android-camera-host.d.ts +23 -0
  64. package/types/e2e/src/driver/android/android-camera.d.ts +28 -0
  65. package/types/e2e/src/driver/android/android-driver.d.ts +5 -0
  66. package/types/e2e/src/driver/android/android-screen-reader.d.ts +23 -0
  67. package/types/e2e/src/driver/android/android-settings.d.ts +9 -2
  68. package/types/e2e/src/driver/android/android-spoken.d.ts +18 -0
  69. package/types/e2e/src/driver/android/android-talkback.d.ts +57 -0
  70. package/types/e2e/src/driver/android/atf-device.d.ts +23 -0
  71. package/types/e2e/src/driver/android/atf-dex.generated.d.ts +7 -0
  72. package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +5 -5
  73. package/types/e2e/src/driver/android/uiad-ops.d.ts +27 -1
  74. package/types/e2e/src/driver/android/uiad-protocol.d.ts +8 -0
  75. package/types/e2e/src/driver/android/uiad-screen-reader.d.ts +33 -0
  76. package/types/e2e/src/driver/driver.types.d.ts +71 -5
  77. package/types/e2e/src/driver/element-actions.d.ts +16 -0
  78. package/types/e2e/src/driver/ios/inject.d.ts +16 -2
  79. package/types/e2e/src/driver/ios/ios-a11y.d.ts +73 -0
  80. package/types/e2e/src/driver/ios/ios-audit.d.ts +36 -0
  81. package/types/e2e/src/driver/ios/ios-driver.d.ts +21 -1
  82. package/types/e2e/src/driver/ios/ios-network.d.ts +7 -2
  83. package/types/e2e/src/driver/ios/sim-runner.d.ts +41 -0
  84. package/types/e2e/src/driver/ios/simd-transport.d.ts +11 -2
  85. package/types/e2e/src/driver/native-audit.d.ts +26 -0
  86. package/types/e2e/src/driver/physical-conditions.d.ts +1 -1
  87. package/types/e2e/src/driver/screen-reader.d.ts +26 -0
  88. package/types/e2e/src/env.d.ts +16 -1
  89. package/types/e2e/src/expect/a11y-matchers.d.ts +33 -0
  90. package/types/e2e/src/expect/expect.d.ts +7 -2
  91. package/types/e2e/src/expect/matchers.d.ts +14 -0
  92. package/types/e2e/src/index.d.ts +5 -1
  93. package/types/e2e/src/runner/callsite.d.ts +3 -1
  94. package/types/e2e/src/screen/render.d.ts +8 -0
@@ -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
  });
@@ -4524,6 +4570,7 @@ function stdioTransport(opts) {
4524
4570
  };
4525
4571
  }
4526
4572
  function socketTransport(opts) {
4573
+ const label = opts.tcp ? `${opts.tcp.hostname}:${opts.tcp.port}` : opts.socketPath ?? "";
4527
4574
  return async (handlers) => {
4528
4575
  const splitter = createLineSplitter();
4529
4576
  const encoder = new TextEncoder;
@@ -4543,37 +4590,35 @@ function socketTransport(opts) {
4543
4590
  queued = written >= queued.length ? new Uint8Array(0) : queued.slice(Math.max(written, 0));
4544
4591
  };
4545
4592
  try {
4546
- const connected = await Bun.connect({
4547
- unix: opts.socketPath,
4548
- socket: {
4549
- open(s) {
4550
- socket = s;
4551
- },
4552
- data(_s, chunk) {
4553
- for (const line of splitter.push(chunk))
4554
- handlers.onLine(line);
4555
- },
4556
- drain() {
4557
- flush();
4558
- },
4559
- close() {
4560
- notifyClosed();
4561
- },
4562
- error() {
4563
- notifyClosed();
4564
- },
4565
- end() {
4566
- notifyClosed();
4567
- }
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();
4568
4612
  }
4569
- });
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 });
4570
4615
  socket = connected;
4571
4616
  } catch (error) {
4572
4617
  const reason = error instanceof Error ? error.message : String(error);
4573
4618
  return {
4574
4619
  success: false,
4575
- error: driverError("E_HELPER_CRASHED", `cannot connect to warden-simd socket ${opts.socketPath}: ${reason}`, {
4576
- detail: { socketPath: opts.socketPath }
4620
+ error: driverError("E_HELPER_CRASHED", `cannot connect to warden-simd socket ${label}: ${reason}`, {
4621
+ detail: { socketPath: label }
4577
4622
  })
4578
4623
  };
4579
4624
  }
@@ -5067,8 +5112,41 @@ function traceDriver(driver, options) {
5067
5112
  return wrap(driver, "driver.");
5068
5113
  }
5069
5114
 
5070
- // ../../packages/core/src/conditions/conditions.config.ts
5115
+ // ../../packages/core/src/a11y/a11y.config.ts
5071
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";
5072
5150
 
5073
5151
  // ../../packages/core/src/duration.ts
5074
5152
  var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
@@ -5110,6 +5188,7 @@ var TEXT_SIZE_NAMES = [
5110
5188
  "accessibility-extra-extra-large",
5111
5189
  "accessibility-extra-extra-extra-large"
5112
5190
  ];
5191
+ var CAMERA_FACINGS = ["back", "front"];
5113
5192
  var CONDITION_AREAS = [
5114
5193
  "network",
5115
5194
  "location",
@@ -5117,7 +5196,8 @@ var CONDITION_AREAS = [
5117
5196
  "permission",
5118
5197
  "locale",
5119
5198
  "a11y",
5120
- "battery"
5199
+ "battery",
5200
+ "camera"
5121
5201
  ];
5122
5202
 
5123
5203
  // ../../packages/core/src/conditions/network.presets.ts
@@ -5227,43 +5307,62 @@ function appliedA11y(op, applied) {
5227
5307
 
5228
5308
  // ../../packages/core/src/conditions/conditions.config.ts
5229
5309
  var NETWORK_WORDS = ["online", "offline", ...NETWORK_PRESET_NAMES];
5230
- var nonNegative2 = z7.number().nonnegative().finite();
5231
- var customNetworkSchema = z7.object({
5310
+ var nonNegative2 = z8.number().nonnegative().finite();
5311
+ var customNetworkSchema = z8.object({
5232
5312
  latencyMs: nonNegative2.optional(),
5233
5313
  jitterMs: nonNegative2.optional(),
5234
5314
  downKbps: nonNegative2.optional(),
5235
5315
  upKbps: nonNegative2.optional(),
5236
- lossPct: z7.number().min(0).max(100).optional()
5316
+ lossPct: z8.number().min(0).max(100).optional()
5237
5317
  }).strict();
5238
- var isoDate = z7.string().refine((v) => !Number.isNaN(Date.parse(v)), { message: "invalid date (use ISO 8601)" });
5239
- var offsetDuration = z7.string().refine((v) => parseDuration(v).success, { message: "invalid duration (use e.g. 2d)" });
5240
- var textSize = z7.union([z7.enum(TEXT_SIZE_NAMES), z7.number().positive().finite()]);
5241
- var a11yConfigSchema = z7.object({
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({
5242
5322
  textSize: textSize.optional(),
5243
- boldText: z7.boolean().optional(),
5244
- contrast: z7.enum(["increased", "normal"]).optional(),
5245
- reduceMotion: z7.boolean().optional()
5323
+ boldText: z8.boolean().optional(),
5324
+ contrast: z8.enum(["increased", "normal"]).optional(),
5325
+ reduceMotion: z8.boolean().optional()
5246
5326
  }).strict();
5247
- var conditionsConfigSchema = z7.object({
5248
- network: z7.union([z7.enum(NETWORK_WORDS), customNetworkSchema]).optional(),
5249
- location: z7.union([
5250
- z7.enum(["city-run", "city-bike", "freeway"]),
5251
- z7.object({
5252
- lat: z7.number().min(-90).max(90),
5253
- lon: z7.number().min(-180).max(180),
5254
- altM: z7.number().finite().optional()
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()
5255
5335
  }).strict()
5256
5336
  ]).optional(),
5257
- clock: z7.union([
5337
+ clock: z8.union([
5258
5338
  isoDate,
5259
- z7.object({ at: isoDate, frozen: z7.boolean().optional() }).strict(),
5260
- z7.object({ offset: offsetDuration }).strict()
5339
+ z8.object({ at: isoDate, frozen: z8.boolean().optional() }).strict(),
5340
+ z8.object({ offset: offsetDuration }).strict()
5261
5341
  ]).optional(),
5262
- permissions: z7.partialRecord(z7.enum(PERMISSION_NAMES), z7.enum(["granted", "denied", "unset"])).optional(),
5263
- locale: z7.string().min(1).optional(),
5264
- timezone: z7.string().min(1).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(),
5265
5345
  a11y: a11yConfigSchema.optional()
5266
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
+ }
5267
5366
  var isWord = (v) => NETWORK_WORDS.includes(v);
5268
5367
  function toNetworkCondition(input) {
5269
5368
  if (typeof input === "string") {
@@ -5334,7 +5433,7 @@ function parseConditionsEnv(raw) {
5334
5433
  }
5335
5434
 
5336
5435
  // ../../packages/e2e/src/env.ts
5337
- import { z as z8 } from "zod";
5436
+ import { z as z9 } from "zod";
5338
5437
  var ENV_NAMES = {
5339
5438
  udid: "WARDEN_UDID",
5340
5439
  deviceName: "WARDEN_DEVICE_NAME",
@@ -5343,6 +5442,7 @@ var ENV_NAMES = {
5343
5442
  appPath: "WARDEN_APP_PATH",
5344
5443
  metroUrl: "WARDEN_METRO_URL",
5345
5444
  launchArgs: "WARDEN_LAUNCH_ARGS",
5445
+ devClientScheme: "WARDEN_DEV_CLIENT_SCHEME",
5346
5446
  out: "WARDEN_E2E_OUT",
5347
5447
  timeout: "WARDEN_E2E_TIMEOUT",
5348
5448
  actionTimeout: "WARDEN_E2E_ACTION_TIMEOUT",
@@ -5363,66 +5463,86 @@ var ENV_NAMES = {
5363
5463
  conditions: "WARDEN_CONDITIONS",
5364
5464
  leaseId: "WARDEN_LEASE_ID",
5365
5465
  netproxyCommand: "WARDEN_NETPROXY_COMMAND",
5366
- biometrics: "WARDEN_BIOMETRICS"
5466
+ biometrics: "WARDEN_BIOMETRICS",
5467
+ a11yAudit: "WARDEN_A11Y_AUDIT",
5468
+ a11y: "WARDEN_A11Y",
5469
+ cameraFeeds: "WARDEN_CAMERA_FEEDS"
5367
5470
  };
5368
5471
  var DEFAULT_TIMEOUT_MS4 = 120000;
5369
5472
  var DEFAULT_ACTION_TIMEOUT_MS = 30000;
5370
5473
  var DEFAULT_ASSERTION_TIMEOUT_MS = 5000;
5371
- var toggle = z8.enum(["1", "true", "0", "false"]).transform((v) => v === "1" || v === "true");
5372
- var millis = z8.string().regex(/^\d+$/, "must be a whole number of milliseconds").transform(Number).pipe(z8.number().int().positive());
5373
- var launchArgs = z8.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) => {
5374
5477
  try {
5375
- const parsed = z8.array(z8.string()).safeParse(JSON.parse(raw));
5478
+ const parsed = z9.array(z9.string()).safeParse(JSON.parse(raw));
5376
5479
  if (parsed.success)
5377
5480
  return parsed.data;
5378
5481
  } catch {}
5379
5482
  ctx.addIssue({ code: "custom", message: "must be a JSON array of strings" });
5380
- return z8.NEVER;
5483
+ return z9.NEVER;
5381
5484
  });
5382
- var conditions = z8.string().transform((raw, ctx) => {
5485
+ var conditions = z9.string().transform((raw, ctx) => {
5383
5486
  const parsed = parseConditionsEnv(raw);
5384
5487
  if (parsed.success)
5385
5488
  return parsed.data;
5386
5489
  ctx.addIssue({ code: "custom", message: parsed.error });
5387
- return z8.NEVER;
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;
5388
5505
  });
5389
5506
  var REQUIRED = "required when WARDEN_UDID is set";
5390
- var schema = z8.object({
5391
- udid: z8.string().min(1),
5392
- deviceName: z8.string().optional(),
5393
- platform: z8.enum(["ios", "android"]).default("ios"),
5394
- bundleId: z8.string({ error: REQUIRED }).min(1, REQUIRED),
5395
- appPath: z8.string().optional(),
5396
- metroUrl: z8.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(),
5397
5515
  launchArgs: launchArgs.default([]),
5398
- out: z8.string().optional(),
5516
+ out: z9.string().optional(),
5399
5517
  timeout: millis.default(DEFAULT_TIMEOUT_MS4),
5400
5518
  actionTimeout: millis.default(DEFAULT_ACTION_TIMEOUT_MS),
5401
5519
  assertionTimeout: millis.default(DEFAULT_ASSERTION_TIMEOUT_MS),
5402
- specsDir: z8.string().optional(),
5403
- helperDir: z8.string().optional(),
5404
- simd: z8.string().optional(),
5405
- simdSocket: z8.string().optional(),
5406
- iosRunnerSocket: z8.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(),
5407
5525
  logTimestamps: toggle.default(false),
5408
- deviceKind: z8.enum(["physical"]).optional(),
5526
+ deviceKind: z9.enum(["physical"]).optional(),
5409
5527
  iosRunnerRestart: launchArgs.optional(),
5410
5528
  afterReboot: launchArgs.optional(),
5411
- version: z8.string().optional(),
5412
- bin: z8.string().optional(),
5413
- perf: z8.string().optional(),
5414
- perfBudgets: z8.string().optional(),
5529
+ version: z9.string().optional(),
5530
+ bin: z9.string().optional(),
5531
+ perf: z9.string().optional(),
5532
+ perfBudgets: z9.string().optional(),
5415
5533
  conditions: conditions.optional(),
5416
- leaseId: z8.string().min(1).optional(),
5417
- netproxyCommand: launchArgs.pipe(z8.array(z8.string()).min(1)).optional(),
5418
- biometrics: toggle.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()
5419
5539
  });
5420
5540
  function defined2(values) {
5421
5541
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
5422
5542
  }
5423
5543
  function parseArgv(raw) {
5424
5544
  try {
5425
- const parsed = z8.array(z8.string()).min(1).safeParse(JSON.parse(raw));
5545
+ const parsed = z9.array(z9.string()).min(1).safeParse(JSON.parse(raw));
5426
5546
  if (parsed.success)
5427
5547
  return parsed.data;
5428
5548
  } catch {}
@@ -5443,6 +5563,7 @@ function toEnv(v, now) {
5443
5563
  deviceName: v.deviceName,
5444
5564
  appPath: v.appPath,
5445
5565
  metroUrl: v.metroUrl,
5566
+ devClientScheme: v.devClientScheme,
5446
5567
  specsDir: v.specsDir,
5447
5568
  helperDir: v.helperDir,
5448
5569
  simd: v.simd,
@@ -5457,6 +5578,8 @@ function toEnv(v, now) {
5457
5578
  ...v.leaseId === undefined ? {} : { leaseId: v.leaseId },
5458
5579
  ...v.netproxyCommand === undefined ? {} : { netproxyCommand: v.netproxyCommand },
5459
5580
  ...v.biometrics === true ? { biometrics: true } : {},
5581
+ ...v.a11yAudit === undefined ? {} : { a11yAudit: v.a11yAudit },
5582
+ ...v.cameraFeeds === undefined ? {} : { cameraFeeds: v.cameraFeeds },
5460
5583
  ...v.afterReboot === undefined ? {} : { afterReboot: v.afterReboot },
5461
5584
  ...v.bin === undefined ? {} : { wardenBin: parseArgv(v.bin) },
5462
5585
  ...v.perf === undefined ? {} : {
@@ -5489,25 +5612,25 @@ function readEnv(env, opts = {}) {
5489
5612
  // ../../packages/e2e/src/runner/results.ts
5490
5613
  import { appendFileSync, mkdirSync } from "fs";
5491
5614
  import { dirname as dirname2 } from "path";
5492
- import { z as z9 } from "zod";
5493
- var testStatusSchema = z9.enum(["passed", "failed", "skipped", "timed-out"]);
5494
- var testResultSchema = z9.object({
5495
- file: z9.string(),
5496
- title: z9.array(z9.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()),
5497
5620
  status: testStatusSchema,
5498
- durationMs: z9.number(),
5499
- startedAt: z9.number(),
5500
- only: z9.boolean().optional(),
5501
- error: z9.object({
5502
- message: z9.string(),
5503
- code: z9.string().optional(),
5504
- stack: z9.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()
5505
5628
  }).optional(),
5506
- artifacts: z9.object({ dir: z9.string(), files: z9.array(z9.string()) }).optional(),
5507
- device: z9.object({
5508
- udid: z9.string(),
5509
- platform: z9.enum(["ios", "android"]),
5510
- name: z9.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()
5511
5634
  })
5512
5635
  });
5513
5636
  function openResults(path) {
@@ -5935,19 +6058,624 @@ function getSession(opts = {}) {
5935
6058
  return session;
5936
6059
  }
5937
6060
 
5938
- // ../../packages/e2e/src/driver/ios/device/capabilities.ts
5939
- var IOS_DEVICE_CAPABILITIES = {
5940
- pinch: false,
5941
- maxPointers: 0,
5942
- unicodeText: true,
5943
- nativeSettle: false,
5944
- alerts: true,
5945
- reachability: true,
5946
- cheapHitTest: false
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;
6109
+ }
6110
+ function descendantTexts(el, platform, out) {
6111
+ for (const child of el.children) {
6112
+ if (child.hidden)
6113
+ continue;
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);
6121
+ }
6122
+ }
6123
+ function hasActionableDescendant(el, platform) {
6124
+ return el.children.some((c) => !c.hidden && (isActionable(c, platform) || hasActionableDescendant(c, platform)));
6125
+ }
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:
6175
+ }
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:
6205
+ }
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);
6287
+ }
6288
+
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)
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
+ }
6362
+ }
6363
+ for (const child of orderSiblings(el.children, platform))
6364
+ walk(child, platform, screen, opts, out);
6365
+ }
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);
6371
+ }
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;
5947
6422
  };
5948
6423
 
5949
- // ../../packages/e2e/src/conditions/device-conditions.ts
5950
- import { resolve as resolve3 } from "path";
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
+ }
5951
6679
 
5952
6680
  // ../../packages/e2e/src/driver/unwrap.ts
5953
6681
  var TIMEOUTS = new Set(["E_TIMEOUT", "E_APP_NOT_RESPONDING"]);
@@ -6000,14 +6728,1048 @@ async function unwrap(call, action, signal) {
6000
6728
  return result.data;
6001
6729
  }
6002
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
+
6003
7765
  // ../../packages/e2e/src/conditions/condition-events.ts
6004
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "fs";
6005
- import { dirname as dirname3 } from "path";
7766
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
7767
+ import { dirname as dirname4 } from "path";
6006
7768
  function openConditionSink(path) {
6007
7769
  let ready = false;
6008
7770
  return (event) => {
6009
7771
  if (!ready) {
6010
- mkdirSync3(dirname3(path), { recursive: true });
7772
+ mkdirSync5(dirname4(path), { recursive: true });
6011
7773
  ready = true;
6012
7774
  }
6013
7775
  appendFileSync2(path, `${JSON.stringify(event)}
@@ -6022,7 +7784,8 @@ var KNOWN_AREAS = new Set([
6022
7784
  "permission",
6023
7785
  "locale",
6024
7786
  "a11y",
6025
- "battery"
7787
+ "battery",
7788
+ "camera"
6026
7789
  ]);
6027
7790
 
6028
7791
  // ../../packages/e2e/src/conditions/geo.ts
@@ -6350,12 +8113,28 @@ function locationOf(input) {
6350
8113
  validatePoint(input);
6351
8114
  return { kind: "point", lat: input.lat, lon: input.lon };
6352
8115
  }
6353
- var registry = new WeakMap;
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;
6354
8133
  function registerConditions(device, internals) {
6355
- registry.set(device, internals);
8134
+ registry2.set(device, internals);
6356
8135
  }
6357
8136
  function conditionsOf(device) {
6358
- return registry.get(device);
8137
+ return registry2.get(device);
6359
8138
  }
6360
8139
  function conditionSinkFor(session) {
6361
8140
  return session.results === undefined ? undefined : openConditionSink(session.results.path);
@@ -6379,6 +8158,8 @@ function createDeviceConditions(deps) {
6379
8158
  let localeDirty = false;
6380
8159
  let a11yDirty = false;
6381
8160
  let batteryDirty = false;
8161
+ let cameraDirty = false;
8162
+ let currentCamera = {};
6382
8163
  const permissionStates = new Map;
6383
8164
  let permissionBundle;
6384
8165
  let currentLocale = {};
@@ -6728,6 +8509,12 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
6728
8509
  ...appliedA11y(op, state.applied),
6729
8510
  ...resolved?.success ? { fontScale: resolved.data.scale } : {}
6730
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
+ }
6731
8518
  if (Object.keys(currentA11y).length === 0)
6732
8519
  recorder?.settle("a11y", currentA11y, true);
6733
8520
  else
@@ -6747,6 +8534,68 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
6747
8534
  emit({ area: "battery", value });
6748
8535
  return state;
6749
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
+ }
6750
8599
  async function startTimelineRun(steps) {
6751
8600
  const converted = steps.map((step, index) => {
6752
8601
  const atMs = durationMs(step.at, `timeline step ${index} at`);
@@ -6787,10 +8636,10 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
6787
8636
  throw new E2eError2("ACTION_FAILED", `a network flap or timeline stopped on its own after failing: ${failures[0]}`);
6788
8637
  }
6789
8638
  }
6790
- const declared = baseline.network !== undefined || baseline.location !== undefined || baseline.clock !== undefined || baseline.permissions !== undefined || baseline.locale !== undefined || baseline.timezone !== undefined || baseline.a11y !== undefined;
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;
6791
8640
  const internals = {
6792
8641
  declared,
6793
- dirty: () => networkDirty || locationDirty || clockDirty || permissionsDirty || localeDirty || a11yDirty || batteryDirty || runs.size > 0,
8642
+ dirty: () => networkDirty || locationDirty || clockDirty || permissionsDirty || localeDirty || a11yDirty || batteryDirty || cameraDirty || runs.size > 0,
6794
8643
  wantsInject: () => baseline.network !== undefined || baseline.clock !== undefined || currentNetwork.kind !== "online" || currentClock.kind !== "real",
6795
8644
  async applyBaseline() {
6796
8645
  if (baseline.permissions !== undefined) {
@@ -6810,6 +8659,9 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
6810
8659
  await applyLocation(baseline.location);
6811
8660
  if (baseline.network !== undefined)
6812
8661
  await applyNetwork(baseline.network, { relaunch: true });
8662
+ if (env.cameraFeeds !== undefined)
8663
+ await applyCamera(baselineCamera());
8664
+ cameraDirty = false;
6813
8665
  networkDirty = false;
6814
8666
  locationDirty = false;
6815
8667
  clockDirty = false;
@@ -6863,6 +8715,12 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
6863
8715
  batteryDirty = false;
6864
8716
  });
6865
8717
  }
8718
+ if (cameraDirty) {
8719
+ await attempt(async () => {
8720
+ await applyCamera(baselineCamera());
8721
+ cameraDirty = false;
8722
+ });
8723
+ }
6866
8724
  if (errors.length > 0)
6867
8725
  throw errors[0];
6868
8726
  }
@@ -6876,6 +8734,7 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
6876
8734
  locale: applyLocale,
6877
8735
  a11y: applyA11y,
6878
8736
  battery: applyBattery,
8737
+ camera: cameraApi,
6879
8738
  memoryWarning: async (options) => {
6880
8739
  const op = settingOp("memoryWarning");
6881
8740
  await call("memoryWarning", () => op(options ?? { bundleId: env.bundleId }));
@@ -6890,8 +8749,8 @@ the app runs without the warden network dylib: declare \`conditions.network\` fo
6890
8749
  }
6891
8750
 
6892
8751
  // ../../packages/e2e/src/perf/device-perf.ts
6893
- import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync } from "fs";
6894
- import { join as join2 } from "path";
8752
+ import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync6, writeFileSync } from "fs";
8753
+ import { join as join6 } from "path";
6895
8754
 
6896
8755
  // ../../packages/e2e/src/perf/budgets.ts
6897
8756
  var BUDGET_KEYS = ["startupColdMs", "jankPct", "maxCommitMs", "p95CpuPct", "peakMemoryMb"];
@@ -6980,30 +8839,30 @@ var flagsOf = (o) => [
6980
8839
  ...o.reset ? ["--reset"] : [],
6981
8840
  ...o.maxDurationMs === undefined ? [] : ["--max-duration", `${Math.ceil(o.maxDurationMs / 1000)}s`]
6982
8841
  ];
6983
- var isRecord = (v) => typeof v === "object" && v !== null;
8842
+ var isRecord2 = (v) => typeof v === "object" && v !== null;
6984
8843
  function saveReport(deps, result, report, sessionId) {
6985
8844
  const sessionReport = typeof result.reportPath === "string" ? result.reportPath : undefined;
6986
8845
  const attempt = deps.attemptDir();
6987
8846
  if (attempt === undefined)
6988
8847
  return sessionReport;
6989
- const out = join2(attempt, PERF_DIR);
6990
- mkdirSync4(out, { recursive: true });
8848
+ const out = join6(attempt, PERF_DIR);
8849
+ mkdirSync6(out, { recursive: true });
6991
8850
  const sessionDir = typeof result.dir === "string" ? result.dir : undefined;
6992
8851
  const write = (name, from, fallback) => {
6993
- if (from !== undefined && existsSync3(from))
6994
- copyFileSync(from, join2(out, name));
8852
+ if (from !== undefined && existsSync4(from))
8853
+ copyFileSync2(from, join6(out, name));
6995
8854
  else if (fallback !== undefined)
6996
- writeFileSync(join2(out, name), fallback);
8855
+ writeFileSync(join6(out, name), fallback);
6997
8856
  else
6998
8857
  return;
6999
8858
  deps.addFile?.(`${PERF_DIR}/${name}`);
7000
8859
  };
7001
8860
  write("report.json", sessionReport, `${JSON.stringify(report, null, 2)}
7002
8861
  `);
7003
- write("report.md", sessionDir === undefined ? undefined : join2(sessionDir, "report.md"));
8862
+ write("report.md", sessionDir === undefined ? undefined : join6(sessionDir, "report.md"));
7004
8863
  write("session.json", undefined, `${JSON.stringify({ sessionId, dir: sessionDir }, null, 2)}
7005
8864
  `);
7006
- return join2(out, "report.json");
8865
+ return join6(out, "report.json");
7007
8866
  }
7008
8867
  function createDevicePerf(deps) {
7009
8868
  const run = async (sub, extra) => {
@@ -7015,7 +8874,7 @@ function createDevicePerf(deps) {
7015
8874
  const why = env === undefined ? stderr.trim() || `exit ${code}` : `${env.error.code}: ${env.error.message}`;
7016
8875
  throw new E2eError2("SESSION", `warden perf ${sub} failed: ${why}`);
7017
8876
  }
7018
- return isRecord(env.result) ? env.result : {};
8877
+ return isRecord2(env.result) ? env.result : {};
7019
8878
  };
7020
8879
  let active = false;
7021
8880
  return {
@@ -7037,7 +8896,7 @@ function createDevicePerf(deps) {
7037
8896
  const result = await run("stop", []);
7038
8897
  active = false;
7039
8898
  const report = result.report;
7040
- if (!isRecord(report))
8899
+ if (!isRecord2(report))
7041
8900
  throw new E2eError2("SESSION", "warden perf stop returned no report");
7042
8901
  const sessionId = typeof result.sessionId === "string" ? result.sessionId : "";
7043
8902
  const reportPath = saveReport(deps, result, report, sessionId);
@@ -7074,21 +8933,21 @@ function spawnWarden(bin) {
7074
8933
  }
7075
8934
 
7076
8935
  // ../../packages/e2e/src/runner/artifacts.ts
7077
- import { createHash } from "crypto";
7078
- 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";
7079
8938
  var SLUG_MAX = 60;
7080
8939
  function slug(text, fallback = "test") {
7081
8940
  const slugged = text.normalize("NFKD").replace(/[\u0300-\u036F]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX).replace(/-+$/g, "");
7082
8941
  return slugged === "" ? fallback : slugged;
7083
8942
  }
7084
8943
  function hash6(...parts) {
7085
- 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);
7086
8945
  }
7087
8946
  function attemptDir(out, specId, title, hash) {
7088
- return join3(out, specId, `${slug(title.join(" "))}-${hash.slice(0, 6)}`);
8947
+ return join7(out, specId, `${slug(title.join(" "))}-${hash.slice(0, 6)}`);
7089
8948
  }
7090
8949
  function screenshotPath(dir, n, name) {
7091
- 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`);
7092
8951
  }
7093
8952
  var SPEC_EXT = /\.e2e$/;
7094
8953
  function specIdOf(file, specsDir) {
@@ -7105,9 +8964,9 @@ function specIdOf(file, specsDir) {
7105
8964
  }
7106
8965
 
7107
8966
  // ../../packages/e2e/src/devtools/native-log.ts
7108
- import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync5, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
8967
+ import { copyFileSync as copyFileSync3, mkdirSync as mkdirSync7, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
7109
8968
  import { homedir } from "os";
7110
- import { dirname as dirname4, join as join4 } from "path";
8969
+ import { dirname as dirname6, join as join8 } from "path";
7111
8970
  var NATIVE_LOG = "native.log";
7112
8971
  var DEFAULT_TIMEOUT_MS5 = 1e4;
7113
8972
  var SLACK_S = 5;
@@ -7116,23 +8975,23 @@ function realNativeFs() {
7116
8975
  return {
7117
8976
  listReports: (dir) => {
7118
8977
  try {
7119
- 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 }));
7120
8979
  } catch {
7121
8980
  return [];
7122
8981
  }
7123
8982
  },
7124
8983
  copy: (from, to) => {
7125
- mkdirSync5(dirname4(to), { recursive: true });
7126
- copyFileSync2(from, to);
8984
+ mkdirSync7(dirname6(to), { recursive: true });
8985
+ copyFileSync3(from, to);
7127
8986
  },
7128
8987
  write: (path, text) => {
7129
- mkdirSync5(dirname4(path), { recursive: true });
8988
+ mkdirSync7(dirname6(path), { recursive: true });
7130
8989
  writeFileSync2(path, text);
7131
8990
  }
7132
8991
  };
7133
8992
  }
7134
8993
  function realNativeLogDeps(exec, adb) {
7135
- return { exec, adb, fs: realNativeFs(), reportsDir: join4(homedir(), "Library", "Logs", "DiagnosticReports") };
8994
+ return { exec, adb, fs: realNativeFs(), reportsDir: join8(homedir(), "Library", "Logs", "DiagnosticReports") };
7136
8995
  }
7137
8996
  function tail(text) {
7138
8997
  return text.length > MAX_BYTES ? text.slice(text.length - MAX_BYTES) : text;
@@ -7148,7 +9007,7 @@ async function iosProcessName(target, exec, timeoutMs) {
7148
9007
  const app = container.stdout.trim();
7149
9008
  if (container.exitCode !== 0 || !app)
7150
9009
  return { error: failed(`app container of ${target.bundleId}`, container) };
7151
- 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")], {
7152
9011
  timeoutMs
7153
9012
  });
7154
9013
  const exe = name.stdout.trim();
@@ -7177,7 +9036,7 @@ async function iosNativeLog(target, deps, win, timeoutMs) {
7177
9036
  `process == "${proc.exe}"`
7178
9037
  ], { timeoutMs });
7179
9038
  if (show.exitCode === 0) {
7180
- deps.fs.write(join4(win.dir, NATIVE_LOG), tail(show.stdout));
9039
+ deps.fs.write(join8(win.dir, NATIVE_LOG), tail(show.stdout));
7181
9040
  capture.files.push(NATIVE_LOG);
7182
9041
  } else {
7183
9042
  capture.problems.push(`native log: ${failed("log show", show)}`);
@@ -7185,7 +9044,7 @@ async function iosNativeLog(target, deps, win, timeoutMs) {
7185
9044
  for (const report of deps.fs.listReports(deps.reportsDir)) {
7186
9045
  if (!report.name.startsWith(proc.exe) || !report.name.endsWith(".ips") || report.mtimeMs < win.sinceMs)
7187
9046
  continue;
7188
- 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));
7189
9048
  capture.files.push(report.name);
7190
9049
  }
7191
9050
  return capture;
@@ -7207,7 +9066,7 @@ async function androidNativeLog(target, deps, win, timeoutMs) {
7207
9066
  crash.exitCode === 0 ? `--- crash buffer ---
7208
9067
  ${crash.stdout}` : ""
7209
9068
  ];
7210
- 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(`
7211
9070
  `)));
7212
9071
  capture.files.push(NATIVE_LOG);
7213
9072
  return capture;
@@ -7221,24 +9080,6 @@ async function captureNativeLog(target, deps, win) {
7221
9080
  }
7222
9081
  }
7223
9082
 
7224
- // ../../packages/e2e/src/runner/callsite.ts
7225
- import { join as join5 } from "path";
7226
- 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")));
7227
- var FRAME = /(?:\(|\s)((?:file:\/\/)?\/[^\s()]+?):\d+(?::\d+)?\)?\s*$/;
7228
- function callerFile(stack = new Error().stack) {
7229
- for (const line of (stack ?? "").split(`
7230
- `)) {
7231
- const raw = FRAME.exec(line)?.[1];
7232
- if (raw === undefined)
7233
- continue;
7234
- const path = raw.startsWith("file://") ? raw.slice("file://".length) : raw;
7235
- if (OWN_FILES.has(path) || path.includes("/node_modules/"))
7236
- continue;
7237
- return path;
7238
- }
7239
- return "unknown";
7240
- }
7241
-
7242
9083
  // ../../packages/e2e/src/runner/scope.ts
7243
9084
  var BUN_TIMEOUT_MARGIN_MS2 = 30000;
7244
9085
  var hookOwners = new WeakMap;
@@ -7329,6 +9170,6 @@ function onlyWasSeen() {
7329
9170
 
7330
9171
  // ../../packages/e2e/src/version.ts
7331
9172
  var WORKSPACE_VERSION = "0.0.0-workspace";
7332
- var WARDEN_E2E_VERSION2 = "0.7.0";
9173
+ var WARDEN_E2E_VERSION2 = "0.9.0";
7333
9174
 
7334
- 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, isRecoverableLoss, CONDITION_AREAS, resolveNetwork, resolveTextSize, androidFontScale, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, IOS_DEVICE_CAPABILITIES, toE2eError, settled, unwrap, formatCoord, DEFAULT_SPEED_MPS, DEFAULT_INTERVAL_S, realTickerClock, startRouteTicker, startFlap, registerConditions, conditionsOf, conditionSinkFor, createDeviceConditions, 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 };