@delacour/warden-e2e 0.14.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 (34) hide show
  1. package/{chunk-zk3wxa70.mjs → chunk-e3cry5br.mjs} +287 -37
  2. package/index.mjs +32 -3
  3. package/package.json +2 -2
  4. package/preload.mjs +304 -30
  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 +4 -0
  19. package/types/e2e/src/driver/driver.types.d.ts +30 -1
  20. package/types/e2e/src/driver/ios/inject.d.ts +17 -2
  21. package/types/e2e/src/driver/ios/ios-driver.d.ts +7 -2
  22. package/types/e2e/src/driver/ios/ios-keyboard.d.ts +39 -0
  23. package/types/e2e/src/driver/ios/simd-protocol.d.ts +99 -0
  24. package/types/e2e/src/driver/keyboard-state.d.ts +9 -0
  25. package/types/e2e/src/driver/physical-conditions.d.ts +1 -1
  26. package/types/e2e/src/driver/read-deadline.d.ts +4 -0
  27. package/types/e2e/src/driver/settle.d.ts +3 -1
  28. package/types/e2e/src/expect/a11y-matchers.d.ts +7 -0
  29. package/types/e2e/src/index.d.ts +2 -1
  30. package/types/e2e/src/screen/actions.d.ts +7 -0
  31. package/types/e2e/src/screen/context.d.ts +3 -1
  32. package/types/e2e/src/screen/keyboard-cover.d.ts +26 -0
  33. package/types/e2e/src/screen/locator.d.ts +16 -7
  34. package/types/e2e/src/session.d.ts +6 -0
package/preload.mjs CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  isInteractiveRole,
31
31
  resolveRole,
32
32
  indexSnapshot,
33
+ keyboardVisible,
33
34
  testIdQuery,
34
35
  textQuery,
35
36
  roleQuery,
@@ -61,6 +62,10 @@ import {
61
62
  createSimdClient,
62
63
  spawnSimd,
63
64
  connectSimd,
65
+ KEYBOARD_MODES,
66
+ DEFAULT_KEYBOARD_MODE,
67
+ IME_STASH_ENABLED,
68
+ IME_STASH_DEFAULT,
64
69
  CAMERA_FACINGS,
65
70
  CONDITION_AREAS,
66
71
  resolveNetwork,
@@ -114,7 +119,7 @@ import {
114
119
  onlyWasSeen,
115
120
  WORKSPACE_VERSION,
116
121
  WARDEN_E2E_VERSION2
117
- } from "./chunk-zk3wxa70.mjs";
122
+ } from "./chunk-e3cry5br.mjs";
118
123
 
119
124
  // ../../packages/e2e/src/preload.ts
120
125
  import { afterAll, setDefaultTimeout } from "bun:test";
@@ -1376,6 +1381,8 @@ function physicalConditionOps(device) {
1376
1381
  locale: () => refuse("locale"),
1377
1382
  a11y: () => refuse("a11y"),
1378
1383
  battery: () => refuse("battery"),
1384
+ keyboard: () => refuse("keyboard"),
1385
+ keyboardState: () => refuse("keyboardState"),
1379
1386
  memoryWarning: () => refuse("memoryWarning"),
1380
1387
  push: () => refuse("push"),
1381
1388
  camera: () => refuse("camera")
@@ -4079,6 +4086,75 @@ function batteryOp(c) {
4079
4086
  };
4080
4087
  };
4081
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
+ }
4082
4158
  function memoryWarningOp(c) {
4083
4159
  return async (opts) => {
4084
4160
  const app = c.packageOf(opts?.bundleId, "memoryWarning");
@@ -4153,6 +4229,12 @@ function createAndroidSettings(deps) {
4153
4229
  locale: localeOp(ctx),
4154
4230
  a11y: a11yOp(ctx),
4155
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
+ },
4156
4238
  memoryWarning: memoryWarningOp(ctx),
4157
4239
  async push() {
4158
4240
  return driverErr("E_UNSUPPORTED", "push notifications through the shell are iOS only (`simctl push`); Android needs FCM");
@@ -5840,7 +5922,12 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5840
5922
  });
5841
5923
  return { success: true, data: { ...applied.data, condition: op } };
5842
5924
  };
