@delacour/warden-e2e 0.13.0 → 0.15.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 (42) hide show
  1. package/{chunk-tnr47n9c.mjs → chunk-e3cry5br.mjs} +828 -255
  2. package/index.mjs +144 -14
  3. package/package.json +2 -2
  4. package/preload.mjs +375 -63
  5. package/types/core/src/affected/affected.schema.d.ts +5 -0
  6. package/types/core/src/builds/builds.defaults.d.ts +8 -0
  7. package/types/core/src/builds/config.schema.d.ts +9 -0
  8. package/types/core/src/builds/define-config.d.ts +9 -1
  9. package/types/core/src/conditions/conditions.config.d.ts +6 -1
  10. package/types/core/src/conditions/conditions.types.d.ts +21 -0
  11. package/types/core/src/conditions/inject-state.d.ts +3 -2
  12. package/types/e2e/src/a11y/a11y-target.d.ts +2 -0
  13. package/types/e2e/src/conditions/condition-events.d.ts +6 -1
  14. package/types/e2e/src/conditions/device-conditions.d.ts +8 -2
  15. package/types/e2e/src/device.d.ts +10 -2
  16. package/types/e2e/src/driver/android/android-settings.d.ts +9 -2
  17. package/types/e2e/src/driver/ax-wedge.d.ts +38 -0
  18. package/types/e2e/src/driver/bridge-stall.d.ts +41 -0
  19. package/types/e2e/src/driver/driver.types.d.ts +35 -2
  20. package/types/e2e/src/driver/ios/inject.d.ts +17 -2
  21. package/types/e2e/src/driver/ios/ios-driver.d.ts +17 -2
  22. package/types/e2e/src/driver/ios/ios-keyboard.d.ts +39 -0
  23. package/types/e2e/src/driver/ios/simd-client.d.ts +2 -0
  24. package/types/e2e/src/driver/ios/simd-protocol.d.ts +129 -0
  25. package/types/e2e/src/driver/keyboard-state.d.ts +9 -0
  26. package/types/e2e/src/driver/physical-conditions.d.ts +1 -1
  27. package/types/e2e/src/driver/read-deadline.d.ts +4 -0
  28. package/types/e2e/src/driver/settle.d.ts +11 -1
  29. package/types/e2e/src/errors.d.ts +3 -1
  30. package/types/e2e/src/expect/a11y-matchers.d.ts +7 -0
  31. package/types/e2e/src/expect/matchers.d.ts +5 -0
  32. package/types/e2e/src/index.d.ts +2 -1
  33. package/types/e2e/src/runner/bridge-evidence.d.ts +12 -0
  34. package/types/e2e/src/runner/results.d.ts +29 -0
  35. package/types/e2e/src/screen/actions.d.ts +7 -0
  36. package/types/e2e/src/screen/context.d.ts +11 -12
  37. package/types/e2e/src/screen/degraded-read.d.ts +23 -0
  38. package/types/e2e/src/screen/keyboard-cover.d.ts +26 -0
  39. package/types/e2e/src/screen/locator.d.ts +61 -7
  40. package/types/e2e/src/screen/react-locator.d.ts +17 -2
  41. package/types/e2e/src/screen/react-merge.d.ts +10 -0
  42. package/types/e2e/src/session.d.ts +6 -0
package/preload.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  settleDetailOf,
12
12
  DEFAULT_SETTLE_QUIET_MS,
13
13
  DEFAULT_SETTLE_TIMEOUT_MS,
14
+ hashFallbackQuietMs,
14
15
  settleByHash,
15
16
  snapshotHash,
16
17
  ok,
