@delacour/warden-e2e 0.6.0 → 0.7.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 (86) hide show
  1. package/{chunk-yb1hcjqm.mjs → chunk-empqr9n0.mjs} +1364 -53
  2. package/index.mjs +34 -4
  3. package/package.json +2 -2
  4. package/preload.mjs +4008 -335
  5. package/types/core/src/affected/affected.schema.d.ts +93 -0
  6. package/types/core/src/allocate.d.ts +2 -1
  7. package/types/core/src/android-keyguard.d.ts +34 -0
  8. package/types/core/src/batch/preset.schema.d.ts +1 -0
  9. package/types/core/src/biometrics.d.ts +15 -0
  10. package/types/core/src/builds/builds.defaults.d.ts +15 -0
  11. package/types/core/src/builds/config.d.ts +5 -1
  12. package/types/core/src/builds/config.schema.d.ts +182 -0
  13. package/types/core/src/builds/define-config.d.ts +104 -0
  14. package/types/core/src/builds/install-device.d.ts +37 -0
  15. package/types/core/src/builds/install.d.ts +17 -3
  16. package/types/core/src/builds/kinds.d.ts +21 -3
  17. package/types/core/src/builds/lock.d.ts +38 -0
  18. package/types/core/src/builds/resolve.d.ts +12 -2
  19. package/types/core/src/conditions/conditions.config.d.ts +117 -0
  20. package/types/core/src/conditions/conditions.d.ts +22 -0
  21. package/types/core/src/conditions/conditions.types.d.ts +141 -0
  22. package/types/core/src/conditions/inject-state.d.ts +13 -0
  23. package/types/core/src/conditions/launch-settings.d.ts +14 -0
  24. package/types/core/src/conditions/network.presets.d.ts +42 -0
  25. package/types/core/src/conditions/text-size.d.ts +19 -0
  26. package/types/core/src/golden/android-biometrics.d.ts +35 -0
  27. package/types/core/src/ios-runner/classify.d.ts +7 -0
  28. package/types/core/src/ios-runner/runner-build.types.d.ts +76 -0
  29. package/types/core/src/netproxy/control.d.ts +75 -0
  30. package/types/core/src/netproxy/decide.d.ts +22 -0
  31. package/types/core/src/netproxy/handle-file.d.ts +12 -0
  32. package/types/core/src/netproxy/lease-proxy.d.ts +53 -0
  33. package/types/core/src/netproxy/netproxy.main.d.ts +40 -0
  34. package/types/core/src/netproxy/pipe.d.ts +37 -0
  35. package/types/core/src/netproxy/proxy.d.ts +48 -0
  36. package/types/core/src/netproxy/shaping.d.ts +43 -0
  37. package/types/core/src/providers/android.d.ts +36 -1
  38. package/types/core/src/types.d.ts +47 -0
  39. package/types/e2e/src/alert-buttons.d.ts +9 -0
  40. package/types/e2e/src/app.d.ts +3 -1
  41. package/types/e2e/src/conditions/condition-events.d.ts +63 -0
  42. package/types/e2e/src/conditions/device-conditions.d.ts +170 -0
  43. package/types/e2e/src/conditions/geo.d.ts +15 -0
  44. package/types/e2e/src/conditions/gpx.d.ts +8 -0
  45. package/types/e2e/src/conditions/lease-recorder.d.ts +27 -0
  46. package/types/e2e/src/conditions/lease-store.d.ts +9 -0
  47. package/types/e2e/src/conditions/location-plan.d.ts +17 -0
  48. package/types/e2e/src/conditions/location-scenarios.data.d.ts +17 -0
  49. package/types/e2e/src/conditions/route-ticker.d.ts +53 -0
  50. package/types/e2e/src/conditions/scheduler.d.ts +83 -0
  51. package/types/e2e/src/device.d.ts +57 -3
  52. package/types/e2e/src/driver/android/android-app.d.ts +4 -0
  53. package/types/e2e/src/driver/android/android-argv.d.ts +19 -0
  54. package/types/e2e/src/driver/android/android-clock.d.ts +28 -0
  55. package/types/e2e/src/driver/android/android-device.d.ts +8 -0
  56. package/types/e2e/src/driver/android/android-driver.d.ts +17 -0
  57. package/types/e2e/src/driver/android/android-network.d.ts +66 -0
  58. package/types/e2e/src/driver/android/android-phone.d.ts +47 -0
  59. package/types/e2e/src/driver/android/android-settings.d.ts +34 -0
  60. package/types/e2e/src/driver/android/android-state.d.ts +10 -0
  61. package/types/e2e/src/driver/clock-recorder.d.ts +9 -0
  62. package/types/e2e/src/driver/device-settings.d.ts +10 -0
  63. package/types/e2e/src/driver/driver.types.d.ts +176 -1
  64. package/types/e2e/src/driver/ios/device/capabilities.d.ts +3 -0
  65. package/types/e2e/src/driver/ios/device/device-driver.d.ts +49 -0
  66. package/types/e2e/src/driver/ios/device/devicectl.d.ts +38 -0
  67. package/types/e2e/src/driver/ios/device/runner-protocol.d.ts +21 -0
  68. package/types/e2e/src/driver/ios/hid-recovery.d.ts +15 -0
  69. package/types/e2e/src/driver/ios/inject.d.ts +110 -0
  70. package/types/e2e/src/driver/ios/ios-driver.d.ts +29 -0
  71. package/types/e2e/src/driver/ios/ios-network.d.ts +46 -0
  72. package/types/e2e/src/driver/ios/ios-settings.d.ts +44 -0
  73. package/types/e2e/src/driver/ios/simctl.d.ts +41 -1
  74. package/types/e2e/src/driver/ios/type-budget.d.ts +6 -1
  75. package/types/e2e/src/driver/network-proxy.d.ts +52 -0
  76. package/types/e2e/src/driver/open.d.ts +20 -0
  77. package/types/e2e/src/driver/physical-conditions.d.ts +11 -0
  78. package/types/e2e/src/driver/requirements.d.ts +14 -0
  79. package/types/e2e/src/driver/typing.d.ts +1 -1
  80. package/types/e2e/src/env.d.ts +32 -0
  81. package/types/e2e/src/fixture-session.d.ts +4 -0
  82. package/types/e2e/src/fixtures.d.ts +6 -1
  83. package/types/e2e/src/index.d.ts +2 -0
  84. package/types/e2e/src/runner/scope.d.ts +1 -0
  85. package/types/e2e/src/runner/test.d.ts +6 -0
  86. package/types/e2e/src/traced-device.d.ts +2 -1
@@ -3076,6 +3076,9 @@ function describeDevices(targets) {
3076
3076
  }
3077
3077
  return [...seen.values()].join(", ");
3078
3078
  }
3079
+ function sameDeviceName(served, wanted) {
3080
+ return wanted !== undefined && served.replaceAll("+", " ") === wanted.replaceAll("+", " ");
3081
+ }
3079
3082
  function normalizeWsUrl(wsUrl, metroUrl) {
3080
3083
  const ws = new URL(wsUrl);
3081
3084
  const metro = new URL(metroUrl);
@@ -3094,7 +3097,11 @@ function selectTarget(targets, query, metroUrl) {
3094
3097
  (t) => t.reactNative?.logicalDeviceId === query.logicalDeviceId || urlDevice(t) === query.logicalDeviceId,
3095
3098
  "logicalDeviceId"
3096
3099
  ],
3097
- [query.deviceName, (t) => t.deviceName === query.deviceName, "deviceName"]
3100
+ [
3101
+ query.deviceName,
3102
+ (t) => t.deviceName !== undefined && sameDeviceName(t.deviceName, query.deviceName),
3103
+ "deviceName"
3104
+ ]
3098
3105
  ];