5843
- const { talkback, ...settings } = createAndroidSettings({
5925
+ const {
5926
+ talkback,
5927
+ keyboardMode: _mode,
5928
+ keyboardVisible: _visible,
5929
+ ...settings
5930
+ } = createAndroidSettings({
5844
5931
  session,
5845
5932
  serial,
5846
5933
  ...options.appPackage ? { appPackage: options.appPackage } : {},
@@ -5958,6 +6045,11 @@ function buildDriver(options, session, baseScreen, clock, execBinary, facts) {
5958
6045
  ...options.cameraHost ?? createAndroidCameraHost({ serial, exec })
5959
6046
  }),
5960
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
+ },
5961
6053
  biometrics,
5962
6054
  app: createAppCommands({
5963
6055
  session,
@@ -6036,6 +6128,17 @@ function writeLaunchSettings(path, settings) {
6036
6128
  }
6037
6129
  }
6038
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
+
6039
6142
  // ../../packages/e2e/src/driver/ios/inject.ts
6040
6143
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, renameSync as renameSync6, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "fs";
6041
6144
  import { dirname as dirname3, join as join10, resolve } from "path";
@@ -6056,12 +6159,17 @@ function encodeInjectState(state) {
6056
6159
  clockOffsetMs: Math.trunc(state.clockOffsetMs ?? 0),
6057
6160
  ...state.clockFrozenMs === undefined ? {} : { clockFrozenMs: Math.trunc(state.clockFrozenMs) },
6058
6161
  ...state.a11yRead ? { a11yRead: true } : {},
6059
- ...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 } : {}
6060
6165
  });
6061
6166
  }
6062
6167
  function isRecord3(value) {
6063
6168
  return typeof value === "object" && value !== null && !Array.isArray(value);
6064
6169
  }
6170
+ function parseKeyboard(value) {
6171
+ return KEYBOARD_MODES.find((mode) => mode === value);
6172
+ }
6065
6173
  function parseFeed(value) {
6066
6174
  if (!isRecord3(value) || typeof value.path !== "string")
6067
6175
  return;
@@ -6089,7 +6197,8 @@ function parseInjectState(text) {
6089
6197
  clockOffsetMs: typeof data.clockOffsetMs === "number" ? data.clockOffsetMs : 0,
6090
6198
  ...typeof data.clockFrozenMs === "number" ? { clockFrozenMs: data.clockFrozenMs } : {},
6091
6199
  ...data.a11yRead === true ? { a11yRead: true } : {},
6092
- ..."camera" in data ? { camera: parseCamera(data.camera) } : {}
6200
+ ..."camera" in data ? { camera: parseCamera(data.camera) } : {},
6201
+ ...parseKeyboard(data.keyboard) ? { keyboard: parseKeyboard(data.keyboard) } : {}
6093
6202
  };
6094
6203
  } catch {
6095
6204
  return;
@@ -6123,11 +6232,12 @@ function injectLaunchEnv(input) {
6123
6232
  ...input.clockOffsetMs ? { WARDEN_CLOCK_OFFSET_MS: String(Math.trunc(input.clockOffsetMs)) } : {},
6124
6233
  ...input.clockFrozenMs === undefined ? {} : { WARDEN_CLOCK_FROZEN_MS: String(Math.trunc(input.clockFrozenMs)) },
6125
6234
  ...input.camera ? { WARDEN_CAMERA: "1" } : {},
6235
+ ...input.keyboard ? { WARDEN_KEYBOARD: input.keyboard } : {},
6126
6236
  ...input.debug ? { WARDEN_INJECT_DEBUG: "1" } : {}
6127
6237
  };
6128
6238
  }
6129
6239
  function shouldInject(input) {
6130
- return input.networkActive || input.clockActive || input.cameraActive === true || input.requested;
6240
+ return input.networkActive || input.clockActive || input.cameraActive === true || input.keyboardActive === true || input.requested;
6131
6241
  }