@@ -29,13 +30,14 @@ import {
29
30
  isInteractiveRole,
30
31
  resolveRole,
31
32
  indexSnapshot,
33
+ keyboardVisible,
32
34
  testIdQuery,
33
35
  textQuery,
34
36
  roleQuery,
35
37
  selectorExpression,
36
38
  renderSnapshot,
37
39
  renderHeader,
38
- locateByTestIdViaReact,
40
+ lookUpTestIdViaReact,
39
41
  isLocator,
40
42
  Locator,
41
43
  scrollUntilVisible,
@@ -44,6 +46,10 @@ import {
44
46
  execError,
45
47
  driverError,
46
48
  driverErr,
49
+ bridgeStallSchema,
50
+ toBridgeStall,
51
+ noteBridgeStall,
52
+ formatBridgeStall,
47
53
  logEventSchema,
48
54
  fatalEventSchema,
49
55
  responseSchema,
@@ -56,6 +62,10 @@ import {
56
62
  createSimdClient,
57
63
  spawnSimd,
58
64
  connectSimd,
65
+ KEYBOARD_MODES,
66
+ DEFAULT_KEYBOARD_MODE,
67
+ IME_STASH_ENABLED,
68
+ IME_STASH_DEFAULT,
59
69
  CAMERA_FACINGS,
60
70
  CONDITION_AREAS,
61
71
  resolveNetwork,
@@ -109,7 +119,7 @@ import {
109
119
  onlyWasSeen,
110
120
  WORKSPACE_VERSION,
111
121
  WARDEN_E2E_VERSION2
112
- } from "./chunk-tnr47n9c.mjs";
122
+ } from "./chunk-e3cry5br.mjs";
113
123
 
114
124
  // ../../packages/e2e/src/preload.ts
115
125
  import { afterAll, setDefaultTimeout } from "bun:test";
@@ -1371,6 +1381,8 @@ function physicalConditionOps(device) {
1371
1381
  locale: () => refuse("locale"),
1372
1382
  a11y: () => refuse("a11y"),
1373
1383
  battery: () => refuse("battery"),
1384
+ keyboard: () => refuse("keyboard"),
1385
+ keyboardState: () => refuse("keyboardState"),
1374
1386
  memoryWarning: () => refuse("memoryWarning"),
1375
1387
  push: () => refuse("push"),
1376
1388
  camera: () => refuse("camera")
@@ -4074,6 +4086,75 @@ function batteryOp(c) {
4074
4086
  };
4075
4087
  };
4076
4088
  }
4089
+ var nullable = (value) => value.trim() === "null" ? "" : value.trim();
4090
+ async function readIme(c) {
4091
+ const enabled = await c.shell(settingGetArgv("secure", "enabled_input_methods"));
4092
+ const current = await c.shell(settingGetArgv("secure", "default_input_method"));
4093
+ return { enabled: nullable(enabled.stdout), default: nullable(current.stdout) };
4094
+ }
4095
+ async function readImeStash(c) {
4096
+ const enabled = await c.shell(settingGetArgv("global", IME_STASH_ENABLED));
4097
+ const current = await c.shell(settingGetArgv("global", IME_STASH_DEFAULT));
4098
+ const state = { enabled: nullable(enabled.stdout), default: nullable(current.stdout) };
4099
+ return state.enabled === "" ? undefined : state;
4100
+ }
4101
+ async function readKeyboardMode(c) {
4102
+ return (await readIme(c)).enabled === "" ? "hardware" : "software";
4103
+ }
4104
+ function imeVisibleIn(dump) {
4105
+ return /\bisVisible=true\b/.test(dump);
4106
+ }
4107
+ async function readKeyboardVisible(c) {
4108
+ const read = await c.shell(["dumpsys", "window", "InputMethod"]);
4109
+ return read.ok && imeVisibleIn(read.stdout);
4110
+ }
4111
+ async function restoreImes(c) {
4112
+ const stash = await readImeStash(c);
4113
+ if (stash !== undefined) {
4114
+ const steps = [
4115
+ ["settings", "put", "secure", "enabled_input_methods", stash.enabled],
4116
+ ...stash.default === "" ? [] : [["settings", "put", "secure", "default_input_method", stash.default]],
4117
+ ["settings", "delete", "global", IME_STASH_ENABLED],
4118
+ ["settings", "delete", "global", IME_STASH_DEFAULT]
4119
+ ];
4120
+ for (const argv of steps) {
4121
+ const done = await c.must(argv);
4122
+ if (!done.success)
4123
+ return done;
4124
+ }
4125
+ }
4126
+ return { success: true, data: { mode: "software" } };
4127
+ }
4128
+ async function hideImes(c, current) {
4129
+ const listed = await c.shell(["ime", "list", "-s"]);
4130
+ const ids = listed.stdout.split(`
4131
+ `).map((line) => line.trim()).filter((line) => line.includes("/"));
4132
+ const steps = [
4133
+ ["settings", "put", "global", IME_STASH_ENABLED, current.enabled],
4134
+ ...current.default === "" ? [] : [["settings", "put", "global", IME_STASH_DEFAULT, current.default]],
4135
+ ...ids.map((id) => ["ime", "disable", id])
4136
+ ];
4137
+ for (const argv of steps) {
4138
+ const done = await c.must(argv);
4139
+ if (!done.success)
4140
+ return done;
4141
+ }
4142
+ if ((await readIme(c)).enabled !== "") {
4143
+ return driverErr("E_IO", "an input method is still enabled after ime disable", { detail: { serial: c.serial } });
4144
+ }
4145
+ return { success: true, data: { mode: "hardware", previousIme: current } };
4146
+ }
4147
+ function keyboardOp(c) {
4148
+ return async (mode) => {
4149
+ if (mode === "software")
4150
+ return restoreImes(c);
4151
+ const current = await readIme(c);
4152
+ if (current.enabled !== "")
4153
+ return hideImes(c, current);
4154
+ const previous = await readImeStash(c);
4155
+ return { success: true, data: { mode, ...previous === undefined ? {} : { previousIme: previous } } };
4156
+ };
4157
+ }
4077
4158
  function memoryWarningOp(c) {
4078
4159
  return async (opts) => {
4079
4160
  const app = c.packageOf(opts?.bundleId, "memoryWarning");
@@ -4148,6 +4229,12 @@ function createAndroidSettings(deps) {
4148
4229
  locale: localeOp(ctx),
4149
4230
  a11y: a11yOp(ctx),
4150
4231
  battery: batteryOp(ctx),
4232
+ keyboard: keyboardOp(ctx),
4233
+ keyboardMode: () => readKeyboardMode(ctx),
4234
+ keyboardVisible: () => readKeyboardVisible(ctx),
4235
+ async keyboardState() {
4236
+ return { success: true, data: { mode: await readKeyboardMode(ctx), visible: await readKeyboardVisible(ctx) } };
4237
+ },
4151
4238
  memoryWarning: memoryWarningOp(ctx),
4152
4239
  async push() {
4153
4240
  return driverErr("E_UNSUPPORTED", "push notifications through the shell are iOS only (`simctl push`); Android needs FCM");
@@ -5835,7 +5922,12 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5835
5922
  });
5836
5923
  return { success: true, data: { ...applied.data, condition: op } };
5837
5924
  };
5838
- const { talkback, ...settings } = createAndroidSettings({
5925
+ const {
5926
+ talkback,
5927
+ keyboardMode: _mode,
5928
+ keyboardVisible: _visible,
5929
+ ...settings
5930
+ } = createAndroidSettings({
5839
5931
  session,
5840
5932
  serial,
5841
5933
  ...options.appPackage ? { appPackage: options.appPackage } : {},
@@ -5953,6 +6045,11 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5953
6045
  ...options.cameraHost ?? createAndroidCameraHost({ serial, exec })
5954
6046
  }),
5955
6047
  ...facts.physical ? physicalConditionOps(`physical Android device ${serial}`) : {},
6048
+ keyboardState: settings.keyboardState,
6049
+ async dismissKeyboard() {
6050
+ const pressed = await pressButton("back");
6051
+ return pressed.success ? { success: true, data: { via: "back" } } : pressed;
6052
+ },
5956
6053
  biometrics,
5957
6054
  app: createAppCommands({
5958
6055
  session,
@@ -6031,6 +6128,17 @@ function writeLaunchSettings(path, settings) {
6031
6128
  }
6032
6129
  }
6033
6130
 
6131
+ // ../../packages/e2e/src/driver/read-deadline.ts
6132
+ import { AsyncLocalStorage } from "async_hooks";
6133
+ var deadlines = new AsyncLocalStorage;
6134
+ function withReadDeadline(remaining, fn) {
6135
+ return deadlines.run({ remaining }, fn);
6136
+ }
6137
+ function readDeadlineRemaining() {
6138
+ const deadline = deadlines.getStore();
6139
+ return deadline === undefined ? undefined : Math.max(0, deadline.remaining());
6140
+ }
6141
+
6034
6142
  // ../../packages/e2e/src/driver/ios/inject.ts
6035
6143
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, renameSync as renameSync6, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "fs";
6036
6144
  import { dirname as dirname3, join as join10, resolve } from "path";
@@ -6051,12 +6159,17 @@ function encodeInjectState(state) {
6051
6159
  clockOffsetMs: Math.trunc(state.clockOffsetMs ?? 0),
6052
6160
  ...state.clockFrozenMs === undefined ? {} : { clockFrozenMs: Math.trunc(state.clockFrozenMs) },
6053
6161
  ...state.a11yRead ? { a11yRead: true } : {},
6054
- ...state.camera === undefined ? {} : { camera: state.camera }
6162
+ ...state.camera === undefined ? {} : { camera: state.camera },
6163
+ ...state.keyboard === undefined ? {} : { keyboard: state.keyboard },
6164
+ ...state.dismissKeyboard ? { dismissKeyboard: true } : {}
6055
6165
  });
6056
6166
  }
6057
6167
  function isRecord3(value) {
6058
6168
  return typeof value === "object" && value !== null && !Array.isArray(value);
6059
6169
  }
6170
+ function parseKeyboard(value) {
6171
+ return KEYBOARD_MODES.find((mode) => mode === value);
6172
+ }
6060
6173
  function parseFeed(value) {
6061
6174
  if (!isRecord3(value) || typeof value.path !== "string")
6062
6175
  return;
@@ -6084,7 +6197,8 @@ function parseInjectState(text) {
6084
6197
  clockOffsetMs: typeof data.clockOffsetMs === "number" ? data.clockOffsetMs : 0,
6085
6198
  ...typeof data.clockFrozenMs === "number" ? { clockFrozenMs: data.clockFrozenMs } : {},
6086
6199
  ...data.a11yRead === true ? { a11yRead: true } : {},
6087
- ..."camera" in data ? { camera: parseCamera(data.camera) } : {}
6200
+ ..."camera" in data ? { camera: parseCamera(data.camera) } : {},
6201
+ ...parseKeyboard(data.keyboard) ? { keyboard: parseKeyboard(data.keyboard) } : {}
6088
6202
  };
6089
6203
  } catch {
6090
6204
  return;
@@ -6118,11 +6232,12 @@ function injectLaunchEnv(input) {
6118
6232
  ...input.clockOffsetMs ? { WARDEN_CLOCK_OFFSET_MS: String(Math.trunc(input.clockOffsetMs)) } : {},
6119
6233
  ...input.clockFrozenMs === undefined ? {} : { WARDEN_CLOCK_FROZEN_MS: String(Math.trunc(input.clockFrozenMs)) },
6120
6234
  ...input.camera ? { WARDEN_CAMERA: "1" } : {},
6235
+ ...input.keyboard ? { WARDEN_KEYBOARD: input.keyboard } : {},
6121
6236
  ...input.debug ? { WARDEN_INJECT_DEBUG: "1" } : {}
6122
6237
  };
6123
6238
  }
6124
6239
  function shouldInject(input) {
6125
- return input.networkActive || input.clockActive || input.cameraActive === true || input.requested;
6240
+ return input.networkActive || input.clockActive || input.cameraActive === true || input.keyboardActive === true || input.requested;
6126
6241
  }
6127
6242
  function locateInjectDylib(input) {
6128
6243
  const tried = [];
@@ -6207,7 +6322,13 @@ function createInjectChannel(input) {
6207
6322
  current: readState,
6208
6323
  write(next) {
6209
6324
  fs.mkdir(dirname3(statePath));
6210
- const state = { ...next, seq: Math.max(readState().seq + 1, Math.trunc(now())) };
6325
+ const previous = readState();
6326
+ const keyboard = next.keyboard ?? previous.keyboard;
6327
+ const state = {
6328
+ ...next,
6329
+ ...keyboard === undefined ? {} : { keyboard },
6330
+ seq: Math.max(previous.seq + 1, Math.trunc(now()))
6331
+ };
6211
6332
  fs.writeAtomic(statePath, encodeInjectState(state));
6212
6333
  return state;
6213
6334
  },
@@ -6224,9 +6345,14 @@ function createInjectChannel(input) {
6224
6345
  }
6225
6346
  },
6226
6347
  reset() {
6348
+ const keyboard = readState().keyboard;
6227
6349
  fs.remove(ackPath);
6228
6350
  fs.remove(statePath);
6229
6351
  fs.remove(injectEventsPath(statePath));
6352
+ if (keyboard !== undefined) {
6353
+ fs.mkdir(dirname3(statePath));
6354
+ fs.writeAtomic(statePath, encodeInjectState({ seq: 0, keyboard }));
6355
+ }
6230
6356
  }
6231
6357
  };
6232
6358
  }
@@ -6787,8 +6913,14 @@ function defaultBuildCommand(platform, configuration, framework = "expo") {
6787
6913
  if (platform === "ios") {
6788
6914
  return `${IOS_NATIVE_PREPARE} && bunx expo run:ios ${configuration === "Release" ? "--configuration Release " : ""}${flags}`;
6789
6915
  }
6790
- return `bunx expo run:android ${configuration === "Release" ? "--variant release " : ""}${flags}`;
6916
+ return androidGradleCommand(configuration, framework);
6917
+ }
6918
+ function androidGradleCommand(configuration, framework = "expo") {
6919
+ const gradle = `cd android && ./gradlew app:assemble${configuration} -PreactNativeArchitectures=${ANDROID_HOST_ABI}`;
6920
+ return framework === "react-native" ? gradle : `${ANDROID_NATIVE_PREPARE} && ${gradle}`;
6791
6921
  }
6922
+ var ANDROID_NATIVE_PREPARE = "{ [ -f android/gradlew ] || bunx expo prebuild --platform android --no-install; }";
6923
+ var ANDROID_HOST_ABI = '"$([ "$(uname -m)" = arm64 ] && echo arm64-v8a || echo x86_64)"';
6792
6924
  var DEFAULT_BUILD_COMMAND = {
6793
6925
  ios: defaultBuildCommand("ios", "Debug"),
6794
6926
  android: defaultBuildCommand("android", "Debug")
@@ -7365,6 +7497,63 @@ function createIosNativeAudit(o) {
7365
7497
  };
7366
7498
  }
7367
7499
 
7500
+ // ../../packages/e2e/src/driver/ios/ios-keyboard.ts
7501
+ var okVoid2 = { success: true, data: undefined };
7502
+ function createIosKeyboard(deps) {
7503
+ const { channel } = deps;
7504
+ let expectLoad = false;
7505
+ const mode = () => channel.current().keyboard ?? DEFAULT_KEYBOARD_MODE;
7506
+ return {
7507
+ mode,
7508
+ launchEnv() {
7509
+ const dylib = deps.dylib();
7510
+ if (!dylib.success)
7511
+ return;
7512
+ channel.reset();
7513
+ channel.write({});
7514
+ expectLoad = true;
7515
+ return injectLaunchEnv({
7516
+ dylib: dylib.data,
7517
+ statePath: channel.statePath,
7518
+ keyboard: mode(),
7519
+ ...deps.debug ? { debug: true } : {}
7520
+ });
7521
+ },
7522
+ launchMode: () => ({ WARDEN_KEYBOARD: mode() }),
7523
+ async confirmLoaded() {
7524
+ if (!expectLoad)
7525
+ return okVoid2;
7526
+ expectLoad = false;
7527
+ if (await channel.acked(channel.current().seq, INJECT_LOAD_MS))
7528
+ return okVoid2;
7529
+ const error = driverError("E_INTERNAL", `the app did not load libwarden-inject.dylib within ${INJECT_LOAD_MS} ms: the keyboard mode would not reach it. Check the app is a simulator build, and run with WARDEN_E2E_DEBUG=1`, { detail: { statePath: channel.statePath } });
7530
+ return { success: false, error };
7531
+ },
7532
+ async dismiss() {
7533
+ if (!channel.attached())
7534
+ return { success: true, data: { attached: false } };
7535
+ const { seq: _seq, a11yRead: _read, ...rest } = channel.current();
7536
+ const state = channel.write({ ...rest, dismissKeyboard: true });
7537
+ if (await channel.acked(state.seq, INJECT_ACK_MS))
7538
+ return { success: true, data: { attached: true } };
7539
+ return driverErr("E_TIMEOUT", `the app did not acknowledge the keyboard dismissal within ${INJECT_ACK_MS} ms`, {
7540
+ detail: { statePath: channel.statePath }
7541
+ });
7542
+ },
7543
+ async keyboard(next) {
7544
+ const { seq: _seq, a11yRead: _read, ...rest } = channel.current();
7545
+ const state = channel.write({ ...rest, keyboard: next });
7546
+ if (!channel.attached())
7547
+ return { success: true, data: { mode: next, appliesOnLaunch: true } };
7548
+ if (await channel.acked(state.seq, INJECT_ACK_MS))
7549
+ return { success: true, data: { mode: next } };
7550
+ return driverErr("E_TIMEOUT", `the app did not acknowledge the keyboard mode within ${INJECT_ACK_MS} ms`, {
7551
+ detail: { statePath: channel.statePath }
7552
+ });
7553
+ }
7554
+ };
7555
+ }
7556
+
7368
7557
  // ../../packages/e2e/src/driver/ios/ios-session.ts
7369
7558
  var SHUTDOWN_TIMEOUT_MS = 500;
7370
7559
  function swappableClient(initial, o) {
@@ -7774,6 +7963,9 @@ function optionalFields(node) {
7774
7963
  fields.pid = node.pid;
7775
7964
  return fields;
7776
7965
  }
7966
+ function roleOf(node) {
7967
+ return node.role === "other" && node.identifier === "inputView" ? "keyboard" : node.role;
7968
+ }
7777
7969
  function normalizeElements(wire, screen, prefix = "", depth = 0) {
7778
7970
  const flat = [];
7779
7971
  const elements = wire.map((node, index) => {
@@ -7781,7 +7973,7 @@ function normalizeElements(wire, screen, prefix = "", depth = 0) {
7781
7973
  const element = {
7782
7974
  ...optionalFields(node),
7783
7975
  id: node.id ?? path,
7784
- role: node.role,
7976
+ role: roleOf(node),
7785
7977
  rawType: node.rawType,
7786
7978
  frame: node.frame,
7787
7979
  depth: node.depth ?? depth,
@@ -7819,6 +8011,8 @@ function toSnapshot(data, takenAt) {
7819
8011
  snapshot.modal = data.modal;
7820
8012
  if (data.fallback?.from === "axbridge")
7821
8013
  snapshot.fellBackFrom = "axbridge";
8014
+ if (data.fallback?.stall)
8015
+ snapshot.bridgeStall = toBridgeStall(data.fallback.stall);
7822
8016
  return snapshot;
7823
8017
  }
7824
8018
 
@@ -8407,6 +8601,14 @@ var REBIND_TIMEOUT_MS = 1e4;
8407
8601
  var COLD_BRIDGE_BUDGET_MS = 60000;
8408
8602
  var COLD_BRIDGE_FIRST_WAIT_MS = 2000;
8409
8603
  var COLD_BRIDGE_WAIT_MS = 5000;
8604
+ var BRIDGE_COLD_DEADLINE_MS = 12000;
8605
+ var AX_FALLBACK_MARGIN_MS = 8000;
8606
+ var COLD_READ_TIMEOUT_MS = BRIDGE_COLD_DEADLINE_MS + AX_FALLBACK_MARGIN_MS;
8607
+ function coldReadTimeoutMs(env = process.env) {
8608
+ const raw = Number(env.WARDEN_SIMD_BRIDGE_COLD_DEADLINE_MS);
8609
+ const deadline = Number.isFinite(raw) && raw > BRIDGE_COLD_DEADLINE_MS ? raw : BRIDGE_COLD_DEADLINE_MS;
8610
+ return deadline + AX_FALLBACK_MARGIN_MS;
8611
+ }
8410
8612
  var GESTURE_MARGIN_MS = 5000;
8411
8613
  var FRONTMOST_TIMEOUT_MS = 3000;
8412
8614
  var LAUNCH_TIMEOUT_MS = 30000;
@@ -8444,7 +8646,7 @@ var SOCKET_RETRY_MS = 500;
8444
8646
  function params(values) {
8445
8647
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
8446
8648
  }
8447
- var okVoid2 = { success: true, data: undefined };
8649
+ var okVoid3 = { success: true, data: undefined };
8448
8650
  function fail2(error) {
8449
8651
  return { success: false, error };
8450
8652
  }
@@ -8480,6 +8682,23 @@ function noteSnapshotError(detail) {
8480
8682
  if (typeof value === "string")
8481
8683
  noteSpanDetail(to, value);
8482
8684
  }
8685
+ const stall = bridgeStallSchema.safeParse(detail.stall);
8686
+ if (stall.success)
8687
+ noteBridgeStall(toBridgeStall(stall.data));
8688
+ }
8689
+ function noteFallback(fallback, log) {
8690
+ const stall = fallback.stall && toBridgeStall(fallback.stall);
8691
+ const why = stall ? ` (${formatBridgeStall(stall)})` : "";
8692
+ log?.(`[ios] snapshot fell back ${fallback.from} -> ${fallback.to}: ${fallback.reason}${why}`);
8693
+ noteSpanDetail("fallback", fallback.reason);
8694
+ if (stall)
8695
+ noteBridgeStall(stall);
8696
+ }
8697
+ function noteSnapshotTarget(target) {
8698
+ if (!target)
8699
+ return;
8700
+ noteSpanDetail("targetPid", target.pid);
8701
+ noteSpanDetail("targetSource", target.source);
8483
8702
  }
8484
8703
  function noteHelperHealth(health) {
8485
8704
  noteSpanDetail("bridge", health.ax.bridge);
@@ -8495,6 +8714,10 @@ function noteHelperHealth(health) {
8495
8714
  noteSpanDetail("coreSimulator", health.coreSimulatorVersion);
8496
8715
  if (health.frontmost)
8497
8716
  noteSpanDetail("frontmostUnresolvedAgoMs", health.frontmost.unresolvedAgoMs);
8717
+ if (health.bridgeRecycles) {
8718
+ noteSpanDetail("bridgeRecycles", health.bridgeRecycles.count);
8719
+ noteSpanDetail("bridgeRecyclesExhausted", health.bridgeRecycles.exhausted);
8720
+ }
8498
8721
  }
8499
8722
  function coldBridgeWait(error, attempt, spentMs, budgetMs) {
8500
8723
  if (!bridgeNotReady(error))
@@ -8538,8 +8761,26 @@ function healthDetail(d) {
8538
8761
  ...d.ax.error !== undefined ? { axError: d.ax.error } : {},
8539
8762
  ...d.socket ? { socket: d.socket } : {},
8540
8763
  ...d.xcode ? { xcode: d.xcode } : {},
8541
- ...d.state ? { state: d.state } : {}
8542
- };
8764
+ ...d.state ? { state: d.state } : {},
8765
+ ...d.axWedge ? { axWedge: toAxWedgeState(d.axWedge) } : {}
8766
+ };
8767
+ }
8768
+ var AX_WEDGE_RECOVER_MS = 75000;
8769
+ function toAxWedgeState(d) {
8770
+ const state = {
8771
+ restarts: d.restarts,
8772
+ maxRestarts: d.maxRestarts,
8773
+ recovering: d.recovering,
8774
+ ...d.checked !== undefined ? { checked: d.checked } : {}
8775
+ };
8776
+ const last = d.last;
8777
+ if (last?.outcome === "restarted")
8778
+ state.last = { outcome: "restarted", agoMs: last.agoMs, ...last.ms !== undefined ? { ms: last.ms } : {} };
8779
+ else if (last?.outcome === "not-wedged")
8780
+ state.last = { outcome: "not-wedged", agoMs: last.agoMs };
8781
+ else if (last?.outcome === "failed")
8782
+ state.last = { outcome: "failed", agoMs: last.agoMs, error: last.error };
8783
+ return state;
8543
8784
  }
8544
8785
 
8545
8786
  class DriverFailure extends Error {
@@ -8562,14 +8803,15 @@ function decodeBase64(png) {
8562
8803
  function predatesOp(error) {
8563
8804
  return error.code === "E_UNSUPPORTED" || error.code === "E_BAD_REQUEST" && /unknown (input )?op/.test(error.message);
8564
8805
  }
8565
- async function launchWithInjection(net, a11y, requested) {
8806
+ async function launchWithInjection(net, a11y, keyboard, requested) {
8566
8807
  const injected = net ? await net.launchEnv(requested ?? {}) : undefined;
8567
8808
  if (injected && !injected.success)
8568
8809
  return injected;
8569
8810
  const forA11y = injected?.data || !a11y ? undefined : await a11y.launchEnv(requested ?? {});
8570
8811
  if (forA11y && !forA11y.success)
8571
8812
  return forA11y;
8572
- const env = injected?.data ?? forA11y?.data;
8813
+ const carried = injected?.data ?? forA11y?.data;
8814
+ const env = keyboard ? carried ? { ...carried, ...keyboard.launchMode() } : keyboard.launchEnv() : carried;
8573
8815
  return { success: true, data: env ? { ...requested, env: { ...requested?.env, ...env } } : requested };
8574
8816
  }
8575
8817
  function createIosDriver(swap, simctl, opts) {
@@ -8584,6 +8826,7 @@ function createIosDriver(swap, simctl, opts) {
8584
8826
  relaunch: (bundleId) => driverRef ? driverRef.app.launch(bundleId, { relaunch: true, inject: true }) : noLeaseProxy()
8585
8827
  });
8586
8828
  const a11y = opts.a11y?.();
8829
+ const kbd = opts.keyboard?.();
8587
8830
  const nativeAudit = opts.nativeAudit?.({ lastBundle: () => lastLaunched });
8588
8831
  const { client } = swap;
8589
8832
  let axFailures = 0;
@@ -8595,6 +8838,9 @@ function createIosDriver(swap, simctl, opts) {
8595
8838
  let settleAxFailures = 0;
8596
8839
  let rebindUnsupported = false;
8597
8840
  let bridgeProven = false;
8841
+ let warmGeneration;
8842
+ const coldTimeoutMs = coldReadTimeoutMs();
8843
+ const readOpts = () => warmGeneration === swap.generation() ? {} : { minTimeoutMs: coldTimeoutMs };
8598
8844
  let gestureUnsupported = false;
8599
8845
  let appOpsOutdated = false;
8600
8846
  const capabilities = {
@@ -8647,7 +8893,7 @@ function createIosDriver(swap, simctl, opts) {
8647
8893
  const send = (op, p = {}, timeoutMs) => request(op, p, timeoutMs === undefined ? {} : { timeoutMs });
8648
8894
  const sendVoid = async (op, p = {}, timeoutMs) => {
8649
8895
  const result = await send(op, p, timeoutMs);
8650
- return result.success ? okVoid2 : result;
8896
+ return result.success ? okVoid3 : result;
8651
8897
  };
8652
8898
  const gestureTimeout = (durationMs, base = 0) => (durationMs ?? 0) + base + GESTURE_MARGIN_MS;
8653
8899
  const helperOutdated = () => fail2(driverError("E_UNSUPPORTED", `this warden-simd predates the gesture op: restart the helper (warden ui close --udid ${udid}) or rebuild it (bun run --cwd packages/e2e native:build)`, { detail: { reason: "helper_outdated" } }));
@@ -8686,11 +8932,12 @@ function createIosDriver(swap, simctl, opts) {
8686
8932
  if (!viaHelper.success)
8687
8933
  return viaHelper;
8688
8934
  note(viaHelper.data);
8689
- return okVoid2;
8935
+ return okVoid3;
8690
8936
  }
8691
8937
  let failureDiagnosed = false;
8692
8938
  const fetchSnapshot = async (o) => {
8693
- const result = await request("snapshot", params({ interactiveOnly: o?.interactiveOnly, backend: o?.backend }));
8939
+ const generation = swap.generation();
8940
+ const result = await request("snapshot", params({ interactiveOnly: o?.interactiveOnly, backend: o?.backend }), readOpts());
8694
8941
  if (!result.success) {
8695
8942
  noteSnapshotError(result.error.detail);
8696
8943
  if (!failureDiagnosed) {
@@ -8703,15 +8950,12 @@ function createIosDriver(swap, simctl, opts) {
8703
8950
  }
8704
8951
  failureDiagnosed = false;
8705
8952
  const { fallback, target } = result.data;
8953
+ if (result.data.backend === "axbridge" && !fallback)
8954
+ warmGeneration = generation;
8706
8955
  noteSpanDetail("backend", result.data.backend);
8707
- if (fallback) {
8708
- opts.log?.(`[ios] snapshot fell back ${fallback.from} -> ${fallback.to}: ${fallback.reason}`);
8709
- noteSpanDetail("fallback", fallback.reason);
8710
- }
8711
- if (target) {
8712
- noteSpanDetail("targetPid", target.pid);
8713
- noteSpanDetail("targetSource", target.source);
8714
- }
8956
+ if (fallback)
8957
+ noteFallback(fallback, opts.log);
8958
+ noteSnapshotTarget(target);
8715
8959
  return { success: true, data: toSnapshot(result.data, now()) };
8716
8960
  };
8717
8961
  async function snapshotOnce(o) {
@@ -8738,10 +8982,11 @@ function createIosDriver(swap, simctl, opts) {
8738
8982
  async function snapshot(o) {
8739
8983
  if (bridgeProven)
8740
8984
  return snapshotOnce(o);
8985
+ const budgetMs = opts.coldBridgeBudgetMs ?? COLD_BRIDGE_BUDGET_MS;
8741
8986
  const result = await retryColdSnapshot(() => snapshotOnce(o), {
8742
8987
  now,
8743
8988
  sleep,
8744
- budgetMs: opts.coldBridgeBudgetMs ?? COLD_BRIDGE_BUDGET_MS,
8989
+ budgetMs: Math.min(budgetMs, readDeadlineRemaining() ?? budgetMs),
8745
8990
  ...opts.log ? { log: opts.log } : {}
8746
8991
  });
8747
8992
  if (result.success)
@@ -8774,7 +9019,7 @@ function createIosDriver(swap, simctl, opts) {
8774
9019
  if (!snap.success)
8775
9020
  throw new DriverFailure(snap.error);
8776
9021
  return snapshotHash(snap.data);
8777
- }, { quietMs: o.quietMs, timeoutMs: o.timeoutMs, now, sleep });
9022
+ }, { quietMs: hashFallbackQuietMs(o.quietMs), timeoutMs: o.timeoutMs, now, sleep });
8778
9023
  noteSpanDetail("via", "hash");
8779
9024
  return { success: true, data: { ...outcome, via: "hash" } };
8780
9025
  } catch (error) {
@@ -8785,6 +9030,7 @@ function createIosDriver(swap, simctl, opts) {
8785
9030
  }
8786
9031
  async function settleAfterLaunch(bundleId) {
8787
9032
  lastLaunched = bundleId;
9033
+ warmGeneration = undefined;
8788
9034
  launchedBundle = bundleId;
8789
9035
  if (await rebind(bundleId))
8790
9036
  launchedBundle = undefined;
@@ -8794,6 +9040,9 @@ function createIosDriver(swap, simctl, opts) {
8794
9040
  const a11yLoaded = await a11y?.confirmLoaded();
8795
9041
  if (a11yLoaded && !a11yLoaded.success)
8796
9042
  return a11yLoaded.error;
9043
+ const kbdLoaded = await kbd?.confirmLoaded();
9044
+ if (kbdLoaded && !kbdLoaded.success)
9045
+ return kbdLoaded.error;
8797
9046
  return;
8798
9047
  }
8799
9048
  async function rebind(bundleId) {
@@ -8808,8 +9057,6 @@ function createIosDriver(swap, simctl, opts) {
8808
9057
  return false;
8809
9058
  }
8810
9059
  noteRebind(reply.data);
8811
- if (reply.data.bound)
8812
- bridgeProven = true;
8813
9060
  return reply.data.bound;
8814
9061
  }
8815
9062
  function noteSettleAxFailure(message) {
@@ -8882,7 +9129,7 @@ function createIosDriver(swap, simctl, opts) {
8882
9129
  return info;
8883
9130
  }
8884
9131
  async function readAlert(backend) {
8885
- const result = await request("alertButtons", params({ backend }));
9132
+ const result = await request("alertButtons", params({ backend }), readOpts());
8886
9133
  if (!result.success)
8887
9134
  return result;
8888
9135
  return { success: true, data: result.data === null ? null : toAlertInfo(result.data) };
@@ -8895,6 +9142,18 @@ function createIosDriver(swap, simctl, opts) {
8895
9142
  const result = await request("frontmost", { bundleId }, { timeoutMs: FRONTMOST_TIMEOUT_MS });
8896
9143
  return result.success ? { success: true, data: result.data.frontmost } : result;
8897
9144
  }
9145
+ async function axWedge(o) {
9146
+ const recover = o?.recover === true;
9147
+ const result = await request("axWedge", { recover }, { timeoutMs: recover ? AX_WEDGE_RECOVER_MS : 5000 });
9148
+ if (result.success)
9149
+ return { success: true, data: toAxWedgeState(result.data) };
9150
+ if (predatesOp(result.error)) {
9151
+ return driverErr("E_UNSUPPORTED", "this warden-simd predates the axWedge op", {
9152
+ detail: { reason: "helper_outdated" }
9153
+ });
9154
+ }
9155
+ return result;
9156
+ }
8898
9157
  async function health() {
8899
9158
  const result = await request("health");
8900
9159
  if (!result.success)
@@ -8938,7 +9197,7 @@ function createIosDriver(swap, simctl, opts) {
8938
9197
  health,
8939
9198
  snapshot,
8940
9199
  async elementAtPoint(point, o) {
8941
- const result = await request("elementAtPoint", params({ x: point.x, y: point.y, reachability: o?.reachability }));
9200
+ const result = await request("elementAtPoint", params({ x: point.x, y: point.y, reachability: o?.reachability }), readOpts());
8942
9201
  if (!result.success)
8943
9202
  return result;
8944
9203
  return {
@@ -8964,6 +9223,7 @@ function createIosDriver(swap, simctl, opts) {
8964
9223
  alert,
8965
9224
  alertOn,
8966
9225
  isFrontmost,
9226
+ axWedge,
8967
9227
  readA11y: async (snapshot) => a11y ? a11y.read(snapshot) : driverErr("E_UNSUPPORTED", "this iOS driver was opened without the injected library channel"),
8968
9228
  a11yEvents: async (since) => a11y ? a11y.events(since) : driverErr("E_UNSUPPORTED", "this iOS driver was opened without the injected library channel"),
8969
9229
  auditNatively: async (options) => nativeAudit ? nativeAudit.audit(options ?? {}) : { issues: [], skipped: { source: "xcuitest", reason: "this iOS driver was opened without the runner" } },
@@ -8976,6 +9236,26 @@ function createIosDriver(swap, simctl, opts) {
8976
9236
  locale: (tag, o) => settings ? settings.locale(tag, o) : noSettings(),
8977
9237
  a11y: (op) => settings ? settings.a11y(op) : noSettings(),
8978
9238
  battery: (op) => settings ? settings.battery(op) : noSettings(),
9239
+ keyboard: async (mode) => kbd ? kbd.keyboard(mode) : driverErr("E_UNSUPPORTED", "this iOS driver was opened without the injected library channel"),
9240
+ async dismissKeyboard() {
9241
+ const resigned = kbd ? await kbd.dismiss() : undefined;
9242
+ if (resigned !== undefined && !resigned.success)
9243
+ return resigned;
9244
+ if (resigned?.data.attached)
9245
+ return { success: true, data: { via: "resign" } };
9246
+ opts.log?.("[ios] keyboard dismissed with Enter: the app was launched without libwarden-inject.dylib, so Return may submit the focused field");
9247
+ const pressed = await sendVoid("key", params({ usage: KEY_USAGE.Enter }));
9248
+ return pressed.success ? { success: true, data: { via: "key" } } : pressed;
9249
+ },
9250
+ async keyboardState() {
9251
+ const taken = await snapshot();
9252
+ if (!taken.success)
9253
+ return taken;
9254
+ return {
9255
+ success: true,
9256
+ data: { mode: kbd?.mode() ?? DEFAULT_KEYBOARD_MODE, visible: keyboardVisible(taken.data) }
9257
+ };
9258
+ },
8979
9259
  camera: async (op) => net ? net.camera(op) : driverErr("E_UNSUPPORTED", "the simulated camera on iOS needs a device lease for the injected dylib: run the spec with `warden e2e`, or `warden ui`; a bare `bun test` has no lease"),
8980
9260
  memoryWarning: (o) => settings ? settings.memoryWarning(o) : noSettings(),
8981
9261
  push: (payload, o) => settings ? settings.push(payload, o) : noSettings(),
@@ -8997,7 +9277,7 @@ function createIosDriver(swap, simctl, opts) {
8997
9277
  app: {
8998
9278
  launch: async (bundleId, plain) => {
8999
9279
  const requested = settings ? withStagedSettings(plain, settings.launchSettings()) : plain;
9000
- const withEnv = await launchWithInjection(net, a11y, requested);
9280
+ const withEnv = await launchWithInjection(net, a11y, kbd, requested);
9001
9281
  if (!withEnv.success)
9002
9282
  return withEnv;
9003
9283
  const o = withEnv.data;
@@ -9133,6 +9413,7 @@ async function openIosDriver(o) {
9133
9413
  ...o.coldBridgeBudgetMs !== undefined ? { coldBridgeBudgetMs: o.coldBridgeBudgetMs } : {},
9134
9414
  ...network ? { network } : {},
9135
9415
  a11y: () => iosA11yFor(o, now, sleep),
9416
+ keyboard: () => iosKeyboardFor(o, now, sleep),
9136
9417
  nativeAudit: (ctx) => nativeAuditFor(o, exec, log, ctx),
9137
9418
  settings: (ctx) => settingsFor(o, exec, ctx),
9138
9419
  ...o.clockRecorder ? { clockRecorder: o.clockRecorder } : {}
@@ -9172,6 +9453,28 @@ function iosNetworkFactory(o, simctl, now, sleep) {
9172
9453
  ...o.debug ? { debug: true } : {}
9173
9454
  });
9174
9455
  }
9456
+ function iosKeyboardFor(o, now, sleep) {
9457
+ const wardenHome2 = o.wardenHome ?? wardenHome();
9458
+ return createIosKeyboard({
9459
+ channel: createInjectChannel({
9460
+ statePath: injectStatePath2(wardenHome2, o.udid),
9461
+ now,
9462
+ sleep,
9463
+ ...o.bundleId ? { bundleId: () => o.bundleId } : {},
9464
+ ...o.injectFs ? { fs: o.injectFs } : {}
9465
+ }),
9466
+ dylib: () => locateInjectDylib({
9467
+ env: { [INJECT_DYLIB_ENV]: process.env[INJECT_DYLIB_ENV], WARDEN_HELPER_DIR: o.env?.helperDir },
9468
+ execPath: o.execPath ?? process.execPath,
9469
+ importDir: o.importDir ?? import.meta.dir,
9470
+ wardenHome: wardenHome2,
9471
+ exists: o.exists ?? existsSync8,
9472
+ realpath: o.realpath ?? realpathSync2,
9473
+ ...o.version ? { version: o.version } : {}
9474
+ }),
9475
+ ...o.debug ? { debug: true } : {}
9476
+ });
9477
+ }
9175
9478
  function iosA11yFor(o, now, sleep) {
9176
9479
  const wardenHome2 = o.wardenHome ?? wardenHome();
9177
9480
  return createIosA11y({
@@ -9415,7 +9718,7 @@ var SCREENSHOT_FALLBACK2 = new Set(["E_UNSUPPORTED", "E_IO", "E_INTERNAL"]);
9415
9718
  function params2(values) {
9416
9719
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
9417
9720
  }
9418
- var okVoid3 = { success: true, data: undefined };
9721
+ var okVoid4 = { success: true, data: undefined };
9419
9722
  function fail3(error) {
9420
9723
  return { success: false, error };
9421
9724
  }
@@ -9448,7 +9751,7 @@ function createIosDeviceDriver(client, devicectl, opts) {
9448
9751
  }
9449
9752
  const sendVoid = async (op, p = {}, timeoutMs) => {
9450
9753
  const result = await request(op, p, timeoutMs === undefined ? {} : { timeoutMs });
9451
- return result.success ? okVoid3 : result;
9754
+ return result.success ? okVoid4 : result;
9452
9755
  };
9453
9756
  async function snapshot(o) {
9454
9757
  const result = await request("snapshot", params2({ interactiveOnly: o?.interactiveOnly, backend: o?.backend }));
@@ -9466,7 +9769,12 @@ function createIosDeviceDriver(client, devicectl, opts) {
9466
9769
  if (!snap.success)
9467
9770
  throw new DriverFailure2(snap.error);
9468
9771
  return snapshotHash(snap.data);
9469
- }, { quietMs: o.quietMs ?? DEFAULT_QUIET_MS2, timeoutMs: o.timeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS3, now, sleep });
9772
+ }, {
9773
+ quietMs: hashFallbackQuietMs(o.quietMs ?? DEFAULT_QUIET_MS2),
9774
+ timeoutMs: o.timeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS3,
9775
+ now,
9776
+ sleep
9777
+ });
9470
9778
  noteSpanDetail("via", "hash");
9471
9779
  return { success: true, data: { ...outcome, via: "hash" } };
9472
9780
  } catch (error) {
@@ -9578,7 +9886,7 @@ function createIosDeviceDriver(client, devicectl, opts) {
9578
9886
  return installed;
9579
9887
  for (const bundleId of installed.data.bundleIds)
9580
9888
  owned.add(bundleId);
9581
- return okVoid3;
9889
+ return okVoid4;
9582
9890
  }
9583
9891
  async function uninstall(bundleId) {
9584
9892
  const refused = guard(bundleId, "uninstall");
@@ -9617,6 +9925,11 @@ function createIosDeviceDriver(client, devicectl, opts) {
9617
9925
  gesture: async () => unsupported2("gesture", "the XCUITest runner takes taps, swipes and drags only"),
9618
9926
  biometrics: async () => unsupported2("biometrics", "Face ID / Touch ID cannot be simulated on a real sensor; use a simulator"),
9619
9927
  ...physicalConditionOps(`physical iPhone ${udid}`),
9928
+ dismissKeyboard: async () => {
9929
+ const pressed = await sendVoid("key", params2({ usage: KEY_USAGE.Enter }));
9930
+ return pressed.success ? { success: true, data: { via: "key" } } : pressed;
9931
+ },
9932
+ keyboardState: async () => unsupported2("the keyboard mode", "the injected library runs in simulator apps only"),
9620
9933
  typeText: (text) => sendVoid("typeText", { text }, deviceTypeTextBudgetMs(text)),
9621
9934
  key: (key, modifiers) => sendVoid("key", params2({ usage: KEY_USAGE[key], modifiers: modifiers && [...modifiers] })),
9622
9935
  pressButton,
@@ -10339,6 +10652,10 @@ function createDevice({
10339
10652
  locale: conditions.locale,
10340
10653
  a11y: conditions.a11y,
10341
10654
  battery: conditions.battery,
10655
+ keyboard: conditions.keyboard,
10656
+ async keyboardVisible() {
10657
+ return (await unwrap(() => driver.keyboardState(), "read the keyboard", signal())).visible;
10658
+ },
10342
10659
  camera: conditions.camera,
10343
10660
  memoryWarning: conditions.memoryWarning,
10344
10661
  push: conditions.push,
@@ -10408,13 +10725,9 @@ function createDevice({
10408
10725
  await unwrap(() => driver.clipboard(text), "set clipboard", signal());
10409
10726
  },
10410
10727
  async hideKeyboard() {
10411
- const snapshot = await readSnapshot();
10412
- if (!snapshot.flat.some((element) => element.role === "keyboard"))
10728
+ if (!await device.keyboardVisible())
10413
10729
  return;
10414
- if (driver.platform === "ios")
10415
- await unwrap(() => driver.key("Enter"), "hide keyboard", signal());
10416
- else
10417
- await unwrap(() => driver.pressButton("back"), "hide keyboard", signal());
10730
+ await unwrap(() => driver.dismissKeyboard(), "hide keyboard", signal());
10418
10731
  },
10419
10732
  async screenshot(name) {
10420
10733
  const dir = session.attemptDir ?? join22(session.out, "adhoc");
@@ -10455,7 +10768,7 @@ function createDevice({
10455
10768
  if (native.error.code !== "E_UNSUPPORTED")
10456
10769
  throw toE2eError(native.error, "settle");
10457
10770
  const polled = await settleByHash(async () => snapshotHash(await readSnapshot()), {
10458
- quietMs,
10771
+ quietMs: hashFallbackQuietMs(quietMs),
10459
10772
  timeoutMs,
10460
10773
  now: clock.now,
10461
10774
  sleep: clock.sleep
@@ -10534,13 +10847,11 @@ var EXCERPT_LINES = 40;
10534
10847
  var MAX_ALERT_ANSWERS = 3;
10535
10848
  var REACT_THROTTLE_MS = 450;
10536
10849
  var REACT_TIMEOUT_MS = 2000;
10537
- var FIRST_READER_RETRY_MS = 5000;
10538
10850
 
10539
10851
  class Runtime {
10540
10852
  host;
10541
10853
  reactCache;
10542
10854
  indexed = new WeakMap;
10543
- firstReaderRetryAt = 0;
10544
10855
  constructor(host) {
10545
10856
  this.host = host;
10546
10857
  }
@@ -10580,7 +10891,7 @@ class Runtime {
10580
10891
  const result = canObserve(driver) ? await driver.observe({ reuse }) : await driver.snapshot();
10581
10892
  if (!result.success)
10582
10893
  return { ok: false, error: result.error };
10583
- const snapshot = await this.restoreDegraded(driver, result.data);
10894
+ const snapshot = result.data;
10584
10895
  const observation = {
10585
10896
  ok: true,
10586
10897
  observed: { driver, snapshot, index: this.indexOf(snapshot) }
@@ -10594,16 +10905,6 @@ class Runtime {
10594
10905
  reuse = false;
10595
10906
  }
10596
10907
  }
10597
- async restoreDegraded(driver, snapshot) {
10598
- const first = snapshot.fellBackFrom;
10599
- if (first === undefined || this.clock.now() < this.firstReaderRetryAt)
10600
- return snapshot;
10601
- const again = await driver.snapshot({ backend: first });
10602
- if (again.success && again.data.backend === first)
10603
- return again.data;
10604
- this.firstReaderRetryAt = this.clock.now() + FIRST_READER_RETRY_MS;
10605
- return snapshot;
10606
- }
10607
10908
  earlierThan(driver, before, look) {
10608
10909
  const after = canObserve(driver) ? driver.previous?.() : undefined;
10609
10910
  const same = after !== undefined && after.generation === before.generation && after.snapshot === look;
@@ -10640,18 +10941,25 @@ class Runtime {
10640
10941
  throw driverFailure("snapshot", result.last ?? { code: "E_TIMEOUT", message: "no snapshot", retryable: true });
10641
10942
  }
10642
10943
  async reactMatches(testId, observed) {
10944
+ const lookup = await this.reactLookup(testId, observed);
10945
+ return lookup.answered ? lookup.matches : [];
10946
+ }
10947
+ hasReactTree() {
10948
+ return this.host.devtools?.() !== undefined;
10949
+ }
10950
+ async reactLookup(testId, observed) {
10643
10951
  const facet = this.host.devtools?.();
10644
10952
  if (facet === undefined)
10645
- return [];
10953
+ return { answered: false };
10646
10954
  const key = typeof testId === "string" ? `s:${testId}` : `r:${testId.toString()}`;
10647
10955
  const cached = this.reactCache;
10648
10956
  if (cached !== undefined && cached.key === key && this.clock.now() - cached.at < REACT_THROTTLE_MS) {
10649
- return cached.matches;
10957
+ return cached.lookup;
10650
10958
  }
10651
10959
  const view = { platform: observed.snapshot.platform, screen: observed.snapshot.screen };
10652
- const matches = await locateByTestIdViaReact(facet, testId, view, { timeoutMs: REACT_TIMEOUT_MS });
10653
- this.reactCache = { key, at: this.clock.now(), matches };
10654
- return matches;
10960
+ const lookup = await lookUpTestIdViaReact(facet, testId, view, { timeoutMs: REACT_TIMEOUT_MS });
10961
+ this.reactCache = { key, at: this.clock.now(), lookup };
10962
+ return lookup;
10655
10963
  }
10656
10964
  excerpt(snapshot) {
10657
10965
  const rendered = renderSnapshot(snapshot, { interactiveOnly: true, refs: new Map, maxLines: EXCERPT_LINES });
@@ -10660,13 +10968,15 @@ class Runtime {
10660
10968
  }
10661
10969
  async poll(step, timeoutMs, options = {}) {
10662
10970
  const signal = this.host.signal;
10663
- const result = await poll(step, {
10971
+ const outer = readDeadlineRemaining();
10972
+ const deadline = this.clock.now() + (outer === undefined ? timeoutMs : Math.min(timeoutMs, outer));
10973
+ const result = await withReadDeadline(() => deadline - this.clock.now(), () => poll(step, {
10664
10974
  timeoutMs,
10665
10975
  ...options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs },
10666
10976
  ...signal === undefined ? {} : { signal },
10667
10977
  now: this.clock.now,
10668
10978
  sleep: this.clock.sleep
10669
- });
10979
+ }));
10670
10980
  if (result.status === "aborted")
10671
10981
  throw abortError(signal);
10672
10982
  return result;
@@ -10839,6 +11149,8 @@ function traceDevice(device, span) {
10839
11149
  locale: (tag, options) => span("device.locale", () => device.locale(tag, options)),
10840
11150
  a11y: (op) => span("device.a11y", () => device.a11y(op)),
10841
11151
  battery: (op) => span("device.battery", () => device.battery(op)),
11152
+ keyboard: (mode) => span("device.keyboard", () => device.keyboard(mode)),
11153
+ keyboardVisible: () => span("device.keyboardVisible", () => device.keyboardVisible()),
10842
11154
  camera: {
10843
11155
  set: (path, options) => span("device.camera.set", () => camera.set(path, options)),
10844
11156
  clear: (facing) => span("device.camera.clear", () => camera.clear(facing)),