@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
@@ -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. */
@@ -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;
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { type SpanDetail } from "./span-detail";
3
+ /**
4
+ * What one iOS `axbridge` fallback or stall looked like, as `warden-simd` reports it (`fallback.stall` on a snapshot,
5
+ * `detail.stall` on a failed read). Recorded in `trace.jsonl` (the `stall*` span fields), `results.jsonl` (`bridge`)
6
+ * and under the header of `screen-at-failure.txt`.
7
+ */
8
+ export declare const bridgeStallSchema: z.ZodObject<{
9
+ stage: z.ZodString;
10
+ readMs: z.ZodOptional<z.ZodNumber>;
11
+ deadlineMs: z.ZodOptional<z.ZodNumber>;
12
+ guestAgeMs: z.ZodOptional<z.ZodNumber>;
13
+ cold: z.ZodBoolean;
14
+ cooldownRemainingMs: z.ZodOptional<z.ZodNumber>;
15
+ probes: z.ZodNumber;
16
+ recycles: z.ZodOptional<z.ZodNumber>;
17
+ recyclesExhausted: z.ZodOptional<z.ZodBoolean>;
18
+ axWedge: z.ZodOptional<z.ZodString>;
19
+ axWedgeRestarts: z.ZodOptional<z.ZodNumber>;
20
+ }, z.core.$loose>;
21
+ export type BridgeStall = {
22
+ stage: string;
23
+ readMs?: number;
24
+ deadlineMs?: number;
25
+ guestAgeMs?: number;
26
+ cold: boolean;
27
+ cooldownRemainingMs?: number;
28
+ probes: number;
29
+ recycles?: number;
30
+ recyclesExhausted?: boolean;
31
+ axWedge?: string;
32
+ axWedgeRestarts?: number;
33
+ };
34
+ /** Only the fields of the schema, so a newer helper's extra keys stay off the span and out of the results. */
35
+ export declare function toBridgeStall(wire: z.infer<typeof bridgeStallSchema>): BridgeStall;
36
+ /** Leaves the stall on the span timing the read (`stallStage`, `stallReadMs`, …). */
37
+ export declare function noteBridgeStall(stall: BridgeStall): void;
38
+ /** The stall a span recorded, or undefined when it has none. */
39
+ export declare function bridgeStallFromSpan(detail: SpanDetail | undefined): BridgeStall | undefined;
40
+ /** One line for a log or a report: `stage read · read 4012 ms of a 4000 ms deadline · warm · …`. */
41
+ export declare function formatBridgeStall(stall: BridgeStall): string;
@@ -1,6 +1,7 @@
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
+ import type { BridgeStall } from "./bridge-stall";
4
5
  /**
5
6
  * The device driver contract every layer of warden-e2e builds on: the spec API (`screen`, `device`,
6
7
  * `app`), `warden ui`, specs and the devtools facet all talk to a `Driver`. Implementations:
@@ -136,9 +137,12 @@ export type Snapshot = {
136
137
  modal?: ModalInfo;
137
138
  /**
138
139
  * Set when `backend` answered because this reader failed or was skipped first (iOS: `axbridge` stalled, `ax` read in
139
- * its place). The fallback reader can lack what the first one has, so a missing node on such a snapshot is not final.
140
+ * its place). The fallback reader can lack what the first one has, so a missing node on such a snapshot is not final:
141
+ * the iOS helper probes the first reader in the background, and a later read is back on it once it answers.
140
142
  */
141
143
  fellBackFrom?: SnapshotBackend;
144
+ /** iOS: what the bridge failure behind `fellBackFrom` looked like, when the helper reported it */
145
+ bridgeStall?: BridgeStall;
142
146
  };
143
147
  export type Orientation = "portrait" | "portraitUpsideDown" | "landscapeLeft" | "landscapeRight";
144
148
  export type HardwareButton = "home" | "back" | "lock" | "volumeUp" | "volumeDown" | "shake";
@@ -340,6 +344,21 @@ export interface Driver {
340
344
  * OS and the app. iOS: status bar override only (`cosmetic: true`): the app still reads the real battery.
341
345
  */
342
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>;
343
362
  /** Android: `am send-trim-memory RUNNING_CRITICAL` to the running app. iOS: `E_UNSUPPORTED` (no way verified to reach an app). */
344
363
  memoryWarning(opts?: SettingOptions): DriverResult<void>;
345
364
  /** iOS: `simctl push` of an APNs payload (needs `aps`) to the app. Android: `E_UNSUPPORTED`. */
@@ -439,6 +458,20 @@ export type A11yState = {
439
458
  enabled: string | null;
440
459
  };
441
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
+ };
442
475
  export type BatteryOp = {
443
476
  level?: number;
444
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,9 +90,20 @@ 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;
97
+ /**
98
+ * warden-simd's deadline for the first `axbridge` read of a cold guest (helper start, a recycled guest, an app just
99
+ * launched): `BridgeDeadline.defaultColdReadMs`, or `WARDEN_SIMD_BRIDGE_COLD_DEADLINE_MS`. On a 3-vCPU runner that
100
+ * first read takes 4-11 s; warm reads keep the 4 s deadline.
101
+ */
102
+ export declare const BRIDGE_COLD_DEADLINE_MS = 12000;
103
+ /** The request timeout of a read while the bridge is cold, so the client never gives up before the helper's deadline. */
104
+ export declare const COLD_READ_TIMEOUT_MS: number;
105
+ /** {@link COLD_READ_TIMEOUT_MS}, following a `WARDEN_SIMD_BRIDGE_COLD_DEADLINE_MS` the helper also reads. */
106
+ export declare function coldReadTimeoutMs(env?: Readonly<Record<string, string | undefined>>): number;
94
107
  /** USB HID keyboard-page usage ids for the keys `Driver.key` offers. */
95
108
  export declare const KEY_USAGE: Readonly<Record<KeyName, number>>;
96
109
  /** The fields of a failed snapshot's `detail` the helper sets (which reader, why, both readers' errors), on its span. */
@@ -112,6 +125,8 @@ export declare function createIosDriver(swap: SwappableClient, simctl: Simctl, o
112
125
  }) => IosSettings;
113
126
  /** screen-reader facts from the injected dylib (`ios-a11y.ts`); absent: `readA11y` answers `E_UNSUPPORTED` */
114
127
  a11y?: () => IosA11y;
128
+ /** the on-screen keyboard mode (`ios-keyboard.ts`); absent: `keyboard` answers `E_UNSUPPORTED` */
129
+ keyboard?: () => IosKeyboard;
115
130
  /** per-app network conditions (`ios-network.ts`); absent: `network` answers `E_UNSUPPORTED` */
116
131
  /** the XCUITest audit (`ios-audit.ts`); absent: `auditNatively` skips with a reason */
117
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;
@@ -21,6 +21,8 @@ export type SimdEventMap = {
21
21
  };
22
22
  export type RequestOptions = {
23
23
  timeoutMs?: number;
24
+ /** a floor for this request's timeout (a cold bridge read): raises the default or `timeoutMs`, never lowers it */
25
+ minTimeoutMs?: number;
24
26
  };
25
27
  export type SimdClient = {
26
28
  /** the handshake of the current helper process (refreshed after a respawn) */