6132
6242
  function locateInjectDylib(input) {
6133
6243
  const tried = [];
@@ -6212,7 +6322,13 @@ function createInjectChannel(input) {
6212
6322
  current: readState,
6213
6323
  write(next) {
6214
6324
  fs.mkdir(dirname3(statePath));
6215
- 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
+ };
6216
6332
  fs.writeAtomic(statePath, encodeInjectState(state));
6217
6333
  return state;
6218
6334
  },
@@ -6229,9 +6345,14 @@ function createInjectChannel(input) {
6229
6345
  }
6230
6346
  },
6231
6347
  reset() {
6348
+ const keyboard = readState().keyboard;
6232
6349
  fs.remove(ackPath);
6233
6350
  fs.remove(statePath);
6234
6351
  fs.remove(injectEventsPath(statePath));
6352
+ if (keyboard !== undefined) {
6353
+ fs.mkdir(dirname3(statePath));
6354
+ fs.writeAtomic(statePath, encodeInjectState({ seq: 0, keyboard }));
6355
+ }
6235
6356
  }
6236
6357
  };
6237
6358
  }
@@ -6792,8 +6913,14 @@ function defaultBuildCommand(platform, configuration, framework = "expo") {
6792
6913
  if (platform === "ios") {
6793
6914
  return `${IOS_NATIVE_PREPARE} && bunx expo run:ios ${configuration === "Release" ? "--configuration Release " : ""}${flags}`;
6794
6915
  }
6795
- 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}`;
6796
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)"';
6797
6924
  var DEFAULT_BUILD_COMMAND = {
6798
6925
  ios: defaultBuildCommand("ios", "Debug"),
6799
6926
  android: defaultBuildCommand("android", "Debug")
@@ -7370,6 +7497,63 @@ function createIosNativeAudit(o) {
7370
7497
  };
7371
7498
  }
7372
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
+
7373
7557
  // ../../packages/e2e/src/driver/ios/ios-session.ts
7374
7558
  var SHUTDOWN_TIMEOUT_MS = 500;
7375
7559
  function swappableClient(initial, o) {
@@ -7779,6 +7963,9 @@ function optionalFields(node) {
7779
7963
  fields.pid = node.pid;
7780
7964
  return fields;
7781
7965
  }
7966
+ function roleOf(node) {
7967
+ return node.role === "other" && node.identifier === "inputView" ? "keyboard" : node.role;
7968
+ }
7782
7969
  function normalizeElements(wire, screen, prefix = "", depth = 0) {
7783
7970
  const flat = [];
7784
7971
  const elements = wire.map((node, index) => {
@@ -7786,7 +7973,7 @@ function normalizeElements(wire, screen, prefix = "", depth = 0) {
7786
7973
  const element = {
7787
7974
  ...optionalFields(node),
7788
7975
  id: node.id ?? path,
7789
- role: node.role,
7976
+ role: roleOf(node),
7790
7977
  rawType: node.rawType,
7791
7978
  frame: node.frame,
7792
7979
  depth: node.depth ?? depth,
@@ -8459,7 +8646,7 @@ var SOCKET_RETRY_MS = 500;
8459
8646
  function params(values) {
8460
8647
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
8461
8648
  }
8462
- var okVoid2 = { success: true, data: undefined };
8649
+ var okVoid3 = { success: true, data: undefined };
8463
8650
  function fail2(error) {
8464
8651
  return { success: false, error };
8465
8652
  }
@@ -8574,8 +8761,26 @@ function healthDetail(d) {
8574
8761
  ...d.ax.error !== undefined ? { axError: d.ax.error } : {},
8575
8762
  ...d.socket ? { socket: d.socket } : {},
8576
8763
  ...d.xcode ? { xcode: d.xcode } : {},
8577
- ...d.state ? { state: d.state } : {}
8578
- };
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;
8579
8784
  }
8580
8785
 
8581
8786
  class DriverFailure extends Error {
@@ -8598,14 +8803,15 @@ function decodeBase64(png) {
8598
8803
  function predatesOp(error) {
8599
8804
  return error.code === "E_UNSUPPORTED" || error.code === "E_BAD_REQUEST" && /unknown (input )?op/.test(error.message);
8600
8805
  }
8601
- async function launchWithInjection(net, a11y, requested) {
8806
+ async function launchWithInjection(net, a11y, keyboard, requested) {
8602
8807
  const injected = net ? await net.launchEnv(requested ?? {}) : undefined;
8603
8808
  if (injected && !injected.success)
8604
8809
  return injected;
8605
8810
  const forA11y = injected?.data || !a11y ? undefined : await a11y.launchEnv(requested ?? {});
8606
8811
  if (forA11y && !forA11y.success)
8607
8812
  return forA11y;
8608
- const env = injected?.data ?? forA11y?.data;
8813
+ const carried = injected?.data ?? forA11y?.data;
8814
+ const env = keyboard ? carried ? { ...carried, ...keyboard.launchMode() } : keyboard.launchEnv() : carried;
8609
8815
  return { success: true, data: env ? { ...requested, env: { ...requested?.env, ...env } } : requested };
8610
8816
  }
8611
8817
  function createIosDriver(swap, simctl, opts) {
@@ -8620,6 +8826,7 @@ function createIosDriver(swap, simctl, opts) {
8620
8826
  relaunch: (bundleId) => driverRef ? driverRef.app.launch(bundleId, { relaunch: true, inject: true }) : noLeaseProxy()
8621
8827
  });
8622
8828
  const a11y = opts.a11y?.();
8829
+ const kbd = opts.keyboard?.();
8623
8830
  const nativeAudit = opts.nativeAudit?.({ lastBundle: () => lastLaunched });
8624
8831
  const { client } = swap;
8625
8832
  let axFailures = 0;
@@ -8686,7 +8893,7 @@ function createIosDriver(swap, simctl, opts) {
8686
8893
  const send = (op, p = {}, timeoutMs) => request(op, p, timeoutMs === undefined ? {} : { timeoutMs });
8687
8894
  const sendVoid = async (op, p = {}, timeoutMs) => {
8688
8895
  const result = await send(op, p, timeoutMs);
8689
- return result.success ? okVoid2 : result;
8896
+ return result.success ? okVoid3 : result;
8690
8897
  };
8691
8898
  const gestureTimeout = (durationMs, base = 0) => (durationMs ?? 0) + base + GESTURE_MARGIN_MS;
8692
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" } }));
@@ -8725,7 +8932,7 @@ function createIosDriver(swap, simctl, opts) {
8725
8932
  if (!viaHelper.success)
8726
8933
  return viaHelper;
8727
8934
  note(viaHelper.data);
8728
- return okVoid2;
8935
+ return okVoid3;
8729
8936
  }
8730
8937
  let failureDiagnosed = false;
8731
8938
  const fetchSnapshot = async (o) => {
@@ -8775,10 +8982,11 @@ function createIosDriver(swap, simctl, opts) {
8775
8982
  async function snapshot(o) {
8776
8983
  if (bridgeProven)
8777
8984
  return snapshotOnce(o);
8985
+ const budgetMs = opts.coldBridgeBudgetMs ?? COLD_BRIDGE_BUDGET_MS;
8778
8986
  const result = await retryColdSnapshot(() => snapshotOnce(o), {
8779
8987
  now,
8780
8988
  sleep,
8781
- budgetMs: opts.coldBridgeBudgetMs ?? COLD_BRIDGE_BUDGET_MS,
8989
+ budgetMs: Math.min(budgetMs, readDeadlineRemaining() ?? budgetMs),
8782
8990
  ...opts.log ? { log: opts.log } : {}
8783
8991
  });
8784
8992
  if (result.success)
@@ -8832,6 +9040,9 @@ function createIosDriver(swap, simctl, opts) {
8832
9040
  const a11yLoaded = await a11y?.confirmLoaded();
8833
9041
  if (a11yLoaded && !a11yLoaded.success)
8834
9042
  return a11yLoaded.error;
9043
+ const kbdLoaded = await kbd?.confirmLoaded();
9044
+ if (kbdLoaded && !kbdLoaded.success)
9045
+ return kbdLoaded.error;
8835
9046
  return;
8836
9047
  }
8837
9048
  async function rebind(bundleId) {
@@ -8846,8 +9057,6 @@ function createIosDriver(swap, simctl, opts) {
8846
9057
  return false;
8847
9058
  }
8848
9059
  noteRebind(reply.data);
8849
- if (reply.data.bound)
8850
- bridgeProven = true;
8851
9060
  return reply.data.bound;
8852
9061
  }
8853
9062
  function noteSettleAxFailure(message) {
@@ -8933,6 +9142,18 @@ function createIosDriver(swap, simctl, opts) {
8933
9142
  const result = await request("frontmost", { bundleId }, { timeoutMs: FRONTMOST_TIMEOUT_MS });
8934
9143
  return result.success ? { success: true, data: result.data.frontmost } : result;
8935
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
+ }
8936
9157
  async function health() {
8937
9158
  const result = await request("health");
8938
9159
  if (!result.success)
@@ -9002,6 +9223,7 @@ function createIosDriver(swap, simctl, opts) {
9002
9223
  alert,
9003
9224
  alertOn,
9004
9225
  isFrontmost,
9226
+ axWedge,
9005
9227
  readA11y: async (snapshot) => a11y ? a11y.read(snapshot) : driverErr("E_UNSUPPORTED", "this iOS driver was opened without the injected library channel"),
9006
9228
  a11yEvents: async (since) => a11y ? a11y.events(since) : driverErr("E_UNSUPPORTED", "this iOS driver was opened without the injected library channel"),
9007
9229
  auditNatively: async (options) => nativeAudit ? nativeAudit.audit(options ?? {}) : { issues: [], skipped: { source: "xcuitest", reason: "this iOS driver was opened without the runner" } },
@@ -9014,6 +9236,26 @@ function createIosDriver(swap, simctl, opts) {
9014
9236
  locale: (tag, o) => settings ? settings.locale(tag, o) : noSettings(),
9015
9237
  a11y: (op) => settings ? settings.a11y(op) : noSettings(),
9016
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
+ },
9017
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"),
9018
9260
  memoryWarning: (o) => settings ? settings.memoryWarning(o) : noSettings(),
9019
9261
  push: (payload, o) => settings ? settings.push(payload, o) : noSettings(),
@@ -9035,7 +9277,7 @@ function createIosDriver(swap, simctl, opts) {
9035
9277
  app: {
9036
9278
  launch: async (bundleId, plain) => {
9037
9279
  const requested = settings ? withStagedSettings(plain, settings.launchSettings()) : plain;
9038
- const withEnv = await launchWithInjection(net, a11y, requested);
9280
+ const withEnv = await launchWithInjection(net, a11y, kbd, requested);
9039
9281
  if (!withEnv.success)
9040
9282
  return withEnv;
9041
9283
  const o = withEnv.data;
@@ -9171,6 +9413,7 @@ async function openIosDriver(o) {
9171
9413
  ...o.coldBridgeBudgetMs !== undefined ? { coldBridgeBudgetMs: o.coldBridgeBudgetMs } : {},
9172
9414
  ...network ? { network } : {},
9173
9415
  a11y: () => iosA11yFor(o, now, sleep),
9416
+ keyboard: () => iosKeyboardFor(o, now, sleep),
9174
9417
  nativeAudit: (ctx) => nativeAuditFor(o, exec, log, ctx),
9175
9418
  settings: (ctx) => settingsFor(o, exec, ctx),
9176
9419
  ...o.clockRecorder ? { clockRecorder: o.clockRecorder } : {}
@@ -9210,6 +9453,28 @@ function iosNetworkFactory(o, simctl, now, sleep) {
9210
9453
  ...o.debug ? { debug: true } : {}
9211
9454
  });
9212
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
+ }
9213
9478
  function iosA11yFor(o, now, sleep) {
9214
9479
  const wardenHome2 = o.wardenHome ?? wardenHome();
9215
9480
  return createIosA11y({
@@ -9453,7 +9718,7 @@ var SCREENSHOT_FALLBACK2 = new Set(["E_UNSUPPORTED", "E_IO", "E_INTERNAL"]);
9453
9718
  function params2(values) {
9454
9719
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
9455
9720
  }
9456
- var okVoid3 = { success: true, data: undefined };
9721
+ var okVoid4 = { success: true, data: undefined };
9457
9722
  function fail3(error) {
9458
9723
  return { success: false, error };
9459
9724
  }
@@ -9486,7 +9751,7 @@ function createIosDeviceDriver(client, devicectl, opts) {
9486
9751
  }
9487
9752
  const sendVoid = async (op, p = {}, timeoutMs) => {
9488
9753
  const result = await request(op, p, timeoutMs === undefined ? {} : { timeoutMs });
9489
- return result.success ? okVoid3 : result;
9754
+ return result.success ? okVoid4 : result;
9490
9755
  };
9491
9756
  async function snapshot(o) {
9492
9757
  const result = await request("snapshot", params2({ interactiveOnly: o?.interactiveOnly, backend: o?.backend }));
@@ -9621,7 +9886,7 @@ function createIosDeviceDriver(client, devicectl, opts) {
9621
9886
  return installed;
9622
9887
  for (const bundleId of installed.data.bundleIds)
9623
9888
  owned.add(bundleId);
9624
- return okVoid3;
9889
+ return okVoid4;
9625
9890
  }
9626
9891
  async function uninstall(bundleId) {
9627
9892
  const refused = guard(bundleId, "uninstall");
@@ -9660,6 +9925,11 @@ function createIosDeviceDriver(client, devicectl, opts) {
9660
9925
  gesture: async () => unsupported2("gesture", "the XCUITest runner takes taps, swipes and drags only"),
9661
9926
  biometrics: async () => unsupported2("biometrics", "Face ID / Touch ID cannot be simulated on a real sensor; use a simulator"),
9662
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"),
9663
9933
  typeText: (text) => sendVoid("typeText", { text }, deviceTypeTextBudgetMs(text)),
9664
9934
  key: (key, modifiers) => sendVoid("key", params2({ usage: KEY_USAGE[key], modifiers: modifiers && [...modifiers] })),
9665
9935
  pressButton,
@@ -10382,6 +10652,10 @@ function createDevice({
10382
10652
  locale: conditions.locale,
10383
10653
  a11y: conditions.a11y,
10384
10654
  battery: conditions.battery,
10655
+ keyboard: conditions.keyboard,
10656
+ async keyboardVisible() {
10657
+ return (await unwrap(() => driver.keyboardState(), "read the keyboard", signal())).visible;
10658
+ },
10385
10659
  camera: conditions.camera,
10386
10660
  memoryWarning: conditions.memoryWarning,
10387
10661
  push: conditions.push,
@@ -10451,13 +10725,9 @@ function createDevice({
10451
10725
  await unwrap(() => driver.clipboard(text), "set clipboard", signal());
10452
10726
  },
10453
10727
  async hideKeyboard() {
10454
- const snapshot = await readSnapshot();
10455
- if (!snapshot.flat.some((element) => element.role === "keyboard"))
10728
+ if (!await device.keyboardVisible())
10456
10729
  return;
10457
- if (driver.platform === "ios")
10458
- await unwrap(() => driver.key("Enter"), "hide keyboard", signal());
10459
- else
10460
- await unwrap(() => driver.pressButton("back"), "hide keyboard", signal());
10730
+ await unwrap(() => driver.dismissKeyboard(), "hide keyboard", signal());
10461
10731
  },
10462
10732
  async screenshot(name) {
10463
10733
  const dir = session.attemptDir ?? join22(session.out, "adhoc");
@@ -10698,13 +10968,15 @@ class Runtime {
10698
10968
  }
10699
10969
  async poll(step, timeoutMs, options = {}) {
10700
10970
  const signal = this.host.signal;
10701
- 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, {
10702
10974
  timeoutMs,
10703
10975
  ...options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs },
10704
10976
  ...signal === undefined ? {} : { signal },
10705
10977
  now: this.clock.now,
10706
10978
  sleep: this.clock.sleep
10707
- });
10979
+ }));
10708
10980
  if (result.status === "aborted")
10709
10981
  throw abortError(signal);
10710
10982
  return result;
@@ -10877,6 +11149,8 @@ function traceDevice(device, span) {
10877
11149
  locale: (tag, options) => span("device.locale", () => device.locale(tag, options)),
10878
11150
  a11y: (op) => span("device.a11y", () => device.a11y(op)),
10879
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()),
10880
11154
  camera: {
10881
11155
  set: (path, options) => span("device.camera.set", () => camera.set(path, options)),
10882
11156
  clear: (facing) => span("device.camera.clear", () => camera.clear(facing)),
@@ -167,6 +167,7 @@ export declare const e2eSuiteSchema: z.ZodObject<{
167
167
  except: z.ZodOptional<z.ZodArray<z.ZodString>>;
168
168
  keep: z.ZodOptional<z.ZodArray<z.ZodString>>;
169
169
  }, z.core.$strict>]>>;
170
+ golden: z.ZodOptional<z.ZodBoolean>;
170
171
  biometrics: z.ZodOptional<z.ZodBoolean>;
171
172
  reclaim: z.ZodOptional<z.ZodEnum<{
172
173
  none: "none";
@@ -262,6 +263,10 @@ export declare const e2eSuiteSchema: z.ZodObject<{
262
263
  }>>;
263
264
  reduceMotion: z.ZodOptional<z.ZodBoolean>;
264
265
  }, z.core.$strict>>;
266
+ keyboard: z.ZodOptional<z.ZodEnum<{
267
+ software: "software";
268
+ hardware: "hardware";
269
+ }>>;
265
270
  }, z.core.$strict>>;
266
271
  a11y: z.ZodOptional<z.ZodObject<{
267
272
  ignore: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -12,6 +12,12 @@ export type BuildConfiguration = "Debug" | "Release";
12
12
  export declare const IOS_NATIVE_PREPARE: string;
13
13
  /** Local build command (run in the project root) when `build.<platform>` is not configured. */
14
14
  export declare function defaultBuildCommand(platform: Platform, configuration: BuildConfiguration, framework?: AppFramework): string;
15
+ /**
16
+ * Android build: prebuild a CNG checkout that has no `android/`, then Gradle alone. Never `expo run:android`: it has no
17
+ * build-only mode (its `--no-install` skips dependency install, not the app's) and installs the APK on whichever
18
+ * emulator is booted, warden's or not. warden installs the cached build itself.
19
+ */
20
+ export declare function androidGradleCommand(configuration: BuildConfiguration, framework?: AppFramework): string;
15
21
  /** What an iOS device build is signed with: the team (always) and, when set, the bundle id that replaces the project's. */
16
22
  export type IosDeviceBuild = {
17
23
  team: string;
@@ -55,6 +61,8 @@ export declare const EAS_TIMEOUT_MS: number;
55
61
  export declare const EAS_TRIGGER_GRACE_MS: number;
56
62
  /** Local native build timeout. */
57
63
  export declare const LOCAL_BUILD_TIMEOUT_MS: number;
64
+ /** How often a local build nobody watches says it is still running (`still building ios (6m 0s): …`). */
65
+ export declare const BUILD_HEARTBEAT_MS = 60000;
58
66
  /** Fingerprint command timeout. */
59
67
  export declare const FINGERPRINT_TIMEOUT_MS: number;
60
68
  /** Build-lock lease ttl; the lease also carries the holder's pid so it lives as long as the process. */