@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
@@ -358,6 +358,7 @@ export declare const wardenConfigSchema: z.ZodObject<{
358
358
  except: z.ZodOptional<z.ZodArray<z.ZodString>>;
359
359
  keep: z.ZodOptional<z.ZodArray<z.ZodString>>;
360
360
  }, z.core.$strict>]>>;
361
+ golden: z.ZodOptional<z.ZodBoolean>;
361
362
  biometrics: z.ZodOptional<z.ZodBoolean>;
362
363
  reclaim: z.ZodOptional<z.ZodEnum<{
363
364
  none: "none";
@@ -453,6 +454,10 @@ export declare const wardenConfigSchema: z.ZodObject<{
453
454
  }>>;
454
455
  reduceMotion: z.ZodOptional<z.ZodBoolean>;
455
456
  }, z.core.$strict>>;
457
+ keyboard: z.ZodOptional<z.ZodEnum<{
458
+ software: "software";
459
+ hardware: "hardware";
460
+ }>>;
456
461
  }, z.core.$strict>>;
457
462
  a11y: z.ZodOptional<z.ZodObject<{
458
463
  ignore: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -606,6 +611,10 @@ export declare const wardenConfigSchema: z.ZodObject<{
606
611
  }>>;
607
612
  reduceMotion: z.ZodOptional<z.ZodBoolean>;
608
613
  }, z.core.$strict>>;
614
+ keyboard: z.ZodOptional<z.ZodEnum<{
615
+ software: "software";
616
+ hardware: "hardware";
617
+ }>>;
609
618
  }, z.core.$strict>>;