3099
3106
  for (const [value, matches, label] of wanted) {
3100
3107
  if (!value)
@@ -4390,7 +4397,9 @@ var DRIVER_CODES = new Set([
4390
4397
  "E_HID",
4391
4398
  "E_IO",
4392
4399
  "E_BAD_REQUEST",
4393
- "E_INTERNAL"
4400
+ "E_INTERNAL",
4401
+ "E_DEVICE_LOCKED",
4402
+ "E_REFUSED"
4394
4403
  ]);
4395
4404
  function isDriverCode(code) {
4396
4405
  return DRIVER_CODES.has(code);
@@ -4914,6 +4923,22 @@ function isHidLost(error) {
4914
4923
  const hidInvalidated = /\bE_HID\b/.test(error.message) && /dtuhidd connection[\s\S]*invalidated/.test(error.message);
4915
4924
  return hidInvalidated || /\bE_NOT_BOOTED\b/.test(error.message);
4916
4925
  }
4926
+ function isRunnerLost(error) {
4927
+ if (!(error instanceof E2eError2) || error.code !== "ACTION_FAILED" && error.code !== "DRIVER")
4928
+ return false;
4929
+ return /\bE_HELPER_CRASHED\b/.test(error.message);
4930
+ }
4931
+ function isRecoverableLoss(error, deviceKind) {
4932
+ return deviceKind === "physical" ? isRunnerLost(error) : isHidLost(error);
4933
+ }
4934
+ var RUNNER_RESTART_MS = 180000;
4935
+ async function restartRunnerHost(deps, argv, udid) {
4936
+ if (argv === undefined || argv.length === 0)
4937
+ return err("WARDEN_IOS_RUNNER_RESTART is not set");
4938
+ const cmd = argv.map((arg) => arg.replaceAll("{udid}", udid));
4939
+ const res = await deps.exec(cmd, { timeoutMs: RUNNER_RESTART_MS });
4940
+ return res.exitCode === 0 ? ok(undefined) : err(execError(cmd, res));
4941
+ }
4917
4942
  async function rebootSim(deps, udid) {
4918
4943
  const down = ["xcrun", "simctl", "shutdown", udid];
4919
4944
  const shutdown = await deps.exec(down, { timeoutMs: SHUTDOWN_MS });
@@ -5042,8 +5067,274 @@ function traceDriver(driver, options) {
5042
5067
  return wrap(driver, "driver.");
5043
5068
  }
5044
5069
 
5045
- // ../../packages/e2e/src/env.ts
5070
+ // ../../packages/core/src/conditions/conditions.config.ts
5046
5071
  import { z as z7 } from "zod";
5072
+
5073
+ // ../../packages/core/src/duration.ts
5074
+ var UNIT_MS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
5075
+ function parseDuration(input) {
5076
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(input.trim());
5077
+ if (!match?.[1])
5078
+ return { success: false, error: `invalid duration "${input}" (use e.g. 30s, 10m, 2h)` };
5079
+ return { success: true, data: Math.round(Number(match[1]) * (UNIT_MS[match[2] ?? "s"] ?? 1000)) };
5080
+ }
5081
+
5082
+ // ../../packages/core/src/conditions/conditions.types.ts
5083
+ var PERMISSION_NAMES = [
5084
+ "location",
5085
+ "locationAlways",
5086
+ "camera",
5087
+ "photos",
5088
+ "photosAdd",
5089
+ "contacts",
5090
+ "contactsLimited",
5091
+ "notifications",
5092
+ "microphone",
5093
+ "calendar",
5094
+ "reminders",
5095
+ "motion",
5096
+ "mediaLibrary",
5097
+ "siri"
5098
+ ];
5099
+ var TEXT_SIZE_NAMES = [
5100
+ "extra-small",
5101
+ "small",
5102
+ "medium",
5103
+ "large",
5104
+ "extra-large",
5105
+ "extra-extra-large",
5106
+ "extra-extra-extra-large",
5107
+ "accessibility-medium",
5108
+ "accessibility-large",
5109
+ "accessibility-extra-large",
5110
+ "accessibility-extra-extra-large",
5111
+ "accessibility-extra-extra-extra-large"
5112
+ ];
5113
+ var CONDITION_AREAS = [
5114
+ "network",
5115
+ "location",
5116
+ "clock",
5117
+ "permission",
5118
+ "locale",
5119
+ "a11y",
5120
+ "battery"
5121
+ ];
5122
+
5123
+ // ../../packages/core/src/conditions/network.presets.ts
5124
+ var NETWORK_PRESET_NAMES = ["edge", "3g", "4g", "lte", "wifi", "slow", "lossy"];
5125
+ var NETWORK_PRESETS = {
5126
+ edge: {
5127
+ emu: { speed: "200:240", delay: "175:225" },
5128
+ shaping: { latencyMs: 400, jitterMs: 25, downKbps: 240, upKbps: 200, lossPct: 0 }
5129
+ },
5130
+ "3g": {
5131
+ emu: { speed: "330:780", delay: "40:60" },
5132
+ shaping: { latencyMs: 100, jitterMs: 10, downKbps: 780, upKbps: 330, lossPct: 0 }
5133
+ },
5134
+ "4g": {
5135
+ emu: { speed: "9000:9000", delay: "70:100" },
5136
+ shaping: { latencyMs: 170, jitterMs: 15, downKbps: 9000, upKbps: 9000, lossPct: 0 }
5137
+ },
5138
+ lte: {
5139
+ emu: { speed: "10000:50000", delay: "20:30" },
5140
+ shaping: { latencyMs: 50, jitterMs: 5, downKbps: 50000, upKbps: 1e4, lossPct: 0 }
5141
+ },
5142
+ wifi: {
5143
+ emu: { speed: "33000:40000", delay: "0:1" },
5144
+ shaping: { latencyMs: 2, jitterMs: 0, downKbps: 40000, upKbps: 33000, lossPct: 0 }
5145
+ },
5146
+ slow: {
5147
+ emu: { speed: "400:400", delay: "900:1100" },
5148
+ shaping: { latencyMs: 2000, jitterMs: 100, downKbps: 400, upKbps: 400, lossPct: 0 }
5149
+ },
5150
+ lossy: {
5151
+ emu: { speed: "1000:1000", delay: "200:300" },
5152
+ shaping: { latencyMs: 500, jitterMs: 50, downKbps: 1000, upKbps: 1000, lossPct: 10 }
5153
+ }
5154
+ };
5155
+ var UNSHAPED = { latencyMs: 0, jitterMs: 0, downKbps: 0, upKbps: 0, lossPct: 0 };
5156
+ var UNSHAPED_EMU = { speed: "full", delay: "none" };
5157
+ var nonNegative = (value) => value === undefined || !Number.isFinite(value) ? 0 : Math.max(0, value);
5158
+ function customEmu(shaping) {
5159
+ const { downKbps, upKbps, latencyMs, jitterMs } = shaping;
5160
+ const speed = downKbps === 0 && upKbps === 0 ? UNSHAPED_EMU.speed : `${upKbps || downKbps}:${downKbps || upKbps}`;
5161
+ const half = latencyMs / 2;
5162
+ const min = Math.max(0, Math.round(half - jitterMs));
5163
+ const max = Math.round(half + jitterMs);
5164
+ const delay = max === 0 ? UNSHAPED_EMU.delay : `${min}:${max}`;
5165
+ return { speed, delay };
5166
+ }
5167
+ function resolveNetwork(condition) {
5168
+ switch (condition.kind) {
5169
+ case "offline":
5170
+ return { online: false };
5171
+ case "online":
5172
+ return { online: true, shaping: UNSHAPED, emu: UNSHAPED_EMU };
5173
+ case "preset":
5174
+ return { online: true, ...NETWORK_PRESETS[condition.preset] };
5175
+ case "flap":
5176
+ return resolveNetwork(condition.base ?? { kind: "online" });
5177
+ case "custom": {
5178
+ const shaping = {
5179
+ latencyMs: nonNegative(condition.latencyMs),
5180
+ jitterMs: nonNegative(condition.jitterMs),
5181
+ downKbps: nonNegative(condition.downKbps),
5182
+ upKbps: nonNegative(condition.upKbps),
5183
+ lossPct: Math.min(100, nonNegative(condition.lossPct))
5184
+ };
5185
+ return { online: true, shaping, emu: customEmu(shaping) };
5186
+ }
5187
+ }
5188
+ }
5189
+
5190
+ // ../../packages/core/src/conditions/text-size.ts
5191
+ var TEXT_SIZE_SCALE = {
5192
+ "extra-small": 0.82,
5193
+ small: 0.88,
5194
+ medium: 0.94,
5195
+ large: 1,
5196
+ "extra-large": 1.12,
5197
+ "extra-extra-large": 1.24,
5198
+ "extra-extra-extra-large": 1.35,
5199
+ "accessibility-medium": 1.6,
5200
+ "accessibility-large": 1.9,
5201
+ "accessibility-extra-large": 2.35,
5202
+ "accessibility-extra-extra-large": 2.75,
5203
+ "accessibility-extra-extra-extra-large": 3.1
5204
+ };
5205
+ var ANDROID_MAX_FONT_SCALE = 2;
5206
+ var ANDROID_MIN_FONT_SCALE = 0.5;
5207
+ var isName = (v) => TEXT_SIZE_NAMES.includes(v);
5208
+ function resolveTextSize(size) {
5209
+ if (typeof size === "number") {
5210
+ if (!Number.isFinite(size) || size <= 0)
5211
+ return err(`text size must be a positive scale, got ${String(size)}`);
5212
+ let best = TEXT_SIZE_NAMES[0];
5213
+ for (const name of TEXT_SIZE_NAMES) {
5214
+ if (Math.abs(TEXT_SIZE_SCALE[name] - size) < Math.abs(TEXT_SIZE_SCALE[best] - size))
5215
+ best = name;
5216
+ }
5217
+ return ok({ category: best, scale: size });
5218
+ }
5219
+ if (!isName(size))
5220
+ return err(`unknown text size "${String(size)}" (use ${TEXT_SIZE_NAMES.join(", ")} or a scale like 1.3)`);
5221
+ return ok({ category: size, scale: TEXT_SIZE_SCALE[size] });
5222
+ }
5223
+ var androidFontScale = (scale) => Math.min(ANDROID_MAX_FONT_SCALE, Math.max(ANDROID_MIN_FONT_SCALE, Math.round(scale * 100) / 100));
5224
+ function appliedA11y(op, applied) {
5225
+ return Object.fromEntries(Object.entries(op).filter(([key]) => applied.includes(key)));
5226
+ }
5227
+
5228
+ // ../../packages/core/src/conditions/conditions.config.ts
5229
+ var NETWORK_WORDS = ["online", "offline", ...NETWORK_PRESET_NAMES];
5230
+ var nonNegative2 = z7.number().nonnegative().finite();
5231
+ var customNetworkSchema = z7.object({
5232
+ latencyMs: nonNegative2.optional(),
5233
+ jitterMs: nonNegative2.optional(),
5234
+ downKbps: nonNegative2.optional(),
5235
+ upKbps: nonNegative2.optional(),
5236
+ lossPct: z7.number().min(0).max(100).optional()
5237
+ }).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({
5242
+ textSize: textSize.optional(),
5243
+ boldText: z7.boolean().optional(),
5244
+ contrast: z7.enum(["increased", "normal"]).optional(),
5245
+ reduceMotion: z7.boolean().optional()
5246
+ }).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()
5255
+ }).strict()
5256
+ ]).optional(),
5257
+ clock: z7.union([
5258
+ isoDate,
5259
+ z7.object({ at: isoDate, frozen: z7.boolean().optional() }).strict(),
5260
+ z7.object({ offset: offsetDuration }).strict()
5261
+ ]).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(),
5265
+ a11y: a11yConfigSchema.optional()
5266
+ }).strict();
5267
+ var isWord = (v) => NETWORK_WORDS.includes(v);
5268
+ function toNetworkCondition(input) {
5269
+ if (typeof input === "string") {
5270
+ if (!isWord(input))
5271
+ return err(`unknown network "${input}" (use ${NETWORK_WORDS.join(", ")})`);
5272
+ if (input === "online" || input === "offline")
5273
+ return ok({ kind: input });
5274
+ return ok({ kind: "preset", preset: input });
5275
+ }
5276
+ if ("kind" in input)
5277
+ return ok(input);
5278
+ return ok({ kind: "custom", ...input });
5279
+ }
5280
+ function toLocation(input) {
5281
+ if (typeof input === "string")
5282
+ return { kind: "scenario", name: input };
5283
+ return { kind: "point", lat: input.lat, lon: input.lon, ...input.altM === undefined ? {} : { altM: input.altM } };
5284
+ }
5285
+ function toClock(input) {
5286
+ if (typeof input === "string")
5287
+ return { kind: "fixed", at: new Date(input).toISOString() };
5288
+ if ("offset" in input) {
5289
+ const ms = parseDuration(input.offset);
5290
+ return { kind: "offset", ms: ms.success ? ms.data : 0 };
5291
+ }
5292
+ const at = new Date(input.at).toISOString();
5293
+ return input.frozen === true ? { kind: "frozen", at } : { kind: "fixed", at };
5294
+ }
5295
+ function normalizeConditions(config) {
5296
+ const conditions = {};
5297
+ if (config.network !== undefined) {
5298
+ const network = toNetworkCondition(config.network);
5299
+ if (!network.success)
5300
+ return network;
5301
+ conditions.network = network.data;
5302
+ }
5303
+ if (config.location !== undefined)
5304
+ conditions.location = toLocation(config.location);
5305
+ if (config.clock !== undefined)
5306
+ conditions.clock = toClock(config.clock);
5307
+ if (config.permissions !== undefined)
5308
+ conditions.permissions = config.permissions;
5309
+ if (config.locale !== undefined)
5310
+ conditions.locale = config.locale;
5311
+ if (config.timezone !== undefined)
5312
+ conditions.timezone = config.timezone;
5313
+ if (config.a11y !== undefined) {
5314
+ const a11y = { ...config.a11y };
5315
+ if (config.a11y.textSize !== undefined) {
5316
+ const size = resolveTextSize(config.a11y.textSize);
5317
+ if (!size.success)
5318
+ return size;
5319
+ a11y.fontScale = size.data.scale;
5320
+ }
5321
+ conditions.a11y = a11y;
5322
+ }
5323
+ return ok({ conditions });
5324
+ }
5325
+ function parseConditionsEnv(raw) {
5326
+ let json;
5327
+ try {
5328
+ json = JSON.parse(raw);
5329
+ } catch {
5330
+ return err("must be JSON");
5331
+ }
5332
+ const parsed = conditionsConfigSchema.safeParse(json);
5333
+ return parsed.success ? ok(parsed.data) : err(parsed.error.issues.map((i) => i.message).join("; "));
5334
+ }
5335
+
5336
+ // ../../packages/e2e/src/env.ts
5337
+ import { z as z8 } from "zod";
5047
5338
  var ENV_NAMES = {
5048
5339
  udid: "WARDEN_UDID",
5049
5340
  deviceName: "WARDEN_DEVICE_NAME",
@@ -5060,57 +5351,78 @@ var ENV_NAMES = {
5060
5351
  helperDir: "WARDEN_HELPER_DIR",
5061
5352
  simd: "WARDEN_SIMD",
5062
5353
  simdSocket: "WARDEN_SIMD_SOCKET",
5354
+ iosRunnerSocket: "WARDEN_IOS_RUNNER_SOCKET",
5063
5355
  logTimestamps: "WARDEN_LOG_TIMESTAMPS",
5356
+ deviceKind: "WARDEN_DEVICE_KIND",
5357
+ iosRunnerRestart: "WARDEN_IOS_RUNNER_RESTART",
5064
5358
  afterReboot: "WARDEN_AFTER_REBOOT",
5065
5359
  version: "WARDEN_VERSION",
5066
5360
  bin: "WARDEN_BIN",
5067
5361
  perf: "WARDEN_PERF",
5068
- perfBudgets: "WARDEN_PERF_BUDGETS"
5362
+ perfBudgets: "WARDEN_PERF_BUDGETS",
5363
+ conditions: "WARDEN_CONDITIONS",
5364
+ leaseId: "WARDEN_LEASE_ID",
5365
+ netproxyCommand: "WARDEN_NETPROXY_COMMAND",
5366
+ biometrics: "WARDEN_BIOMETRICS"
5069
5367
  };
5070
5368
  var DEFAULT_TIMEOUT_MS4 = 120000;
5071
5369
  var DEFAULT_ACTION_TIMEOUT_MS = 30000;
5072
5370
  var DEFAULT_ASSERTION_TIMEOUT_MS = 5000;
5073
- var toggle = z7.enum(["1", "true", "0", "false"]).transform((v) => v === "1" || v === "true");
5074
- var millis = z7.string().regex(/^\d+$/, "must be a whole number of milliseconds").transform(Number).pipe(z7.number().int().positive());
5075
- var launchArgs = z7.string().transform((raw, ctx) => {
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) => {
5076
5374
  try {
5077
- const parsed = z7.array(z7.string()).safeParse(JSON.parse(raw));
5375
+ const parsed = z8.array(z8.string()).safeParse(JSON.parse(raw));
5078
5376
  if (parsed.success)
5079
5377
  return parsed.data;
5080
5378
  } catch {}
5081
5379
  ctx.addIssue({ code: "custom", message: "must be a JSON array of strings" });
5082
- return z7.NEVER;
5380
+ return z8.NEVER;
5381
+ });
5382
+ var conditions = z8.string().transform((raw, ctx) => {
5383
+ const parsed = parseConditionsEnv(raw);
5384
+ if (parsed.success)
5385
+ return parsed.data;
5386
+ ctx.addIssue({ code: "custom", message: parsed.error });
5387
+ return z8.NEVER;
5083
5388
  });
5084
5389
  var REQUIRED = "required when WARDEN_UDID is set";
5085
- var schema = z7.object({
5086
- udid: z7.string().min(1),
5087
- deviceName: z7.string().optional(),
5088
- platform: z7.enum(["ios", "android"]).default("ios"),
5089
- bundleId: z7.string({ error: REQUIRED }).min(1, REQUIRED),
5090
- appPath: z7.string().optional(),
5091
- metroUrl: z7.string().optional(),
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(),
5092
5397
  launchArgs: launchArgs.default([]),
5093
- out: z7.string().optional(),
5398
+ out: z8.string().optional(),
5094
5399
  timeout: millis.default(DEFAULT_TIMEOUT_MS4),
5095
5400
  actionTimeout: millis.default(DEFAULT_ACTION_TIMEOUT_MS),
5096
5401
  assertionTimeout: millis.default(DEFAULT_ASSERTION_TIMEOUT_MS),
5097
- specsDir: z7.string().optional(),
5098
- helperDir: z7.string().optional(),
5099
- simd: z7.string().optional(),
5100
- simdSocket: z7.string().optional(),
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(),
5101
5407
  logTimestamps: toggle.default(false),
5408
+ deviceKind: z8.enum(["physical"]).optional(),
5409
+ iosRunnerRestart: launchArgs.optional(),
5102
5410
  afterReboot: launchArgs.optional(),
5103
- version: z7.string().optional(),
5104
- bin: z7.string().optional(),
5105
- perf: z7.string().optional(),
5106
- perfBudgets: z7.string().optional()
5411
+ version: z8.string().optional(),
5412
+ bin: z8.string().optional(),
5413
+ perf: z8.string().optional(),
5414
+ perfBudgets: z8.string().optional(),
5415
+ 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()
5107
5419
  });
5108
5420
  function defined2(values) {
5109
5421
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
5110
5422
  }
5111
5423
  function parseArgv(raw) {
5112
5424
  try {
5113
- const parsed = z7.array(z7.string()).min(1).safeParse(JSON.parse(raw));
5425
+ const parsed = z8.array(z8.string()).min(1).safeParse(JSON.parse(raw));
5114
5426
  if (parsed.success)
5115
5427
  return parsed.data;
5116
5428
  } catch {}
@@ -5135,9 +5447,16 @@ function toEnv(v, now) {
5135
5447
  helperDir: v.helperDir,
5136
5448
  simd: v.simd,
5137
5449
  simdSocket: v.simdSocket,
5450
+ iosRunnerSocket: v.iosRunnerSocket,
5138
5451
  wardenVersion: v.version,
5139
5452
  perfBudgets: v.perfBudgets
5140
5453
  }),
5454
+ ...v.deviceKind === undefined ? {} : { deviceKind: v.deviceKind },
5455
+ ...v.iosRunnerRestart === undefined ? {} : { iosRunnerRestart: v.iosRunnerRestart },
5456
+ ...v.conditions === undefined ? {} : { conditions: v.conditions },
5457
+ ...v.leaseId === undefined ? {} : { leaseId: v.leaseId },
5458
+ ...v.netproxyCommand === undefined ? {} : { netproxyCommand: v.netproxyCommand },
5459
+ ...v.biometrics === true ? { biometrics: true } : {},
5141
5460
  ...v.afterReboot === undefined ? {} : { afterReboot: v.afterReboot },
5142
5461
  ...v.bin === undefined ? {} : { wardenBin: parseArgv(v.bin) },
5143
5462
  ...v.perf === undefined ? {} : {
@@ -5170,25 +5489,25 @@ function readEnv(env, opts = {}) {
5170
5489
  // ../../packages/e2e/src/runner/results.ts
5171
5490
  import { appendFileSync, mkdirSync } from "fs";
5172
5491
  import { dirname as dirname2 } from "path";
5173
- import { z as z8 } from "zod";
5174
- var testStatusSchema = z8.enum(["passed", "failed", "skipped", "timed-out"]);
5175
- var testResultSchema = z8.object({
5176
- file: z8.string(),
5177
- title: z8.array(z8.string()),
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()),
5178
5497
  status: testStatusSchema,
5179
- durationMs: z8.number(),
5180
- startedAt: z8.number(),
5181
- only: z8.boolean().optional(),
5182
- error: z8.object({
5183
- message: z8.string(),
5184
- code: z8.string().optional(),
5185
- stack: z8.string().optional()
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()
5186
5505
  }).optional(),
5187
- artifacts: z8.object({ dir: z8.string(), files: z8.array(z8.string()) }).optional(),
5188
- device: z8.object({
5189
- udid: z8.string(),
5190
- platform: z8.enum(["ios", "android"]),
5191
- name: z8.string().optional()
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()
5192
5511
  })
5193
5512
  });
5194
5513
  function openResults(path) {
@@ -5260,6 +5579,8 @@ async function runStep(attempt, kind, label, fn, now = Date.now) {
5260
5579
  }
5261
5580
 
5262
5581
  // ../../packages/e2e/src/session.ts
5582
+ var ENROLL_TRIES = 5;
5583
+ var ENROLL_RETRY_MS = 500;
5263
5584
  function devtoolsTarget(env) {
5264
5585
  return { appId: env.bundleId, ...env.deviceName === undefined ? {} : { deviceName: env.deviceName } };
5265
5586
  }
@@ -5366,10 +5687,27 @@ function createSession(opts = {}) {
5366
5687
  await sleep(WARM_INTERVAL_MS);
5367
5688
  }
5368
5689
  }
5690
+ async function recoverRunner(deps, name) {
5691
+ if (hidRecovered) {
5692
+ note(`iPhone ${name}: the runner connection was lost again after a restart; check the phone and retry`);
5693
+ return false;
5694
+ }
5695
+ hidRecovered = true;
5696
+ await resetDevice();
5697
+ const restarted = await restartRunnerHost(deps, resolved.iosRunnerRestart, resolved.udid);
5698
+ if (!restarted.success) {
5699
+ note(`iPhone ${name}: runner connection lost, restart failed: ${restarted.error}`);
5700
+ return false;
5701
+ }
5702
+ note(`iPhone ${name}: runner connection lost, runner host restarted; running the test again`);
5703
+ return true;
5704
+ }
5369
5705
  async function recoverHid() {
5370
5706
  if (hid === undefined || closing !== undefined)
5371
5707
  return false;
5372
5708
  const name = resolved.deviceName ?? resolved.udid;
5709
+ if (resolved.deviceKind === "physical")
5710
+ return recoverRunner(hid, name);
5373
5711
  if (hidRecovered) {
5374
5712
  note(`sim ${name}: HID connection lost again after a reboot; \`warden release <id> --shutdown\` and retry`);
5375
5713
  return false;
@@ -5391,6 +5729,24 @@ function createSession(opts = {}) {
5391
5729
  note(`sim ${name}: HID connection lost (Device Hub quit?), rebooted; running the test again`);
5392
5730
  return true;
5393
5731
  }
5732
+ async function reenroll(driver) {
5733
+ if (resolved.biometrics !== true || resolved.platform !== "ios" || resolved.deviceKind === "physical")
5734
+ return driver;
5735
+ const sleep = hid?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
5736
+ for (let attempt = 1;attempt <= ENROLL_TRIES; attempt += 1) {
5737
+ const state = await driver.biometrics("enroll");
5738
+ if (!state.success) {
5739
+ note(`biometrics enroll failed: ${state.error.code}: ${state.error.message}`);
5740
+ return driver;
5741
+ }
5742
+ if (state.data.enrolled)
5743
+ return driver;
5744
+ if (attempt < ENROLL_TRIES)
5745
+ await sleep(ENROLL_RETRY_MS);
5746
+ }
5747
+ note(`biometrics enroll did not take after ${ENROLL_TRIES} attempts`);
5748
+ return driver;
5749
+ }
5394
5750
  const session = {
5395
5751
  env: resolved,
5396
5752
  out,
@@ -5423,7 +5779,7 @@ function createSession(opts = {}) {
5423
5779
  throw new E2eError2("DRIVER", `${code}: ${message}`);
5424
5780
  }
5425
5781
  current = cacheObservations(traceDriver(result.data, { trace: () => attempt?.trace }));
5426
- return current;
5782
+ return reenroll(current);
5427
5783
  }, (cause) => {
5428
5784
  opened = undefined;
5429
5785
  throw new E2eError2("DRIVER", cause instanceof Error ? cause.message : String(cause));
@@ -5579,8 +5935,962 @@ function getSession(opts = {}) {
5579
5935
  return session;
5580
5936
  }
5581
5937
 
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
5947
+ };
5948
+
5949
+ // ../../packages/e2e/src/conditions/device-conditions.ts
5950
+ import { resolve as resolve3 } from "path";
5951
+
5952
+ // ../../packages/e2e/src/driver/unwrap.ts
5953
+ var TIMEOUTS = new Set(["E_TIMEOUT", "E_APP_NOT_RESPONDING"]);
5954
+ var INFRASTRUCTURE = new Set([
5955
+ "E_HELPER_MISSING",
5956
+ "E_HELPER_CRASHED",
5957
+ "E_NOT_BOOTED",
5958
+ "E_UNKNOWN_DEVICE",
5959
+ "E_AX_UNAVAILABLE",
5960
+ "E_APP_UNAVAILABLE",
5961
+ "E_INTERNAL",
5962
+ "E_DEVICE_LOCKED"
5963
+ ]);
5964
+ function codeFor(code) {
5965
+ if (TIMEOUTS.has(code))
5966
+ return "TIMEOUT";
5967
+ if (INFRASTRUCTURE.has(code))
5968
+ return "DRIVER";
5969
+ return "ACTION_FAILED";
5970
+ }
5971
+ function toE2eError(error, action) {
5972
+ return new E2eError2(codeFor(error.code), `${action} failed: ${error.code}: ${error.message}`);
5973
+ }
5974
+ function aborted2(action, signal) {
5975
+ const reason = signal.reason;
5976
+ const detail = reason instanceof Error ? `: ${reason.message}` : "";
5977
+ return new E2eError2("TIMEOUT", `${action} aborted (test deadline reached or cancelled)${detail}`);
5978
+ }
5979
+ async function settled(call, action, signal) {
5980
+ if (signal === undefined)
5981
+ return call();
5982
+ if (signal.aborted)
5983
+ throw aborted2(action, signal);
5984
+ let onAbort;
5985
+ const abort = new Promise((_, reject) => {
5986
+ onAbort = () => reject(aborted2(action, signal));
5987
+ signal.addEventListener("abort", onAbort, { once: true });
5988
+ });
5989
+ try {
5990
+ return await Promise.race([call(), abort]);
5991
+ } finally {
5992
+ if (onAbort !== undefined)
5993
+ signal.removeEventListener("abort", onAbort);
5994
+ }
5995
+ }
5996
+ async function unwrap(call, action, signal) {
5997
+ const result = await settled(call, action, signal);
5998
+ if (!result.success)
5999
+ throw toE2eError(result.error, action);
6000
+ return result.data;
6001
+ }
6002
+
6003
+ // ../../packages/e2e/src/conditions/condition-events.ts
6004
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "fs";
6005
+ import { dirname as dirname3 } from "path";
6006
+ function openConditionSink(path) {
6007
+ let ready = false;
6008
+ return (event) => {
6009
+ if (!ready) {
6010
+ mkdirSync3(dirname3(path), { recursive: true });
6011
+ ready = true;
6012
+ }
6013
+ appendFileSync2(path, `${JSON.stringify(event)}
6014
+ `);
6015
+ };
6016
+ }
6017
+ var KNOWN_AREAS = new Set([
6018
+ "network",
6019
+ "location",
6020
+ "clock",
6021
+ "faults",
6022
+ "permission",
6023
+ "locale",
6024
+ "a11y",
6025
+ "battery"
6026
+ ]);
6027
+
6028
+ // ../../packages/e2e/src/conditions/geo.ts
6029
+ var EARTH_RADIUS_M = 6371008.8;
6030
+ var rad = (deg) => deg * Math.PI / 180;
6031
+ var deg = (r) => r * 180 / Math.PI;
6032
+ function haversineM(a, b) {
6033
+ const dLat = rad(b.lat - a.lat);
6034
+ const dLon = rad(b.lon - a.lon);
6035
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2;
6036
+ return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
6037
+ }
6038
+ function interpolate(a, b, f) {
6039
+ if (f <= 0)
6040
+ return a;
6041
+ if (f >= 1)
6042
+ return b;
6043
+ const delta = haversineM(a, b) / EARTH_RADIUS_M;
6044
+ if (delta < 0.000000000001)
6045
+ return a;
6046
+ const sinDelta = Math.sin(delta);
6047
+ const wa = Math.sin((1 - f) * delta) / sinDelta;
6048
+ const wb = Math.sin(f * delta) / sinDelta;
6049
+ const lat1 = rad(a.lat);
6050
+ const lon1 = rad(a.lon);
6051
+ const lat2 = rad(b.lat);
6052
+ const lon2 = rad(b.lon);
6053
+ const x = wa * Math.cos(lat1) * Math.cos(lon1) + wb * Math.cos(lat2) * Math.cos(lon2);
6054
+ const y = wa * Math.cos(lat1) * Math.sin(lon1) + wb * Math.cos(lat2) * Math.sin(lon2);
6055
+ const z = wa * Math.sin(lat1) + wb * Math.sin(lat2);
6056
+ return { lat: deg(Math.atan2(z, Math.hypot(x, y))), lon: deg(Math.atan2(y, x)) };
6057
+ }
6058
+ function routeSteps(points, speedMps, intervalS) {
6059
+ const first = points[0];
6060
+ if (!first)
6061
+ return [];
6062
+ if (points.length === 1)
6063
+ return [first];
6064
+ const stride = Math.max(speedMps * intervalS, 0.01);
6065
+ const steps = [first];
6066
+ let carried = 0;
6067
+ for (let i = 1;i < points.length; i++) {
6068
+ const from = points[i - 1];
6069
+ const to = points[i];
6070
+ const leg = haversineM(from, to);
6071
+ let at = stride - carried;
6072
+ while (at < leg - 0.000001) {
6073
+ steps.push(interpolate(from, to, at / leg));
6074
+ at += stride;
6075
+ }
6076
+ carried = leg - (at - stride);
6077
+ }
6078
+ const last = points[points.length - 1];
6079
+ if (haversineM(steps[steps.length - 1], last) > 0.001)
6080
+ steps.push(last);
6081
+ else
6082
+ steps[steps.length - 1] = last;
6083
+ return steps;
6084
+ }
6085
+ function formatCoord(n) {
6086
+ const fixed = n.toFixed(7);
6087
+ return fixed.includes(".") ? fixed.replace(/\.?0+$/, "") : fixed;
6088
+ }
6089
+
6090
+ // ../../packages/e2e/src/conditions/route-ticker.ts
6091
+ var DEFAULT_SPEED_MPS = 20;
6092
+ var DEFAULT_INTERVAL_S = 1;
6093
+ var MAX_CONSECUTIVE_FAILURES = 3;
6094
+ var realTickerClock = {
6095
+ now: () => Date.now(),
6096
+ sleep: (ms, signal) => new Promise((resolve) => {
6097
+ if (signal.aborted)
6098
+ return resolve();
6099
+ const done = () => {
6100
+ clearTimeout(timer);
6101
+ signal.removeEventListener("abort", done);
6102
+ resolve();
6103
+ };
6104
+ const timer = setTimeout(done, ms);
6105
+ signal.addEventListener("abort", done);
6106
+ })
6107
+ };
6108
+ function startRouteTicker(route, deps) {
6109
+ const controller = new AbortController;
6110
+ const { signal } = controller;
6111
+ const startedAt = deps.now();
6112
+ const intervalMs = (route.intervalS ?? DEFAULT_INTERVAL_S) * 1000;
6113
+ const steps = routeSteps(route.points, route.speedMps ?? DEFAULT_SPEED_MPS, route.intervalS ?? DEFAULT_INTERVAL_S);
6114
+ function* playlist() {
6115
+ do
6116
+ yield* steps;
6117
+ while (route.loop && steps.length > 0);
6118
+ }
6119
+ const total = steps.length;
6120
+ const pace = async (done) => {
6121
+ if (!route.loop && done >= total)
6122
+ return false;
6123
+ await deps.sleep(Math.max(0, startedAt + done * intervalMs - deps.now()), signal);
6124
+ return true;
6125
+ };
6126
+ const run = async () => {
6127
+ const count = { ticks: 0, failures: 0, index: 0 };
6128
+ let lastError = "";
6129
+ const sendOne = async (step) => {
6130
+ const failure = await deps.fix(step).catch((e) => e instanceof Error ? e.message : String(e));
6131
+ if (!failure) {
6132
+ count.failures = 0;
6133
+ count.ticks++;
6134
+ return true;
6135
+ }
6136
+ lastError = failure;
6137
+ count.failures++;
6138
+ return count.failures < MAX_CONSECUTIVE_FAILURES;
6139
+ };
6140
+ for (const step of playlist()) {
6141
+ if (signal.aborted)
6142
+ return { ticks: count.ticks, stopped: "aborted" };
6143
+ if (!await sendOne(step))
6144
+ return { ticks: count.ticks, stopped: "failed", error: lastError };
6145
+ count.index++;
6146
+ if (!await pace(count.index))
6147
+ break;
6148
+ }
6149
+ return { ticks: count.ticks, stopped: signal.aborted ? "aborted" : "finished" };
6150
+ };
6151
+ return { stop: () => controller.abort(), done: run() };
6152
+ }
6153
+
6154
+ // ../../packages/e2e/src/conditions/scheduler.ts
6155
+ var MAX_CONSECUTIVE_FAILURES2 = 3;
6156
+ var ONLINE = { kind: "online" };
6157
+ var OFFLINE = { kind: "offline" };
6158
+ function seededRng(seed) {
6159
+ let a = seed >>> 0;
6160
+ return () => {
6161
+ a = a + 1831565813 >>> 0;
6162
+ let t = a;
6163
+ t = Math.imul(t ^ t >>> 15, t | 1);
6164
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
6165
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
6166
+ };
6167
+ }
6168
+ var messageOf = (e) => e instanceof Error ? e.message : String(e);
6169
+ async function attempt(run, what) {
6170
+ if (!run)
6171
+ return `target cannot apply ${what}`;
6172
+ try {
6173
+ const res = await run();
6174
+ return res.success ? undefined : res.error.message;
6175
+ } catch (e) {
6176
+ return messageOf(e);
6177
+ }
6178
+ }
6179
+ function core() {
6180
+ const controller = new AbortController;
6181
+ return { controller, signal: controller.signal, status: { state: "running", transitions: 0 }, transitions: 0 };
6182
+ }
6183
+ function invalid(message) {
6184
+ return {
6185
+ stop: () => {
6186
+ return;
6187
+ },
6188
+ done: Promise.resolve({ transitions: 0, stopped: "failed", error: message }),
6189
+ current: () => ({ state: "failed", transitions: 0, error: message })
6190
+ };
6191
+ }
6192
+ function conclude(state, end, error) {
6193
+ const { transitions } = state;
6194
+ if (error !== undefined) {
6195
+ state.status = { state: "failed", transitions, error };
6196
+ return { transitions, stopped: "failed", error };
6197
+ }
6198
+ state.status = { state: end === "finished" ? "finished" : "stopped", transitions };
6199
+ return { transitions, stopped: end };
6200
+ }
6201
+ function applier(target, deps, state) {
6202
+ const net = target.network?.bind(target);
6203
+ const loc = target.location?.bind(target);
6204
+ const tally = { failures: 0, lastError: "" };
6205
+ const record = async (run, what) => {
6206
+ const failure = await attempt(run, what);
6207
+ if (failure) {
6208
+ tally.failures++;
6209
+ tally.lastError = failure;
6210
+ return false;
6211
+ }
6212
+ tally.failures = 0;
6213
+ state.transitions++;
6214
+ return true;
6215
+ };
6216
+ return {
6217
+ tally,
6218
+ async network(condition) {
6219
+ const landed = await record(net && (() => net(condition)), "network");
6220
+ if (landed)
6221
+ deps.sink?.({ type: "condition", area: "network", value: condition, at: deps.now() });
6222
+ return landed;
6223
+ },
6224
+ async location(op) {
6225
+ const landed = await record(loc && (() => loc(op)), "location");
6226
+ if (landed)
6227
+ deps.sink?.({ type: "condition", area: "location", value: op, at: deps.now() });
6228
+ return landed;
6229
+ }
6230
+ };
6231
+ }
6232
+ function startFlap(target, options, deps) {
6233
+ if (!(options.upMs > 0 && options.downMs > 0 && Number.isFinite(options.upMs + options.downMs))) {
6234
+ return invalid("flap needs upMs and downMs above 0");
6235
+ }
6236
+ if (options.base?.kind === "flap")
6237
+ return invalid("flap base cannot itself be a flap");
6238
+ const base = options.base ?? ONLINE;
6239
+ const rng = deps.rng ?? seededRng(options.seed ?? 1);
6240
+ const jitter = (options.jitterPct ?? 0) / 100;
6241
+ const state = core();
6242
+ const { signal } = state;
6243
+ const apply = applier(target, deps, state);
6244
+ const stretch = (ms) => Math.max(1, ms * (1 + (rng() * 2 - 1) * jitter));
6245
+ const runPhase = async (phase, skipApply, deadline) => {
6246
+ const landed = skipApply || await apply.network(phase === "up" ? base : OFFLINE);
6247
+ state.status = { state: "running", transitions: state.transitions, phase };
6248
+ const next = deadline + stretch(phase === "up" ? options.upMs : options.downMs);
6249
+ await deps.sleep(Math.max(0, next - deps.now()), signal);
6250
+ return [landed && phase === "up", next];
6251
+ };
6252
+ const run = async () => {
6253
+ let phase = "up";
6254
+ let atBase = false;
6255
+ let deadline = deps.now();
6256
+ let skipApply = options.applyInitial === false;
6257
+ while (!signal.aborted && apply.tally.failures < MAX_CONSECUTIVE_FAILURES2) {
6258
+ [atBase, deadline] = await runPhase(phase, skipApply, deadline);
6259
+ skipApply = false;
6260
+ phase = phase === "up" ? "down" : "up";
6261
+ }
6262
+ const { failures, lastError } = apply.tally;
6263
+ if (!atBase)
6264
+ await apply.network(base);
6265
+ return conclude(state, "aborted", failures >= MAX_CONSECUTIVE_FAILURES2 ? lastError : undefined);
6266
+ };
6267
+ return { stop: () => state.controller.abort(), done: run(), current: () => state.status };
6268
+ }
6269
+ function startTimeline(target, steps, deps) {
6270
+ if (steps.some((s) => !(s.atMs >= 0) || !Number.isFinite(s.atMs)))
6271
+ return invalid("timeline atMs must be 0 or more");
6272
+ const ordered = [...steps].sort((a, b) => a.atMs - b.atMs);
6273
+ const startedAt = deps.now();
6274
+ const state = core();
6275
+ const { signal } = state;
6276
+ const apply = applier(target, deps, state);
6277
+ const runStep = async (step) => {
6278
+ const errors = [];
6279
+ if (step.network && !await apply.network(step.network))
6280
+ errors.push(apply.tally.lastError);
6281
+ if (step.location && !await apply.location(step.location))
6282
+ errors.push(apply.tally.lastError);
6283
+ return errors.length > 0 ? errors.join("; ") : undefined;
6284
+ };
6285
+ const run = async () => {
6286
+ let stepFailures = 0;
6287
+ let lastError = "";
6288
+ for (const [index, step] of ordered.entries()) {
6289
+ await deps.sleep(Math.max(0, startedAt + step.atMs - deps.now()), signal);
6290
+ if (signal.aborted)
6291
+ break;
6292
+ state.status = { state: "running", transitions: state.transitions, step: index };
6293
+ const problem = await runStep(step);
6294
+ stepFailures = problem === undefined ? 0 : stepFailures + 1;
6295
+ lastError = problem ?? lastError;
6296
+ if (stepFailures >= MAX_CONSECUTIVE_FAILURES2)
6297
+ return conclude(state, "aborted", lastError);
6298
+ }
6299
+ return conclude(state, signal.aborted ? "aborted" : "finished");
6300
+ };
6301
+ return { stop: () => state.controller.abort(), done: run(), current: () => state.status };
6302
+ }
6303
+
6304
+ // ../../packages/e2e/src/conditions/device-conditions.ts
6305
+ var ONLINE2 = { kind: "online" };
6306
+ var REAL_CLOCK = { kind: "real" };
6307
+ function a11yOpOf(config) {
6308
+ const { fontScale: _scale, ...op } = config;
6309
+ return op;
6310
+ }
6311
+ var stderrLog = (line) => {
6312
+ process.stderr.write(`${line}
6313
+ `);
6314
+ };
6315
+ function invalid2(message) {
6316
+ return new E2eError2("INVALID_ARGUMENT", message);
6317
+ }
6318
+ function durationMs(value, what) {
6319
+ if (typeof value === "number") {
6320
+ if (!Number.isFinite(value) || value < 0)
6321
+ throw invalid2(`${what} must be 0 or more, got ${String(value)}`);
6322
+ return value;
6323
+ }
6324
+ const parsed = parseDuration(value);
6325
+ if (!parsed.success)
6326
+ throw invalid2(`${what}: ${parsed.error}`);
6327
+ return parsed.data;
6328
+ }
6329
+ function network(input) {
6330
+ const condition = toNetworkCondition(input);
6331
+ if (!condition.success)
6332
+ throw invalid2(condition.error);
6333
+ return condition.data;
6334
+ }
6335
+ function instant(at) {
6336
+ const date = at instanceof Date ? new Date(at.getTime()) : new Date(at);
6337
+ if (Number.isNaN(date.getTime()))
6338
+ throw invalid2(`invalid time ${JSON.stringify(at)} (use an ISO 8601 string, a Date or epoch ms)`);
6339
+ return date;
6340
+ }
6341
+ function validatePoint(point) {
6342
+ const ok = Number.isFinite(point.lat) && Number.isFinite(point.lon) && Math.abs(point.lat) <= 90 && Math.abs(point.lon) <= 180;
6343
+ if (!ok)
6344
+ throw invalid2(`invalid location ${point.lat},${point.lon} (lat within 90, lon within 180)`);
6345
+ }
6346
+ var SCENARIOS = ["city-run", "city-bike", "freeway"];
6347
+ function locationOf(input) {
6348
+ if ("kind" in input)
6349
+ return input;
6350
+ validatePoint(input);
6351
+ return { kind: "point", lat: input.lat, lon: input.lon };
6352
+ }
6353
+ var registry = new WeakMap;
6354
+ function registerConditions(device, internals) {
6355
+ registry.set(device, internals);
6356
+ }
6357
+ function conditionsOf(device) {
6358
+ return registry.get(device);
6359
+ }
6360
+ function conditionSinkFor(session) {
6361
+ return session.results === undefined ? undefined : openConditionSink(session.results.path);
6362
+ }
6363
+ function createDeviceConditions(deps) {
6364
+ const { driver, env } = deps;
6365
+ const ticker = deps.tickerClock ?? realTickerClock;
6366
+ const log = deps.log ?? stderrLog;
6367
+ const normalized = normalizeConditions(env.conditions ?? {});
6368
+ if (!normalized.success)
6369
+ throw new E2eError2("SESSION", `invalid conditions: ${normalized.error}`);
6370
+ const baseline = normalized.data.conditions;
6371
+ let currentNetwork = ONLINE2;
6372
+ let currentFaults = [];
6373
+ let currentClock = REAL_CLOCK;
6374
+ let clockSetAt = ticker.now();
6375
+ let networkDirty = false;
6376
+ let locationDirty = false;
6377
+ let clockDirty = false;
6378
+ let permissionsDirty = false;
6379
+ let localeDirty = false;
6380
+ let a11yDirty = false;
6381
+ let batteryDirty = false;
6382
+ const permissionStates = new Map;
6383
+ let permissionBundle;
6384
+ let currentLocale = {};
6385
+ let currentA11y = {};
6386
+ let flap;
6387
+ const runs = new Set;
6388
+ const recorder = deps.recorder;
6389
+ const emit = (event) => {
6390
+ deps.sink?.({ ...event, type: "condition", at: ticker.now() });
6391
+ };
6392
+ async function call(what, run) {
6393
+ const result = await settled(run, what, deps.signal());
6394
+ if (result.success)
6395
+ return result.data;
6396
+ const error = toE2eError(result.error, what);
6397
+ if (result.error.code === "E_NETWORK_RELAUNCH") {
6398
+ throw new E2eError2(error.code, `${error.message}
6399
+ the app runs without the warden network dylib: declare \`conditions.network\` for the suite so it launches with it, or pass { relaunch: true }`);
6400
+ }
6401
+ throw error;
6402
+ }
6403
+ const target = (options) => ({
6404
+ network: async (condition) => {
6405
+ const result = await driver.network(condition, options?.relaunch === undefined ? undefined : { relaunch: options.relaunch });
6406
+ if (result.success)
6407
+ recorder?.record("network", condition);
6408
+ return result;
6409
+ },
6410
+ location: async (op) => {
6411
+ const result = await driver.location(op);
6412
+ if (result.success)
6413
+ recorder?.record("location", op);
6414
+ return result;
6415
+ }
6416
+ });
6417
+ function track(scheduler) {
6418
+ const run = {
6419
+ async stop() {
6420
+ scheduler.stop();
6421
+ const outcome = await scheduler.done;
6422
+ runs.delete(run);
6423
+ if (flap?.run === run)
6424
+ flap = undefined;
6425
+ return outcome;
6426
+ },
6427
+ done: scheduler.done,
6428
+ status: () => scheduler.current()
6429
+ };
6430
+ runs.add(run);
6431
+ return run;
6432
+ }
6433
+ async function stopFlap() {
6434
+ const running = flap;
6435
+ if (running === undefined)
6436
+ return;
6437
+ await running.run.stop();
6438
+ }
6439
+ async function applyNetwork(condition, options) {
6440
+ await stopFlap();
6441
+ networkDirty = true;
6442
+ const opts = options?.relaunch === undefined ? undefined : { relaunch: options.relaunch };
6443
+ const state = await call("network", () => driver.network(condition, opts));
6444
+ currentNetwork = condition;
6445
+ recorder?.record("network", condition);
6446
+ emit({ area: "network", value: condition });
6447
+ return state;
6448
+ }
6449
+ async function startFlapRun(options, netOptions) {
6450
+ const upMs = durationMs(options.up, "flap up");
6451
+ const downMs = durationMs(options.down, "flap down");
6452
+ if (!(upMs > 0 && downMs > 0))
6453
+ throw invalid2("flap needs up and down above 0");
6454
+ const base = options.base === undefined ? ONLINE2 : network(options.base);
6455
+ if (base.kind === "flap")
6456
+ throw invalid2("flap base cannot itself be a flap");
6457
+ await applyNetwork(base, netOptions);
6458
+ const scheduler = startFlap(target(netOptions), {
6459
+ upMs,
6460
+ downMs,
6461
+ base,
6462
+ applyInitial: false,
6463
+ ...options.jitterPct === undefined ? {} : { jitterPct: options.jitterPct },
6464
+ ...options.seed === undefined ? {} : { seed: options.seed }
6465
+ }, { ...ticker, ...deps.sink === undefined ? {} : { sink: deps.sink } });
6466
+ const run = track(scheduler);
6467
+ flap = { run, options };
6468
+ currentNetwork = {
6469
+ kind: "flap",
6470
+ upMs,
6471
+ downMs,
6472
+ base,
6473
+ ...options.jitterPct === undefined ? {} : { jitterPct: options.jitterPct }
6474
+ };
6475
+ emit({ area: "network", value: currentNetwork });
6476
+ return run;
6477
+ }
6478
+ async function setNetwork(input, options) {
6479
+ const condition = network(input);
6480
+ if (condition.kind === "flap") {
6481
+ await startFlapRun({
6482
+ up: condition.upMs,
6483
+ down: condition.downMs,
6484
+ ...condition.jitterPct === undefined ? {} : { jitterPct: condition.jitterPct },
6485
+ ...condition.base === undefined ? {} : { base: condition.base }
6486
+ }, options);
6487
+ return { online: true, condition, needsProxy: [] };
6488
+ }
6489
+ return applyNetwork(condition, options);
6490
+ }
6491
+ async function restoreNetwork(previous, previousFlap, options) {
6492
+ if (previous.kind === "flap" && previousFlap !== undefined)
6493
+ await startFlapRun(previousFlap, options);
6494
+ else
6495
+ await applyNetwork(previous, options);
6496
+ }
6497
+ async function resetNetwork() {
6498
+ await stopFlap();
6499
+ if (currentFaults.length > 0)
6500
+ await setFaults([]);
6501
+ const goal = baseline.network ?? ONLINE2;
6502
+ await applyNetwork(goal);
6503
+ networkDirty = false;
6504
+ recorder?.settle("network", goal, goal.kind === "online");
6505
+ }
6506
+ async function setFaults(rules) {
6507
+ networkDirty = true;
6508
+ const result = await call("network faults", () => driver.networkFaults(rules));
6509
+ currentFaults = rules;
6510
+ recorder?.record("network", currentNetwork);
6511
+ emit({ area: "faults", value: { kind: "faults", rules } });
6512
+ return result;
6513
+ }
6514
+ const networkApi = {
6515
+ set: setNetwork,
6516
+ async during(condition, fn, options) {
6517
+ const previous = currentNetwork;
6518
+ const previousFlap = flap?.options;
6519
+ await setNetwork(condition, options);
6520
+ let result;
6521
+ try {
6522
+ result = await fn();
6523
+ } catch (error) {
6524
+ await restoreNetwork(previous, previousFlap, options).catch((restoreError) => log(`[warden-e2e] network.during: restoring the previous network failed: ${String(restoreError instanceof Error ? restoreError.message : restoreError)}`));
6525
+ throw error;
6526
+ }
6527
+ await restoreNetwork(previous, previousFlap, options);
6528
+ return result;
6529
+ },
6530
+ flap: (options) => startFlapRun(options),
6531
+ fault: (rules) => setFaults("match" in rules ? [rules] : rules),
6532
+ async status() {
6533
+ return { condition: currentNetwork, faults: currentFaults, flapping: flap !== undefined };
6534
+ },
6535
+ reset: resetNetwork
6536
+ };
6537
+ async function applyLocation(op) {
6538
+ locationDirty = true;
6539
+ await call("location", () => driver.location(op));
6540
+ recorder?.record("location", op);
6541
+ emit({ area: "location", value: op });
6542
+ }
6543
+ const locationApi = {
6544
+ async set(point) {
6545
+ validatePoint(point);
6546
+ await applyLocation({
6547
+ kind: "point",
6548
+ lat: point.lat,
6549
+ lon: point.lon,
6550
+ ...point.altM === undefined ? {} : { altM: point.altM }
6551
+ });
6552
+ },
6553
+ async route(points, options = {}) {
6554
+ const pace = {
6555
+ ...options.speedMps === undefined ? {} : { speedMps: options.speedMps }
6556
+ };
6557
+ if ("gpx" in points) {
6558
+ await applyLocation({ kind: "gpx", path: resolve3(points.gpx), ...pace });
6559
+ return;
6560
+ }
6561
+ if (points.length === 0)
6562
+ throw invalid2("a route needs at least one point");
6563
+ for (const point of points)
6564
+ validatePoint(point);
6565
+ await applyLocation({
6566
+ kind: "route",
6567
+ points,
6568
+ ...pace,
6569
+ ...options.intervalS === undefined ? {} : { intervalS: options.intervalS },
6570
+ ...options.loop === undefined ? {} : { loop: options.loop }
6571
+ });
6572
+ },
6573
+ async scenario(name) {
6574
+ if (!SCENARIOS.includes(name))
6575
+ throw invalid2(`unknown scenario "${String(name)}" (use ${SCENARIOS.join(", ")})`);
6576
+ await applyLocation({ kind: "scenario", name });
6577
+ },
6578
+ clear: () => applyLocation({ kind: "clear" })
6579
+ };
6580
+ async function applyClock(condition) {
6581
+ const op = driver.clock;
6582
+ clockDirty = true;
6583
+ if (typeof op !== "function") {
6584
+ throw toE2eError({ code: "E_UNSUPPORTED", message: "this driver has no clock condition", retryable: false }, "clock");
6585
+ }
6586
+ await call("clock", () => op.call(driver, condition));
6587
+ currentClock = condition;
6588
+ clockSetAt = ticker.now();
6589
+ emit({ area: "clock", value: condition });
6590
+ }
6591
+ function clockFor(at, frozen) {
6592
+ const iso = instant(at).toISOString();
6593
+ return frozen === true ? { kind: "frozen", at: iso } : { kind: "fixed", at: iso };
6594
+ }
6595
+ function advanced(by) {
6596
+ switch (currentClock.kind) {
6597
+ case "real":
6598
+ return { kind: "offset", ms: by };
6599
+ case "offset":
6600
+ return { kind: "offset", ms: currentClock.ms + by };
6601
+ case "frozen":
6602
+ return { kind: "frozen", at: new Date(Date.parse(currentClock.at) + by).toISOString() };
6603
+ case "fixed": {
6604
+ const now = Date.parse(currentClock.at) + (ticker.now() - clockSetAt);
6605
+ return { kind: "fixed", at: new Date(now + by).toISOString() };
6606
+ }
6607
+ }
6608
+ }
6609
+ const clockApi = {
6610
+ async set(at, options) {
6611
+ await applyClock(clockFor(at, options?.frozen));
6612
+ },
6613
+ async advance(by) {
6614
+ await applyClock(advanced(durationMs(by, "advance")));
6615
+ },
6616
+ async during(at, fn, options) {
6617
+ const previous = currentClock;
6618
+ await applyClock(clockFor(at, options?.frozen));
6619
+ let result;
6620
+ try {
6621
+ result = await fn();
6622
+ } catch (error) {
6623
+ await applyClock(previous).catch((restoreError) => log(`[warden-e2e] clock.during: restoring the previous clock failed: ${String(restoreError instanceof Error ? restoreError.message : restoreError)}`));
6624
+ throw error;
6625
+ }
6626
+ await applyClock(previous);
6627
+ return result;
6628
+ },
6629
+ async reset() {
6630
+ await applyClock(baseline.clock ?? REAL_CLOCK);
6631
+ clockDirty = false;
6632
+ },
6633
+ status: () => currentClock
6634
+ };
6635
+ function settingOp(name) {
6636
+ const op = driver[name];
6637
+ if (typeof op !== "function") {
6638
+ throw toE2eError({ code: "E_UNSUPPORTED", message: `this driver has no ${name} op`, retryable: false }, name);
6639
+ }
6640
+ return op.bind(driver);
6641
+ }
6642
+ const recordPermissions = (bundleId) => {
6643
+ const value = {
6644
+ grants: [...permissionStates].map(([permission, state]) => ({ bundleId, permission, state }))
6645
+ };
6646
+ recorder?.record("permission", value);
6647
+ emit({ area: "permission", value });
6648
+ };
6649
+ async function applyPermissions(grants, options) {
6650
+ permissionsDirty = true;
6651
+ const op = settingOp("permissions");
6652
+ const state = await call("permissions", () => op(grants, options ?? { bundleId: env.bundleId }));
6653
+ for (const { permission, state: applied } of state.applied)
6654
+ permissionStates.set(permission, applied);
6655
+ permissionBundle = state.bundleId;
6656
+ recordPermissions(state.bundleId);
6657
+ return state;
6658
+ }
6659
+ const permissionsApi = {
6660
+ set: applyPermissions,
6661
+ async reset(options) {
6662
+ const baselinePermissions = baseline.permissions ?? {};
6663
+ const back = {};
6664
+ for (const name of permissionStates.keys())
6665
+ back[name] = baselinePermissions[name] ?? "unset";
6666
+ if (Object.keys(back).length === 0)
6667
+ return;
6668
+ await applyPermissions(back, options ?? (permissionBundle ? { bundleId: permissionBundle } : undefined));
6669
+ permissionsDirty = false;
6670
+ permissionStates.clear();
6671
+ recorder?.settle("permission", { grants: [] }, true);
6672
+ }
6673
+ };
6674
+ function recordLocale() {
6675
+ const empty = currentLocale.locale === undefined && currentLocale.timezone === undefined;
6676
+ if (empty)
6677
+ recorder?.settle("locale", currentLocale, true);
6678
+ else
6679
+ recorder?.record("locale", currentLocale);
6680
+ emit({ area: "locale", value: currentLocale });
6681
+ }
6682
+ async function applyTimezone(tz) {
6683
+ localeDirty = true;
6684
+ const op = settingOp("timezone");
6685
+ const previous = currentLocale.previousTimezone;
6686
+ const state = await call("timezone", () => op(tz, previous === undefined ? undefined : { previous }));
6687
+ const { timezone: _gone, ...rest } = currentLocale;
6688
+ currentLocale = {
6689
+ ...rest,
6690
+ ...tz === null ? {} : { timezone: tz },
6691
+ ...previous ?? state.previous ? { previousTimezone: previous ?? state.previous } : {}
6692
+ };
6693
+ recordLocale();
6694
+ return state;
6695
+ }
6696
+ async function applyLocale(tag, options) {
6697
+ localeDirty = true;
6698
+ const op = settingOp("locale");
6699
+ const state = await call("locale", () => op(tag, options ?? { bundleId: env.bundleId }));
6700
+ const { locale: _gone, ...rest } = currentLocale;
6701
+ currentLocale = {
6702
+ ...rest,
6703
+ ...tag === null ? {} : { locale: tag },
6704
+ ...state.bundleId === undefined ? {} : { bundleId: state.bundleId }
6705
+ };
6706
+ recordLocale();
6707
+ return state;
6708
+ }
6709
+ function checkedA11y(op) {
6710
+ if (op === null)
6711
+ return;
6712
+ if (op.textSize !== undefined) {
6713
+ const size = resolveTextSize(op.textSize);
6714
+ if (!size.success)
6715
+ throw invalid2(size.error);
6716
+ }
6717
+ if (Object.keys(op).length === 0)
6718
+ throw invalid2("a11y needs at least one of textSize, boldText, contrast, reduceMotion");
6719
+ }
6720
+ async function applyA11y(op) {
6721
+ checkedA11y(op);
6722
+ a11yDirty = true;
6723
+ const run = settingOp("a11y");
6724
+ const state = await call("a11y", () => run(op));
6725
+ const resolved = op?.textSize === undefined ? undefined : resolveTextSize(op.textSize);
6726
+ currentA11y = op === null ? {} : {
6727
+ ...currentA11y,
6728
+ ...appliedA11y(op, state.applied),
6729
+ ...resolved?.success ? { fontScale: resolved.data.scale } : {}
6730
+ };
6731
+ if (Object.keys(currentA11y).length === 0)
6732
+ recorder?.settle("a11y", currentA11y, true);
6733
+ else
6734
+ recorder?.record("a11y", currentA11y);
6735
+ emit({ area: "a11y", value: currentA11y });
6736
+ return state;
6737
+ }
6738
+ async function applyBattery(op) {
6739
+ batteryDirty = true;
6740
+ const run = settingOp("battery");
6741
+ const state = await call("battery", () => run(op));
6742
+ const value = op === null ? {} : { ...op, cosmetic: state.cosmetic };
6743
+ if (op === null)
6744
+ recorder?.settle("battery", value, true);
6745
+ else
6746
+ recorder?.record("battery", value);
6747
+ emit({ area: "battery", value });
6748
+ return state;
6749
+ }
6750
+ async function startTimelineRun(steps) {
6751
+ const converted = steps.map((step, index) => {
6752
+ const atMs = durationMs(step.at, `timeline step ${index} at`);
6753
+ const converted = { atMs };
6754
+ if (step.network !== undefined) {
6755
+ const condition = network(step.network);
6756
+ if (condition.kind === "flap")
6757
+ throw invalid2(`timeline step ${index}: use device.network.flap for a flap`);
6758
+ converted.network = condition;
6759
+ }
6760
+ if (step.location !== undefined)
6761
+ converted.location = locationOf(step.location);
6762
+ return converted;
6763
+ });
6764
+ networkDirty = true;
6765
+ locationDirty = true;
6766
+ const scheduler = startTimeline(target(), converted, {
6767
+ ...ticker,
6768
+ ...deps.sink === undefined ? {} : { sink: deps.sink }
6769
+ });
6770
+ const status = scheduler.current();
6771
+ if (status.state === "failed")
6772
+ throw invalid2(status.error);
6773
+ const run = track(scheduler);
6774
+ const last = converted.at(-1);
6775
+ if (last?.network !== undefined)
6776
+ currentNetwork = last.network;
6777
+ return run;
6778
+ }
6779
+ async function stopRuns() {
6780
+ const failures = [];
6781
+ for (const run of [...runs]) {
6782
+ const outcome = await run.stop();
6783
+ if (outcome.stopped === "failed")
6784
+ failures.push(outcome.error);
6785
+ }
6786
+ if (failures.length > 0) {
6787
+ throw new E2eError2("ACTION_FAILED", `a network flap or timeline stopped on its own after failing: ${failures[0]}`);
6788
+ }
6789
+ }
6790
+ const declared = baseline.network !== undefined || baseline.location !== undefined || baseline.clock !== undefined || baseline.permissions !== undefined || baseline.locale !== undefined || baseline.timezone !== undefined || baseline.a11y !== undefined;
6791
+ const internals = {
6792
+ declared,
6793
+ dirty: () => networkDirty || locationDirty || clockDirty || permissionsDirty || localeDirty || a11yDirty || batteryDirty || runs.size > 0,
6794
+ wantsInject: () => baseline.network !== undefined || baseline.clock !== undefined || currentNetwork.kind !== "online" || currentClock.kind !== "real",
6795
+ async applyBaseline() {
6796
+ if (baseline.permissions !== undefined) {
6797
+ for (const [name, state] of Object.entries(baseline.permissions))
6798
+ permissionStates.set(name, state);
6799
+ await applyPermissions(baseline.permissions);
6800
+ }
6801
+ if (baseline.timezone !== undefined)
6802
+ await applyTimezone(baseline.timezone);
6803
+ if (baseline.locale !== undefined)
6804
+ await applyLocale(baseline.locale);
6805
+ if (baseline.a11y !== undefined)
6806
+ await applyA11y(a11yOpOf(baseline.a11y));
6807
+ if (baseline.clock !== undefined)
6808
+ await applyClock(baseline.clock);
6809
+ if (baseline.location !== undefined)
6810
+ await applyLocation(baseline.location);
6811
+ if (baseline.network !== undefined)
6812
+ await applyNetwork(baseline.network, { relaunch: true });
6813
+ networkDirty = false;
6814
+ locationDirty = false;
6815
+ clockDirty = false;
6816
+ permissionsDirty = false;
6817
+ localeDirty = false;
6818
+ a11yDirty = false;
6819
+ batteryDirty = false;
6820
+ },
6821
+ async teardown() {
6822
+ const errors = [];
6823
+ const attempt = async (run) => {
6824
+ try {
6825
+ await run();
6826
+ } catch (error) {
6827
+ errors.push(error);
6828
+ }
6829
+ };
6830
+ await attempt(stopRuns);
6831
+ if (networkDirty)
6832
+ await attempt(resetNetwork);
6833
+ if (locationDirty) {
6834
+ await attempt(async () => {
6835
+ const goal = baseline.location ?? { kind: "clear" };
6836
+ await applyLocation(goal);
6837
+ locationDirty = false;
6838
+ recorder?.settle("location", goal, goal.kind === "clear");
6839
+ });
6840
+ }
6841
+ if (clockDirty)
6842
+ await attempt(clockApi.reset);
6843
+ if (permissionsDirty)
6844
+ await attempt(() => permissionsApi.reset());
6845
+ if (localeDirty) {
6846
+ await attempt(async () => {
6847
+ await applyTimezone(baseline.timezone ?? null);
6848
+ await applyLocale(baseline.locale ?? null);
6849
+ localeDirty = false;
6850
+ });
6851
+ }
6852
+ if (a11yDirty) {
6853
+ await attempt(async () => {
6854
+ await applyA11y(null);
6855
+ if (baseline.a11y !== undefined)
6856
+ await applyA11y(a11yOpOf(baseline.a11y));
6857
+ a11yDirty = false;
6858
+ });
6859
+ }
6860
+ if (batteryDirty) {
6861
+ await attempt(async () => {
6862
+ await applyBattery(null);
6863
+ batteryDirty = false;
6864
+ });
6865
+ }
6866
+ if (errors.length > 0)
6867
+ throw errors[0];
6868
+ }
6869
+ };
6870
+ return {
6871
+ network: networkApi,
6872
+ location: locationApi,
6873
+ clock: clockApi,
6874
+ permissions: permissionsApi,
6875
+ timezone: applyTimezone,
6876
+ locale: applyLocale,
6877
+ a11y: applyA11y,
6878
+ battery: applyBattery,
6879
+ memoryWarning: async (options) => {
6880
+ const op = settingOp("memoryWarning");
6881
+ await call("memoryWarning", () => op(options ?? { bundleId: env.bundleId }));
6882
+ },
6883
+ push: async (payload, options) => {
6884
+ const op = settingOp("push");
6885
+ await call("push", () => op(payload, options ?? { bundleId: env.bundleId }));
6886
+ },
6887
+ timeline: startTimelineRun,
6888
+ internals
6889
+ };
6890
+ }
6891
+
5582
6892
  // ../../packages/e2e/src/perf/device-perf.ts
5583
- import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync } from "fs";
6893
+ import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync } from "fs";
5584
6894
  import { join as join2 } from "path";
5585
6895
 
5586
6896
  // ../../packages/e2e/src/perf/budgets.ts
@@ -5677,7 +6987,7 @@ function saveReport(deps, result, report, sessionId) {
5677
6987
  if (attempt === undefined)
5678
6988
  return sessionReport;
5679
6989
  const out = join2(attempt, PERF_DIR);
5680
- mkdirSync3(out, { recursive: true });
6990
+ mkdirSync4(out, { recursive: true });
5681
6991
  const sessionDir = typeof result.dir === "string" ? result.dir : undefined;
5682
6992
  const write = (name, from, fallback) => {
5683
6993
  if (from !== undefined && existsSync3(from))
@@ -5795,9 +7105,9 @@ function specIdOf(file, specsDir) {
5795
7105
  }
5796
7106
 
5797
7107
  // ../../packages/e2e/src/devtools/native-log.ts
5798
- import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
7108
+ import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync5, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
5799
7109
  import { homedir } from "os";
5800
- import { dirname as dirname3, join as join4 } from "path";
7110
+ import { dirname as dirname4, join as join4 } from "path";
5801
7111
  var NATIVE_LOG = "native.log";
5802
7112
  var DEFAULT_TIMEOUT_MS5 = 1e4;
5803
7113
  var SLACK_S = 5;
@@ -5812,11 +7122,11 @@ function realNativeFs() {
5812
7122
  }
5813
7123
  },
5814
7124
  copy: (from, to) => {
5815
- mkdirSync4(dirname3(to), { recursive: true });
7125
+ mkdirSync5(dirname4(to), { recursive: true });
5816
7126
  copyFileSync2(from, to);
5817
7127
  },
5818
7128
  write: (path, text) => {
5819
- mkdirSync4(dirname3(path), { recursive: true });
7129
+ mkdirSync5(dirname4(path), { recursive: true });
5820
7130
  writeFileSync2(path, text);
5821
7131
  }
5822
7132
  };
@@ -6005,7 +7315,8 @@ function registrationDefaults(env = process.env) {
6005
7315
  timeoutMs: parsed?.timeoutMs ?? DEFAULT_TIMEOUT_MS4,
6006
7316
  actionTimeoutMs: parsed?.actionTimeoutMs ?? DEFAULT_ACTION_TIMEOUT_MS,
6007
7317
  assertionTimeoutMs: parsed?.assertionTimeoutMs ?? DEFAULT_ASSERTION_TIMEOUT_MS,
6008
- platform: parsed?.platform
7318
+ platform: parsed?.platform,
7319
+ deviceKind: parsed?.deviceKind
6009
7320
  };
6010
7321
  }
6011
7322
  var ONLY_KEY = Symbol.for("@delacour/warden-e2e/only");
@@ -6018,6 +7329,6 @@ function onlyWasSeen() {
6018
7329
 
6019
7330
  // ../../packages/e2e/src/version.ts
6020
7331
  var WORKSPACE_VERSION = "0.0.0-workspace";
6021
- var WARDEN_E2E_VERSION2 = "0.6.0";
7332
+ var WARDEN_E2E_VERSION2 = "0.7.0";
6022
7333
 
6023
- export { E2eError2, noteSpanDetail, addSpanDetail, canObserve, canDetectAlerts, settleDetailOf, DEFAULT_SETTLE_QUIET_MS, DEFAULT_SETTLE_TIMEOUT_MS, settleByHash, snapshotHash, ok, err, swipeVector, screenRect, fieldValue, PINCH_MS, ROTATE_MS, PATH_MS, centreOf, pinchRadius, rotateRadius, requireGesture, assertScale, assertDegrees, isInteractiveRole, resolveRole, normalizeText, matchesText, indexSnapshot, testIdQuery, textQuery, roleQuery, selectorExpression, renderSnapshot, renderHeader, resolve, locateByTestIdViaReact, isLocator, Locator, pickOne, textOf, scrollUntilVisible, bunExec, execError, driverError, driverErr, logEventSchema, fatalEventSchema, responseSchema, mapWireError, encodeRequest, bunSpawn, stdioTransport, spawnSimd, connectSimd, isHidLost, ENV_NAMES, DEFAULT_ASSERTION_TIMEOUT_MS, readEnv, runStep, setDefaultHidRecovery, setDefaultNativeLog, setDefaultDriverFactory, hasDefaultDriverFactory, setDefaultFixtureFactory, hasDefaultFixtureFactory, attemptSession, getSession, parseBudgets, createDevicePerf, perfStartFor, spawnWarden, hash6, attemptDir, screenshotPath, specIdOf, realNativeLogDeps, captureNativeLog, callerFile, BUN_TIMEOUT_MARGIN_MS2, ownHook, currentScope, createScope, runInScope, hooksFor, resolveUse, registrationDefaults, markOnly, onlyWasSeen, WORKSPACE_VERSION, WARDEN_E2E_VERSION2 };
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 };