610
619
  a11y: z.ZodOptional<z.ZodObject<{
611
620
  ignore: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -579,7 +579,7 @@ export interface WardenE2eSuite {
579
579
  * `"idle"`: leave them booted, so the next run skips the 10-30 s boot, and let `warden gc` shut
580
580
  * them down once they have sat idle. A failed run always leaves them up. `--no-shutdown` wins.
581
581
  *
582
- * @default "always"
582
+ * @default "always" locally, "idle" under `CI` / `GITHUB_ACTIONS` (the runner is thrown away)
583
583
  */
584
584
  shutdown?: "always" | "idle" | undefined;
585
585
  /**
@@ -734,6 +734,14 @@ export interface WardenE2eSuite {
734
734
  * `biometrics`. Default false.
735
735
  */
736
736
  biometrics?: boolean | undefined;
737
+ /**
738
+ * iOS: simulators the run creates are cloned from a golden image (a first-booted, settled simulator built once
739
+ * per profile and runtime). Off: each is a fresh `simctl create` and first boot, and `warden ci cache --suite`
740
+ * prints no golden group. Existing pool devices are reused either way; `WARDEN_GOLDEN=0` turns it off everywhere.
741
+ *
742
+ * @default false
743
+ */
744
+ golden?: boolean | undefined;
737
745
  /**
738
746
  * What happens to the state of the devices of this run when they are released: `"erase"` (iOS: shutdown +
739
747
  * `simctl erase`; Android: uninstalls the third-party apps), `"uninstall"` (removes the user-installed apps) or
@@ -1,6 +1,6 @@
1
1
  import { type Result } from "../../../types/src/result";
2
2
  import { z } from "zod";
3
- import { type A11yCondition, type ClockCondition, type LocationCondition, type NetworkCondition, type NetworkPreset, type PermissionName, type PermissionState } from "./conditions.types";
3
+ import { type A11yCondition, type ClockCondition, type KeyboardCondition, type LocationCondition, type NetworkCondition, type NetworkPreset, type PermissionName, type PermissionState } from "./conditions.types";
4
4
  /** `network: { latencyMs, ... }`: the same fields as a `custom` network condition, without its `kind`. */
5
5
  declare const customNetworkSchema: z.ZodObject<{
6
6
  latencyMs: z.ZodOptional<z.ZodNumber>;
@@ -91,6 +91,10 @@ export declare const conditionsConfigSchema: z.ZodObject<{
91
91
  }>>;
92
92
  reduceMotion: z.ZodOptional<z.ZodBoolean>;
93
93
  }, z.core.$strict>>;
94
+ keyboard: z.ZodOptional<z.ZodEnum<{
95
+ software: "software";
96
+ hardware: "hardware";
97
+ }>>;
94
98
  }, z.core.$strict>;
95
99
  export type ConditionsConfig = z.input<typeof conditionsConfigSchema>;
96
100
  /**
@@ -129,6 +133,7 @@ export type SuiteConditions = {
129
133
  locale?: string;
130
134
  timezone?: string;
131
135
  a11y?: A11yCondition;
136
+ keyboard?: KeyboardCondition;
132
137
  };
133
138
  /** A config as conditions: shaped values, with a text size resolved. */
134
139
  export declare function normalizeConditions(config: ConditionsConfig): Result<{
@@ -127,6 +127,26 @@ export type BatteryCondition = {
127
127
  charging?: boolean;
128
128
  cosmetic?: boolean;
129
129
  };
130
+ export type KeyboardMode = "software" | "hardware";
131
+ export declare const KEYBOARD_MODES: readonly KeyboardMode[];
132
+ /** What every warden-leased device starts on and what release restores: the on-screen keyboard shows. */
133
+ export declare const DEFAULT_KEYBOARD_MODE: KeyboardMode;
134
+ /** `software`: the on-screen keyboard shows; `hardware`: it is hidden, input arrives as from a connected keyboard. */
135
+ export type KeyboardCondition = {
136
+ mode: KeyboardMode;
137
+ previousIme?: ImeState;
138
+ };
139
+ /** Global settings holding the IME state `hardware` mode switched off (survives the driver process). */
140
+ export declare const IME_STASH_ENABLED = "warden_ime_enabled";
141
+ export declare const IME_STASH_DEFAULT = "warden_ime_default";
142
+ /**
143
+ * Android only: the input methods `hardware` mode switched off, recorded so release puts them back exactly.
144
+ * `enabled` is `settings secure enabled_input_methods` and `default` is `default_input_method` (empty = none).
145
+ */
146
+ export type ImeState = {
147
+ enabled: string;
148
+ default: string;
149
+ };
130
150
  export type CameraFacing = "back" | "front";
131
151
  export declare const CAMERA_FACINGS: readonly CameraFacing[];
132
152
  /** A staged asset the simulated camera plays; `path` is the staged copy under the device's camera dir. */
@@ -150,6 +170,7 @@ export type ConditionValues = {
150
170
  a11y: A11yCondition;
151
171
  battery: BatteryCondition;
152
172
  camera: CameraCondition;
173
+ keyboard: KeyboardCondition;
153
174
  };
154
175
  export type ConditionArea = keyof ConditionValues;
155
176
  /** Display/iteration order; also the order undo resets areas in. */
@@ -1,3 +1,4 @@
1
+ import { type ConditionArea } from "./conditions.types";
1
2
  /**
2
3
  * The state file an iOS app launched with `libwarden-inject.dylib` watches (offline flag, wall-clock offset /
3
4
  * freeze). The e2e driver owns the full format (`packages/e2e/src/driver/ios/inject.ts`); core only knows where it
@@ -7,7 +8,7 @@
7
8
  export declare function injectStatePath(wardenHome: string, udid: string): string;
8
9
  /**
9
10
  * Write `{ seq: previous + 1, offline: false, clockOffsetMs: 0, camera: {} }` (no freeze, no camera feeds) when a state file exists; the dylib
10
- * applies any higher `seq`. No file = no injected app was ever launched on the device: nothing to do.
11
+ * applies any higher `seq`. `keyboard` stays what the file had, except when `area` is `keyboard`: then it goes back to the default. No file = no injected app was ever launched on the device: nothing to do.
11
12
  * Returns an error message, or undefined.
12
13
  */
13
- export declare function neutralizeInjectState(path: string): string | undefined;
14
+ export declare function neutralizeInjectState(path: string, area?: ConditionArea): string | undefined;
@@ -13,6 +13,8 @@ export type A11yTarget = {
13
13
  }): Promise<Result<AnnouncementMatch, DriverError>>;
14
14
  /** the current screen as text with the `--a11y` columns (failure evidence); undefined when it cannot be read */
15
15
  a11yScreen(): Promise<string | undefined>;
16
+ /** whether the on-screen keyboard is up right now (one fresh read) */
17
+ keyboardVisible(): Promise<boolean>;
16
18
  /** the assertion timeout in ms */
17
19
  assertionTimeout(): number;
18
20
  /** one reported step */
@@ -1,4 +1,4 @@
1
- import type { A11yCondition, BatteryCondition, CameraCondition, ClockCondition, HostFault, LocaleCondition, LocationCondition, NetworkCondition, PermissionCondition } from "../../../core/src/conditions/conditions.types";
1
+ import type { A11yCondition, BatteryCondition, CameraCondition, ClockCondition, HostFault, KeyboardCondition, LocaleCondition, LocationCondition, NetworkCondition, PermissionCondition } from "../../../core/src/conditions/conditions.types";
2
2
  /**
3
3
  * One applied condition transition, the evidence line that lets a failure show which network/location was
4
4
  * active. Written to `results.jsonl` beside the test results (`readResults` skips lines that are not a
@@ -39,6 +39,11 @@ export type ConditionEvent = {
39
39
  area: "battery";
40
40
  value: BatteryCondition;
41
41
  at: number;
42
+ } | {
43
+ type: "condition";
44
+ area: "keyboard";
45
+ value: KeyboardCondition;
46
+ at: number;
42
47
  } | {
43
48
  type: "condition";
44
49
  area: "camera";
@@ -1,6 +1,6 @@
1
1
  import { type NetworkInput } from "../../../core/src/conditions/conditions.config";
2
- import type { CameraFacing, ClockCondition, HostFault, LatLon, LocationCondition, LocationScenario, NetworkCondition } from "../../../core/src/conditions/conditions.types";
3
- import type { A11yOp, A11yState, BatteryOp, BatteryState, CameraState, Driver, LocaleState, NetworkState, PermissionGrants, PermissionsState, PushPayload, SettingOptions, TimezoneState } from "../driver/driver.types";
2
+ import { type CameraFacing, type ClockCondition, type HostFault, type KeyboardMode, type LatLon, type LocationCondition, type LocationScenario, type NetworkCondition } from "../../../core/src/conditions/conditions.types";
3
+ import type { A11yOp, A11yState, BatteryOp, BatteryState, CameraState, Driver, KeyboardResult, LocaleState, NetworkState, PermissionGrants, PermissionsState, PushPayload, SettingOptions, TimezoneState } from "../driver/driver.types";
4
4
  import type { E2eEnv } from "../env";
5
5
  import { type ConditionSink } from "./condition-events";
6
6
  import type { ConditionRecorder } from "./lease-recorder";
@@ -160,6 +160,12 @@ export type DeviceConditions = {
160
160
  a11y(op: A11yOp | null): Promise<A11yState>;
161
161
  /** Battery level and charging. Android emulators: real. iOS: the status bar only (`cosmetic: true`). `null` resets. */
162
162
  battery(op: BatteryOp | null): Promise<BatteryState>;
163
+ /**
164
+ * The on-screen keyboard: `software` (the default) keeps it up while the spec types, `hardware` hides it (input as from
165
+ * a connected keyboard), `toggle` flips the mode this spec has in place. Live, no relaunch. Put back to the suite's
166
+ * `conditions.keyboard` (else `software`) when the test ends.
167
+ */
168
+ keyboard(mode: KeyboardMode | "toggle"): Promise<KeyboardResult>;
163
169
  /** Android: a critical trim-memory signal to the running app. iOS: `E_UNSUPPORTED` (no delivery mechanism works). */
164
170
  memoryWarning(options?: SettingOptions): Promise<void>;
165
171
  /** iOS: delivers an APNs payload (`{ aps: { alert: ... } }`) to the app. Android: `E_UNSUPPORTED`. */
@@ -81,8 +81,8 @@ export type Device = DeviceA11y & {
81
81
  clipboard(): Promise<string>;
82
82
  setClipboard(text: string): Promise<void>;
83
83
  /**
84
- * Dismisses the soft keyboard when the snapshot shows one, else does nothing. iOS presses Enter
85
- * (which may submit the focused field); Android presses back.
84
+ * Dismisses the soft keyboard when the snapshot shows one, else does nothing. iOS resigns the first responder
85
+ * (an app launched without the injected library gets Enter, which may submit the field); Android presses back.
86
86
  */
87
87
  hideKeyboard(): Promise<void>;
88
88
  /** Saves `<attemptDir>/screenshots/NNN-<name>.png` and returns the path. */
@@ -125,6 +125,14 @@ export type Device = DeviceA11y & {
125
125
  a11y: DeviceConditions["a11y"];
126
126
  /** Battery level and charging: real on Android emulators, status bar only on iOS (`cosmetic: true`). */
127
127
  battery: DeviceConditions["battery"];
128
+ /**
129
+ * The on-screen keyboard mode: `"software"` (the default: it stays up while the spec types), `"hardware"` (hidden,
130
+ * as with a connected keyboard) or `"toggle"`. Live. Put back to the suite's `conditions.keyboard` when the test
131
+ * ends. Physical iPhones: `E_UNSUPPORTED`.
132
+ */
133
+ keyboard: DeviceConditions["keyboard"];
134
+ /** Whether the on-screen keyboard is up now, read from the accessibility tree. `expect(device).toHaveKeyboard()` polls it. */
135
+ keyboardVisible(): Promise<boolean>;
128
136
  /**
129
137
  * Simulated camera: `set("fixtures/qr.png", { facing })`, `clear`, `status`. The asset (png, jpg, heic, mp4, mov; path relative
130
138
  * to the spec file) plays in the app's real camera. iOS: live in an app launched with the camera, else from the next launch.
@@ -1,4 +1,4 @@
1
- import type { PermissionName, PermissionState } from "../../../../core/src/conditions/conditions.types";
1
+ import { type KeyboardMode, type PermissionName, type PermissionState } from "../../../../core/src/conditions/conditions.types";
2
2
  import type { Driver } from "../driver.types";
3
3
  import type { AdbSession } from "./adb-session";
4
4
  import { type TalkBackControl } from "./android-talkback";
@@ -35,7 +35,14 @@ export type AndroidSettingsDeps = {
35
35
  /** is `warden-uiad` how this driver reads the screen (TalkBack mode needs it: the dump fallback unbinds TalkBack); default false */
36
36
  uiadAvailable?: () => boolean;
37
37
  };
38
- export type AndroidSettings = Pick<Driver, "permissions" | "timezone" | "locale" | "a11y" | "battery" | "memoryWarning" | "push">;
38
+ export type AndroidSettings = Pick<Driver, "permissions" | "timezone" | "locale" | "a11y" | "battery" | "memoryWarning" | "push" | "keyboard" | "keyboardState">;
39
+ /**
40
+ * Whether the IME window is up: `dumpsys window InputMethod` says `isVisible=true` while it is (the uiautomator dump
41
+ * carries the app's window only, so the keyboard is never in the snapshot on Android).
42
+ */
43
+ export declare function imeVisibleIn(dump: string): boolean;
39
44
  export declare function createAndroidSettings(deps: AndroidSettingsDeps): AndroidSettings & {
40
45
  talkback: TalkBackControl;
46
+ keyboardMode(): Promise<KeyboardMode>;
47
+ keyboardVisible(): Promise<boolean>;
41
48
  };
@@ -0,0 +1,38 @@
1
+ import type { Driver, DriverResult } from "./driver.types";
2
+ /**
3
+ * The iOS helper's check for a wedged accessibility service (`warden-simd` `AXWedgeRecovery`): when both AX readers
4
+ * stay silent and the sim log names the wedge, it restarts backboardd (which quits the foreground app), ends its stale
5
+ * guests and reads on. `last` is the latest check; `agoMs` counts from its end.
6
+ */
7
+ export type AxWedgeState = {
8
+ restarts: number;
9
+ maxRestarts: number;
10
+ recovering: boolean;
11
+ /** `recover: true` only: what the check that ask ran (or joined) found; absent when none ran (budget spent) */
12
+ checked?: "restarted" | "not-wedged" | "failed";
13
+ last?: {
14
+ outcome: "restarted";
15
+ agoMs: number;
16
+ ms?: number;
17
+ } | {
18
+ outcome: "not-wedged";
19
+ agoMs: number;
20
+ } | {
21
+ outcome: "failed";
22
+ agoMs: number;
23
+ error: string;
24
+ };
25
+ };
26
+ /**
27
+ * Optional capability next to the frozen `Driver` (iOS simulators): read the helper's wedge state, or with
28
+ * `recover: true` run the check now and answer once it is over. A helper built before it answers `E_UNSUPPORTED`
29
+ * `detail.reason: "helper_outdated"`.
30
+ */
31
+ export type RecoversAxWedge = {
32
+ axWedge(opts?: {
33
+ recover?: boolean;
34
+ }): DriverResult<AxWedgeState>;
35
+ };
36
+ export declare function canRecoverAxWedge(driver: Driver): driver is Driver & RecoversAxWedge;
37
+ /** Whether the helper restarted backboardd within the last `windowMs` (e.g. during the test that just failed). */
38
+ export declare function restartedWithin(state: AxWedgeState, windowMs: number): boolean;
@@ -15,6 +15,8 @@ export declare const bridgeStallSchema: z.ZodObject<{
15
15
  probes: z.ZodNumber;
16
16
  recycles: z.ZodOptional<z.ZodNumber>;
17
17
  recyclesExhausted: z.ZodOptional<z.ZodBoolean>;
18
+ axWedge: z.ZodOptional<z.ZodString>;
19
+ axWedgeRestarts: z.ZodOptional<z.ZodNumber>;
18
20
  }, z.core.$loose>;
19
21
  export type BridgeStall = {
20
22
  stage: string;
@@ -26,6 +28,8 @@ export type BridgeStall = {
26
28
  probes: number;
27
29
  recycles?: number;
28
30
  recyclesExhausted?: boolean;
31
+ axWedge?: string;
32
+ axWedgeRestarts?: number;
29
33
  };
30
34
  /** Only the fields of the schema, so a newer helper's extra keys stay off the span and out of the results. */
31
35
  export declare function toBridgeStall(wire: z.infer<typeof bridgeStallSchema>): BridgeStall;
@@ -1,4 +1,4 @@
1
- import type { CameraCondition, ClockCondition, HostFault, LocationCondition, NetworkCondition, PermissionName, PermissionState, TextSize } from "../../../core/src/conditions/conditions.types";
1
+ import type { CameraCondition, ClockCondition, HostFault, ImeState, KeyboardMode, LocationCondition, NetworkCondition, PermissionName, PermissionState, TextSize } from "../../../core/src/conditions/conditions.types";
2
2
  import type { AsyncResult } from "../../../types/src/result";
3
3
  import type { ScreenReaderState } from "../a11y/a11y.types";
4
4
  import type { BridgeStall } from "./bridge-stall";
@@ -344,6 +344,21 @@ export interface Driver {
344
344
  * OS and the app. iOS: status bar override only (`cosmetic: true`): the app still reads the real battery.
345
345
  */
346
346
  battery(op: BatteryOp | null): DriverResult<BatteryState>;
347
+ /**
348
+ * The on-screen keyboard mode (KBD-2): `software` keeps it up while warden types, `hardware` hides it (input as from
349
+ * a connected keyboard). Live, no relaunch. iOS simulator: the injected dylib (`appliesOnLaunch` when the running
350
+ * app was launched without it). Android: disables the soft IMEs (stashed on the device, restored by `software` and on release). Physical iOS:
351
+ * `E_UNSUPPORTED`.
352
+ */
353
+ keyboard(mode: KeyboardMode): DriverResult<KeyboardResult>;
354
+ /** The mode in force and whether a keyboard is on screen now (a `keyboard` element in the accessibility tree). */
355
+ keyboardState(): DriverResult<KeyboardState>;
356
+ /**
357
+ * Takes the on-screen keyboard down without a keystroke that could submit a form. iOS simulator: the injected
358
+ * dylib resigns the first responder (`via: "resign"`); an app launched without it falls back to Enter (`via:
359
+ * "key"`, with a warning in the log). Android: back. Physical iPhone: Enter.
360
+ */
361
+ dismissKeyboard(): DriverResult<DismissKeyboardResult>;
347
362
  /** Android: `am send-trim-memory RUNNING_CRITICAL` to the running app. iOS: `E_UNSUPPORTED` (no way verified to reach an app). */
348
363
  memoryWarning(opts?: SettingOptions): DriverResult<void>;
349
364
  /** iOS: `simctl push` of an APNs payload (needs `aps`) to the app. Android: `E_UNSUPPORTED`. */
@@ -443,6 +458,20 @@ export type A11yState = {
443
458
  enabled: string | null;
444
459
  };
445
460
  };
461
+ /** `previousIme` (Android `hardware`): the input methods switched off, for the lease record. */
462
+ export type KeyboardResult = {
463
+ mode: KeyboardMode;
464
+ appliesOnLaunch?: boolean;
465
+ previousIme?: ImeState;
466
+ };
467
+ /** `via`: how the keyboard was dismissed; `key` (Enter) can submit the focused field. */
468
+ export type DismissKeyboardResult = {
469
+ via: "resign" | "key" | "back";
470
+ };
471
+ export type KeyboardState = {
472
+ mode: KeyboardMode;
473
+ visible: boolean;
474
+ };
446
475
  export type BatteryOp = {
447
476
  level?: number;
448
477
  charging?: boolean;
@@ -1,4 +1,4 @@
1
- import type { CameraCondition } from "../../../../core/src/conditions/conditions.types";
1
+ import { type CameraCondition, type KeyboardMode } from "../../../../core/src/conditions/conditions.types";
2
2
  import type { Result } from "../../../../types/src/result";
3
3
  import type { DriverError } from "../driver.types";
4
4
  import type { ProxyAddress } from "../network-proxy";
@@ -25,6 +25,17 @@ export type InjectState = {
25
25
  /** one-shot request (A11Y-1): walk the accessibility tree and write `<state>.a11y` before acknowledging this `seq` */
26
26
  a11yRead?: boolean;
27
27
  camera?: CameraCondition;
28
+ /**
29
+ * The on-screen keyboard (KBD-2): `software` keeps it up while warden types, `hardware` lets iOS minimise it. Unlike
30
+ * the other fields a message that leaves it out keeps what the file had (see `createInjectChannel`), so a network or
31
+ * a11y write never undoes it.
32
+ */
33
+ keyboard?: KeyboardMode;
34
+ /**
35
+ * one-shot request: resign the first responder (the keyboard goes away without a Return that could submit the form)
36
+ * before acknowledging this `seq`. Written, never read back, so a later write cannot repeat it.
37
+ */
38
+ dismissKeyboard?: boolean;
28
39
  };
29
40
  /** What the dylib answers in `<state>.ack` once it applied `seq` (`bundleId`: the app it loaded into; `camera`: launched with `WARDEN_CAMERA=1`). */
30
41
  export type InjectAck = {
@@ -56,6 +67,8 @@ export declare function injectLaunchEnv(input: {
56
67
  clockFrozenMs?: number;
57
68
  /** fake the capture stack from the state file's `camera` feeds (`WARDEN_CAMERA=1`) */
58
69
  camera?: boolean;
70
+ /** the keyboard mode the app starts in (`WARDEN_KEYBOARD`); the state file's `keyboard` overrides it live */
71
+ keyboard?: KeyboardMode;
59
72
  debug?: boolean;
60
73
  }): Record<string, string>;
61
74
  /**
@@ -69,6 +82,8 @@ export declare function shouldInject(input: {
69
82
  clockActive: boolean;
70
83
  /** the lease has a camera feed: the dylib must be loaded at launch to fake the capture stack */
71
84
  cameraActive?: boolean;
85
+ /** the lease's keyboard mode is `software` (the default): the dylib keeps the on-screen keyboard up while warden types */
86
+ keyboardActive?: boolean;
72
87
  /** the caller asked for injection (`LaunchOptions.inject`, a suite that declares `conditions.network`) */
73
88
  requested: boolean;
74
89
  }): boolean;
@@ -111,7 +126,7 @@ export type InjectChannel = {
111
126
  attached(): InjectAck | undefined;
112
127
  /** Wait until the app acknowledged `seq`; `false` on timeout. */
113
128
  acked(seq: number, timeoutMs: number): Promise<boolean>;
114
- /** Forget the state and ack of a previous app (before a launch). */
129
+ /** Forget the state and ack of a previous app (before a launch); the keyboard mode is the lease's, so it stays. */
115
130
  reset(): void;
116
131
  };
117
132
  export declare function createInjectChannel(input: {
@@ -3,6 +3,7 @@ import type { E2eEnv } from "../../env";
3
3
  import type { ReadsA11yEvents } from "../a11y-events";
4
4
  import type { ReadsA11y } from "../a11y-read";
5
5
  import type { ProbesFrontmost, ReadsAlertOn } from "../alerts";
6
+ import type { RecoversAxWedge } from "../ax-wedge";
6
7
  import type { ClockRecorder } from "../clock-recorder";
7
8
  import type { Driver, DriverResult, KeyName, Logger } from "../driver.types";
8
9
  import type { AuditsNatively } from "../native-audit";
@@ -11,6 +12,7 @@ import type { PastesText, SelectsAll } from "../typing";
11
12
  import { type InjectFs } from "./inject";
12
13
  import { type IosA11y } from "./ios-a11y";
13
14
  import { type IosNativeAudit, type IosNativeAuditOptions } from "./ios-audit";
15
+ import { type IosKeyboard } from "./ios-keyboard";
14
16
  import { type IosNetwork } from "./ios-network";
15
17
  import { type SwappableClient } from "./ios-session";
16
18
  import { type IosSettings, type StagedSettings } from "./ios-settings";
@@ -27,7 +29,7 @@ import type { SimdSpawn } from "./simd-transport";
27
29
  */
28
30
  export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir"> & Partial<Pick<E2eEnv, "wardenVersion">>;
29
31
  /** The iOS driver is a `Driver` plus the optional capabilities of `driver/alerts.ts`. */
30
- export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost & ReadsA11y & ReadsA11yEvents & AuditsNatively;
32
+ export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost & RecoversAxWedge & ReadsA11y & ReadsA11yEvents & AuditsNatively;
31
33
  export type OpenIosDriverOptions = {
32
34
  udid: string;
33
35
  env?: IosDriverEnv;
@@ -88,7 +90,8 @@ export type OpenIosDriverOptions = {
88
90
  * How long the first snapshot of a session waits for an in-sim bridge that is up but silent. A freshly cloned
89
91
  * simulator on a loaded host can take longer than one read's budget (axbridge 4 s + AX fallback 5 s) to answer the
90
92
  * first time, and every read after it is fast, so the first one retries while the failure says "not ready yet".
91
- * Once a read or a rebind has succeeded the normal per-call budgets apply.
93
+ * Once a read has succeeded the normal per-call budgets apply. Inside a screen-layer poll (`withReadDeadline`) the
94
+ * wait also ends at that poll's deadline, so a 5 s assertion is not held for the whole budget.
92
95
  */
93
96
  export declare const COLD_BRIDGE_BUDGET_MS = 60000;
94
97
  /**
@@ -122,6 +125,8 @@ export declare function createIosDriver(swap: SwappableClient, simctl: Simctl, o
122
125
  }) => IosSettings;
123
126
  /** screen-reader facts from the injected dylib (`ios-a11y.ts`); absent: `readA11y` answers `E_UNSUPPORTED` */
124
127
  a11y?: () => IosA11y;
128
+ /** the on-screen keyboard mode (`ios-keyboard.ts`); absent: `keyboard` answers `E_UNSUPPORTED` */
129
+ keyboard?: () => IosKeyboard;
125
130
  /** per-app network conditions (`ios-network.ts`); absent: `network` answers `E_UNSUPPORTED` */
126
131
  /** the XCUITest audit (`ios-audit.ts`); absent: `auditNatively` skips with a reason */
127
132
  nativeAudit?: (ctx: {
@@ -0,0 +1,39 @@
1
+ import { type KeyboardMode } from "../../../../core/src/conditions/conditions.types";
2
+ import type { Result } from "../../../../types/src/result";
3
+ import type { Driver, DriverError, DriverResult } from "../driver.types";
4
+ import { type InjectChannel } from "./inject";
5
+ /**
6
+ * The on-screen keyboard of an iOS simulator app (KBD-2). HID typing makes iOS treat a hardware keyboard as attached
7
+ * and minimise the software keyboard; the injected dylib answers `-[UIKeyboardImpl isMinimized]` with NO while the mode
8
+ * is `software`, and re-lays the input views on every change. The mode lives in the state file's `keyboard` field (the
9
+ * lease's, so it survives an app relaunch) and in `WARDEN_KEYBOARD` at launch.
10
+ */
11
+ export type IosKeyboardDeps = {
12
+ channel: InjectChannel;
13
+ /** where `libwarden-inject.dylib` is */
14
+ dylib: () => Result<string, DriverError>;
15
+ debug?: boolean;
16
+ };
17
+ export type IosKeyboard = {
18
+ /** the mode in force: the state file's, else the default */
19
+ mode(): KeyboardMode;
20
+ /**
21
+ * Env that loads the dylib with the keyboard mode, for a launch no other layer already injects (`undefined`: those
22
+ * layers carry the dylib; `WARDEN_KEYBOARD` goes through `launchMode`). A dylib that cannot be found is no launch
23
+ * failure: the app runs plain, with the keyboard as iOS decides.
24
+ */
25
+ launchEnv(): Record<string, string> | undefined;
26
+ /** `WARDEN_KEYBOARD` to merge into the env of a launch that carries the dylib already */
27
+ launchMode(): Record<string, string>;
28
+ /** after a launch that carried the dylib on its own: wait until it loaded */
29
+ confirmLoaded(): DriverResult<void>;
30
+ keyboard: Driver["keyboard"];
31
+ /**
32
+ * Resign the first responder in the app (the dylib, on the main thread), acknowledged through the channel.
33
+ * `attached: false`: no app with the dylib is running, nothing was written and the caller falls back.
34
+ */
35
+ dismiss(): DriverResult<{
36
+ attached: boolean;
37
+ }>;
38
+ };
39
+ export declare function createIosKeyboard(deps: IosKeyboardDeps): IosKeyboard;
@@ -196,6 +196,28 @@ export declare const healthDataSchema: z.ZodObject<{
196
196
  count: z.ZodNumber;
197
197
  exhausted: z.ZodBoolean;
198
198
  }, z.core.$strip>>;
199
+ axWedge: z.ZodOptional<z.ZodLazy<z.ZodObject<{
200
+ restarts: z.ZodNumber;
201
+ maxRestarts: z.ZodNumber;
202
+ recovering: z.ZodBoolean;
203
+ checked: z.ZodOptional<z.ZodEnum<{
204
+ failed: "failed";
205
+ restarted: "restarted";
206
+ "not-wedged": "not-wedged";
207
+ }>>;
208
+ last: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
209
+ outcome: z.ZodLiteral<"restarted">;
210
+ agoMs: z.ZodNumber;
211
+ ms: z.ZodOptional<z.ZodNumber>;
212
+ }, z.core.$loose>, z.ZodObject<{
213
+ outcome: z.ZodLiteral<"not-wedged">;
214
+ agoMs: z.ZodNumber;
215
+ }, z.core.$loose>, z.ZodObject<{
216
+ outcome: z.ZodLiteral<"failed">;
217
+ agoMs: z.ZodNumber;
218
+ error: z.ZodString;
219
+ }, z.core.$loose>], "outcome">>;
220
+ }, z.core.$loose>>>;
199
221
  state: z.ZodOptional<z.ZodString>;
200
222
  uptimeMs: z.ZodOptional<z.ZodNumber>;
201
223
  }, z.core.$loose>;
@@ -228,6 +250,8 @@ export declare const snapshotDataSchema: z.ZodObject<{
228
250
  probes: z.ZodNumber;
229
251
  recycles: z.ZodOptional<z.ZodNumber>;
230
252
  recyclesExhausted: z.ZodOptional<z.ZodBoolean>;
253
+ axWedge: z.ZodOptional<z.ZodString>;
254
+ axWedgeRestarts: z.ZodOptional<z.ZodNumber>;
231
255
  }, z.core.$loose>>;
232
256
  }, z.core.$strip>>;
233
257
  target: z.ZodOptional<z.ZodObject<{
@@ -339,6 +363,34 @@ export declare const orientationDataSchema: z.ZodObject<{
339
363
  }>>;
340
364
  }, z.core.$strip>;
341
365
  export type OrientationData = z.infer<typeof orientationDataSchema>;
366
+ /**
367
+ * `axWedge{recover?}`: the helper's check for a wedged accessibility service (both readers silent + the marker in the
368
+ * sim log → backboardd restarted). `recover: true` runs the check now (or joins the one in flight) and answers when it
369
+ * is over; nothing runs once `restarts` reached `maxRestarts` (`WARDEN_SIMD_AX_WEDGE_RESTARTS`).
370
+ */
371
+ export declare const axWedgeDataSchema: z.ZodObject<{
372
+ restarts: z.ZodNumber;
373
+ maxRestarts: z.ZodNumber;
374
+ recovering: z.ZodBoolean;
375
+ checked: z.ZodOptional<z.ZodEnum<{
376
+ failed: "failed";
377
+ restarted: "restarted";
378
+ "not-wedged": "not-wedged";
379
+ }>>;
380
+ last: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
381
+ outcome: z.ZodLiteral<"restarted">;
382
+ agoMs: z.ZodNumber;
383
+ ms: z.ZodOptional<z.ZodNumber>;
384
+ }, z.core.$loose>, z.ZodObject<{
385
+ outcome: z.ZodLiteral<"not-wedged">;
386
+ agoMs: z.ZodNumber;
387
+ }, z.core.$loose>, z.ZodObject<{
388
+ outcome: z.ZodLiteral<"failed">;
389
+ agoMs: z.ZodNumber;
390
+ error: z.ZodString;
391
+ }, z.core.$loose>], "outcome">>;
392
+ }, z.core.$loose>;
393
+ export type AxWedgeData = z.infer<typeof axWedgeDataSchema>;
342
394
  /** Ops whose `data` the client validates and types; every other op's data stays `unknown`. */
343
395
  export declare const opSchemas: {
344
396
  readonly health: z.ZodObject<{
@@ -381,6 +433,28 @@ export declare const opSchemas: {
381
433
  count: z.ZodNumber;
382
434
  exhausted: z.ZodBoolean;
383
435
  }, z.core.$strip>>;
436
+ axWedge: z.ZodOptional<z.ZodLazy<z.ZodObject<{
437
+ restarts: z.ZodNumber;
438
+ maxRestarts: z.ZodNumber;
439
+ recovering: z.ZodBoolean;
440
+ checked: z.ZodOptional<z.ZodEnum<{
441
+ failed: "failed";
442
+ restarted: "restarted";
443
+ "not-wedged": "not-wedged";
444
+ }>>;
445
+ last: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
446
+ outcome: z.ZodLiteral<"restarted">;
447
+ agoMs: z.ZodNumber;
448
+ ms: z.ZodOptional<z.ZodNumber>;
449
+ }, z.core.$loose>, z.ZodObject<{
450
+ outcome: z.ZodLiteral<"not-wedged">;
451
+ agoMs: z.ZodNumber;
452
+ }, z.core.$loose>, z.ZodObject<{
453
+ outcome: z.ZodLiteral<"failed">;
454
+ agoMs: z.ZodNumber;
455
+ error: z.ZodString;
456
+ }, z.core.$loose>], "outcome">>;
457
+ }, z.core.$loose>>>;
384
458
  state: z.ZodOptional<z.ZodString>;
385
459
  uptimeMs: z.ZodOptional<z.ZodNumber>;
386
460
  }, z.core.$loose>;
@@ -412,6 +486,8 @@ export declare const opSchemas: {
412
486
  probes: z.ZodNumber;
413
487
  recycles: z.ZodOptional<z.ZodNumber>;
414
488
  recyclesExhausted: z.ZodOptional<z.ZodBoolean>;
489
+ axWedge: z.ZodOptional<z.ZodString>;
490
+ axWedgeRestarts: z.ZodOptional<z.ZodNumber>;
415
491
  }, z.core.$loose>>;
416
492
  }, z.core.$strip>>;
417
493
  target: z.ZodOptional<z.ZodObject<{
@@ -478,6 +554,28 @@ export declare const opSchemas: {
478
554
  readonly terminate: z.ZodObject<{
479
555
  terminated: z.ZodBoolean;
480
556
  }, z.core.$loose>;
557
+ readonly axWedge: z.ZodObject<{
558
+ restarts: z.ZodNumber;
559
+ maxRestarts: z.ZodNumber;
560
+ recovering: z.ZodBoolean;
561
+ checked: z.ZodOptional<z.ZodEnum<{
562
+ failed: "failed";
563
+ restarted: "restarted";
564
+ "not-wedged": "not-wedged";
565
+ }>>;
566
+ last: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
567
+ outcome: z.ZodLiteral<"restarted">;
568
+ agoMs: z.ZodNumber;
569
+ ms: z.ZodOptional<z.ZodNumber>;
570
+ }, z.core.$loose>, z.ZodObject<{
571
+ outcome: z.ZodLiteral<"not-wedged">;
572
+ agoMs: z.ZodNumber;
573
+ }, z.core.$loose>, z.ZodObject<{
574
+ outcome: z.ZodLiteral<"failed">;
575
+ agoMs: z.ZodNumber;
576
+ error: z.ZodString;
577
+ }, z.core.$loose>], "outcome">>;
578
+ }, z.core.$loose>;
481
579
  };
482
580
  export type OpDataMap = {
483
581
  health: HealthData;
@@ -491,6 +589,7 @@ export type OpDataMap = {
491
589
  orientation: OrientationData;
492
590
  launch: LaunchData;
493
591
  terminate: TerminateData;
592
+ axWedge: AxWedgeData;
494
593
  };
495
594
  export type OpData<Op extends string> = Op extends keyof OpDataMap ? OpDataMap[Op] : unknown;
496
595
  export declare function hasOpSchema(op: string): op is keyof typeof opSchemas;
@@ -0,0 +1,9 @@
1
+ import type { Element, Rect, Snapshot } from "./driver.types";
2
+ /**
3
+ * The on-screen keyboard of a snapshot: the largest `keyboard` element with a frame on the screen. A keyboard slid
4
+ * off the bottom (iOS keeps the hidden one in the tree) is not on screen.
5
+ */
6
+ export declare function keyboardElement(snapshot: Snapshot): Element | undefined;
7
+ export declare function keyboardVisible(snapshot: Snapshot): boolean;
8
+ /** The keyboard's frame, or `undefined` when none shows. */
9
+ export declare function keyboardFrame(snapshot: Snapshot): Rect | undefined;
@@ -1,6 +1,6 @@
1
1
  import type { Driver } from "./driver.types";
2
2
  /** The condition and setting ops of a `Driver`: each changes the device (or an app's grants) beyond driving its UI. */
3
- export declare const PHYSICAL_CONDITION_OPS: readonly ["network", "networkFaults", "location", "clock", "permissions", "timezone", "locale", "a11y", "battery", "memoryWarning", "push", "camera"];
3
+ export declare const PHYSICAL_CONDITION_OPS: readonly ["network", "networkFaults", "location", "clock", "permissions", "timezone", "locale", "a11y", "battery", "keyboard", "keyboardState", "memoryWarning", "push", "camera"];
4
4
  type PhysicalConditionOps = Pick<Driver, (typeof PHYSICAL_CONDITION_OPS)[number]>;
5
5
  /**
6
6
  * The condition ops for a physical phone: all `E_UNSUPPORTED`. On a phone they would flip airplane mode (cutting
@@ -0,0 +1,4 @@
1
+ /** Runs `fn` with `remaining` as the deadline of every read made inside it, awaits included. */
2
+ export declare function withReadDeadline<T>(remaining: () => number, fn: () => T): T;
3
+ /** How long the innermost read deadline has left (never negative), or undefined outside one. */
4
+ export declare function readDeadlineRemaining(): number | undefined;