@delacour/warden-e2e 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/{chunk-yb1hcjqm.mjs → chunk-empqr9n0.mjs} +1364 -53
  2. package/index.mjs +34 -4
  3. package/package.json +2 -2
  4. package/preload.mjs +4008 -335
  5. package/types/core/src/affected/affected.schema.d.ts +93 -0
  6. package/types/core/src/allocate.d.ts +2 -1
  7. package/types/core/src/android-keyguard.d.ts +34 -0
  8. package/types/core/src/batch/preset.schema.d.ts +1 -0
  9. package/types/core/src/biometrics.d.ts +15 -0
  10. package/types/core/src/builds/builds.defaults.d.ts +15 -0
  11. package/types/core/src/builds/config.d.ts +5 -1
  12. package/types/core/src/builds/config.schema.d.ts +182 -0
  13. package/types/core/src/builds/define-config.d.ts +104 -0
  14. package/types/core/src/builds/install-device.d.ts +37 -0
  15. package/types/core/src/builds/install.d.ts +17 -3
  16. package/types/core/src/builds/kinds.d.ts +21 -3
  17. package/types/core/src/builds/lock.d.ts +38 -0
  18. package/types/core/src/builds/resolve.d.ts +12 -2
  19. package/types/core/src/conditions/conditions.config.d.ts +117 -0
  20. package/types/core/src/conditions/conditions.d.ts +22 -0
  21. package/types/core/src/conditions/conditions.types.d.ts +141 -0
  22. package/types/core/src/conditions/inject-state.d.ts +13 -0
  23. package/types/core/src/conditions/launch-settings.d.ts +14 -0
  24. package/types/core/src/conditions/network.presets.d.ts +42 -0
  25. package/types/core/src/conditions/text-size.d.ts +19 -0
  26. package/types/core/src/golden/android-biometrics.d.ts +35 -0
  27. package/types/core/src/ios-runner/classify.d.ts +7 -0
  28. package/types/core/src/ios-runner/runner-build.types.d.ts +76 -0
  29. package/types/core/src/netproxy/control.d.ts +75 -0
  30. package/types/core/src/netproxy/decide.d.ts +22 -0
  31. package/types/core/src/netproxy/handle-file.d.ts +12 -0
  32. package/types/core/src/netproxy/lease-proxy.d.ts +53 -0
  33. package/types/core/src/netproxy/netproxy.main.d.ts +40 -0
  34. package/types/core/src/netproxy/pipe.d.ts +37 -0
  35. package/types/core/src/netproxy/proxy.d.ts +48 -0
  36. package/types/core/src/netproxy/shaping.d.ts +43 -0
  37. package/types/core/src/providers/android.d.ts +36 -1
  38. package/types/core/src/types.d.ts +47 -0
  39. package/types/e2e/src/alert-buttons.d.ts +9 -0
  40. package/types/e2e/src/app.d.ts +3 -1
  41. package/types/e2e/src/conditions/condition-events.d.ts +63 -0
  42. package/types/e2e/src/conditions/device-conditions.d.ts +170 -0
  43. package/types/e2e/src/conditions/geo.d.ts +15 -0
  44. package/types/e2e/src/conditions/gpx.d.ts +8 -0
  45. package/types/e2e/src/conditions/lease-recorder.d.ts +27 -0
  46. package/types/e2e/src/conditions/lease-store.d.ts +9 -0
  47. package/types/e2e/src/conditions/location-plan.d.ts +17 -0
  48. package/types/e2e/src/conditions/location-scenarios.data.d.ts +17 -0
  49. package/types/e2e/src/conditions/route-ticker.d.ts +53 -0
  50. package/types/e2e/src/conditions/scheduler.d.ts +83 -0
  51. package/types/e2e/src/device.d.ts +57 -3
  52. package/types/e2e/src/driver/android/android-app.d.ts +4 -0
  53. package/types/e2e/src/driver/android/android-argv.d.ts +19 -0
  54. package/types/e2e/src/driver/android/android-clock.d.ts +28 -0
  55. package/types/e2e/src/driver/android/android-device.d.ts +8 -0
  56. package/types/e2e/src/driver/android/android-driver.d.ts +17 -0
  57. package/types/e2e/src/driver/android/android-network.d.ts +66 -0
  58. package/types/e2e/src/driver/android/android-phone.d.ts +47 -0
  59. package/types/e2e/src/driver/android/android-settings.d.ts +34 -0
  60. package/types/e2e/src/driver/android/android-state.d.ts +10 -0
  61. package/types/e2e/src/driver/clock-recorder.d.ts +9 -0
  62. package/types/e2e/src/driver/device-settings.d.ts +10 -0
  63. package/types/e2e/src/driver/driver.types.d.ts +176 -1
  64. package/types/e2e/src/driver/ios/device/capabilities.d.ts +3 -0
  65. package/types/e2e/src/driver/ios/device/device-driver.d.ts +49 -0
  66. package/types/e2e/src/driver/ios/device/devicectl.d.ts +38 -0
  67. package/types/e2e/src/driver/ios/device/runner-protocol.d.ts +21 -0
  68. package/types/e2e/src/driver/ios/hid-recovery.d.ts +15 -0
  69. package/types/e2e/src/driver/ios/inject.d.ts +110 -0
  70. package/types/e2e/src/driver/ios/ios-driver.d.ts +29 -0
  71. package/types/e2e/src/driver/ios/ios-network.d.ts +46 -0
  72. package/types/e2e/src/driver/ios/ios-settings.d.ts +44 -0
  73. package/types/e2e/src/driver/ios/simctl.d.ts +41 -1
  74. package/types/e2e/src/driver/ios/type-budget.d.ts +6 -1
  75. package/types/e2e/src/driver/network-proxy.d.ts +52 -0
  76. package/types/e2e/src/driver/open.d.ts +20 -0
  77. package/types/e2e/src/driver/physical-conditions.d.ts +11 -0
  78. package/types/e2e/src/driver/requirements.d.ts +14 -0
  79. package/types/e2e/src/driver/typing.d.ts +1 -1
  80. package/types/e2e/src/env.d.ts +32 -0
  81. package/types/e2e/src/fixture-session.d.ts +4 -0
  82. package/types/e2e/src/fixtures.d.ts +6 -1
  83. package/types/e2e/src/index.d.ts +2 -0
  84. package/types/e2e/src/runner/scope.d.ts +1 -0
  85. package/types/e2e/src/runner/test.d.ts +6 -0
  86. package/types/e2e/src/traced-device.d.ts +2 -1
@@ -0,0 +1,110 @@
1
+ import type { Result } from "../../../../types/src/result";
2
+ import type { DriverError } from "../driver.types";
3
+ import type { ProxyAddress } from "../network-proxy";
4
+ /**
5
+ * The injected dylib for iOS simulator apps (`native/ios-inject`): routes NSURLSession through the lease's shaping
6
+ * proxy and reports offline / a clock offset to the app, live, through a state file the dylib watches. This module is
7
+ * the host side: where the dylib is, the launch env that loads it, and the state file protocol. COND-12 (mock clock)
8
+ * reuses all of it and only sets `clockOffsetMs` (shift) or `clockFrozenMs` (hold).
9
+ */
10
+ export declare const INJECT_DYLIB = "libwarden-inject.dylib";
11
+ export declare const INJECT_DYLIB_ENV = "WARDEN_INJECT_DYLIB";
12
+ /**
13
+ * The one message of the control channel. `seq` rises with every write (the dylib ignores a state whose `seq` it
14
+ * already applied); a field left out means "off" (`offline` false, `clockOffsetMs` 0), so a message is always the
15
+ * whole state, never a delta. `clockFrozenMs` (ms since the epoch) holds the wall clocks at one instant and wins over
16
+ * `clockOffsetMs`; left out, they run.
17
+ */
18
+ export type InjectState = {
19
+ seq: number;
20
+ offline?: boolean;
21
+ clockOffsetMs?: number;
22
+ clockFrozenMs?: number;
23
+ };
24
+ /** What the dylib answers in `<state>.ack` once it applied `seq` (`bundleId`: the app it loaded into). */
25
+ export type InjectAck = {
26
+ seq: number;
27
+ pid: number;
28
+ proxy?: boolean;
29
+ bundleId?: string;
30
+ };
31
+ export declare function encodeInjectState(state: InjectState): string;
32
+ export declare function parseInjectState(text: string): InjectState | undefined;
33
+ export declare function parseInjectAck(text: string): InjectAck | undefined;
34
+ /** One directory per device: the dylib watches its directory, so two devices never wake each other's app. Core owns the path (release resets the file). */
35
+ export declare const injectStatePath: (wardenHome: string, udid: string) => string;
36
+ export declare const injectAckPath: (statePath: string) => string;
37
+ /**
38
+ * Launch env that loads the dylib. `proxy` is fixed for the life of the process (the lease's proxy keeps its port
39
+ * for the whole lease); everything that changes later goes through the state file.
40
+ */
41
+ export declare function injectLaunchEnv(input: {
42
+ dylib: string;
43
+ statePath: string;
44
+ proxy?: ProxyAddress;
45
+ clockOffsetMs?: number;
46
+ clockFrozenMs?: number;
47
+ debug?: boolean;
48
+ }): Record<string, string>;
49
+ /**
50
+ * Whether a launch loads the dylib. Never without a reason: a run with no condition keeps the plain launch (no
51
+ * dyld insertion, no watcher thread, no proxy hop).
52
+ */
53
+ export declare function shouldInject(input: {
54
+ /** the lease has a live shaping proxy, or a non-trivial network condition was set */
55
+ networkActive: boolean;
56
+ /** a clock offset is set (COND-12) */
57
+ clockActive: boolean;
58
+ /** the caller asked for injection (`LaunchOptions.inject`, a suite that declares `conditions.network`) */
59
+ requested: boolean;
60
+ }): boolean;
61
+ export type LocateInjectInput = {
62
+ /** `WARDEN_INJECT_DYLIB` (a file) and `WARDEN_HELPER_DIR` (a directory that also holds the dylib) */
63
+ env: Readonly<Record<string, string | undefined>>;
64
+ execPath: string;
65
+ /** directory of this module (`packages/e2e/src/driver/ios`) */
66
+ importDir: string;
67
+ wardenHome: string;
68
+ version?: string;
69
+ exists: (path: string) => boolean;
70
+ realpath?: (path: string) => string;
71
+ };
72
+ /**
73
+ * Find `libwarden-inject.dylib`. Order mirrors `locateHelper`: `WARDEN_INJECT_DYLIB` → `WARDEN_HELPER_DIR` → next to
74
+ * the running `warden` → `<packages/e2e>/native/dist` → `$WARDEN_HOME/simd/<version>`. A set variable that points at
75
+ * nothing is an error.
76
+ */
77
+ export declare function locateInjectDylib(input: LocateInjectInput): Result<string, DriverError>;
78
+ /** File operations of the state channel, injected. */
79
+ export type InjectFs = {
80
+ mkdir(path: string): void;
81
+ /** write `text` to `path` so a reader sees all of it or none (tmp + rename) */
82
+ writeAtomic(path: string, text: string): void;
83
+ read(path: string): string | undefined;
84
+ remove(path: string): void;
85
+ /** is `pid` a live process */
86
+ pidAlive(pid: number): boolean;
87
+ };
88
+ export declare const nodeInjectFs: InjectFs;
89
+ /** The host end of the control channel for one device. */
90
+ export type InjectChannel = {
91
+ readonly statePath: string;
92
+ /** Last state written (read back from disk, so another warden process's writes count). */
93
+ current(): InjectState;
94
+ /** Write the whole next state (`seq` assigned here). */
95
+ write(next: Omit<InjectState, "seq">): InjectState;
96
+ /** The injected process that answered last, when it is still alive and is the app under test. */
97
+ attached(): InjectAck | undefined;
98
+ /** Wait until the app acknowledged `seq`; `false` on timeout. */
99
+ acked(seq: number, timeoutMs: number): Promise<boolean>;
100
+ /** Forget the state and ack of a previous app (before a launch). */
101
+ reset(): void;
102
+ };
103
+ export declare function createInjectChannel(input: {
104
+ statePath: string;
105
+ fs?: InjectFs;
106
+ now: () => number;
107
+ sleep: (ms: number) => Promise<void>;
108
+ /** the app under test; an ack from another app (another injected bundle still running) is ignored. None = any */
109
+ bundleId?: () => string | undefined;
110
+ }): InjectChannel;
@@ -1,9 +1,14 @@
1
1
  import type { Exec } from "../../../../core/src/exec";
2
2
  import type { E2eEnv } from "../../env";
3
3
  import type { ProbesFrontmost, ReadsAlertOn } from "../alerts";
4
+ import type { ClockRecorder } from "../clock-recorder";
4
5
  import type { Driver, DriverResult, KeyName, Logger } from "../driver.types";
6
+ import type { NetworkProxyPort } from "../network-proxy";
5
7
  import type { PastesText, SelectsAll } from "../typing";
8
+ import { type InjectFs } from "./inject";
9
+ import { type IosNetwork } from "./ios-network";
6
10
  import { type SwappableClient } from "./ios-session";
11
+ import { type IosSettings, type StagedSettings } from "./ios-settings";
7
12
  import { type Simctl } from "./simctl";
8
13
  import { type SimdClient } from "./simd-client";
9
14
  import type { SimdSpawn } from "./simd-transport";
@@ -46,6 +51,19 @@ export type OpenIosDriverOptions = {
46
51
  defaultTimeoutMs?: number;
47
52
  /** log each helper op and simctl call with its duration (`WARDEN_E2E_DEBUG=1`) */
48
53
  debug?: boolean;
54
+ /**
55
+ * The lease's shaping proxy (`leaseNetworkProxy`). Without it `network` / `networkFaults` answer `E_UNSUPPORTED`:
56
+ * a driver opened by a bare caller has no lease to hang a proxy on.
57
+ */
58
+ networkProxy?: NetworkProxyPort;
59
+ /** told about every successful `clock` op, so the lease records the `clock` area (`leaseClockRecorder`) */
60
+ clockRecorder?: ClockRecorder;
61
+ /** the session's app (`WARDEN_BUNDLE_ID`): the default target of permissions, push and locale */
62
+ bundleId?: string;
63
+ /** where a staged timezone / locale is kept between processes (default: `<wardenHome>/launch-settings/<udid>.json`) */
64
+ stagedSettings?: StagedSettings;
65
+ /** state-file operations of the injected dylib's control channel, injected for tests */
66
+ injectFs?: InjectFs;
49
67
  /** CoreSimulator devices directory for the host-side `AutomationEnabled` check (default `~/Library/Developer/CoreSimulator/Devices`) */
50
68
  devicesRoot?: string;
51
69
  };
@@ -56,6 +74,17 @@ export declare function createIosDriver(swap: SwappableClient, simctl: Simctl, o
56
74
  now: () => number;
57
75
  sleep: (ms: number) => Promise<void>;
58
76
  log?: Logger;
77
+ /** told about every successful `clock` op */
78
+ clockRecorder?: ClockRecorder;
79
+ /** device settings (`ios-settings.ts`); absent: those ops answer `E_UNSUPPORTED` */
80
+ settings?: (ctx: {
81
+ lastBundle: () => string | undefined;
82
+ }) => IosSettings;
83
+ /** per-app network conditions (`ios-network.ts`); absent: `network` answers `E_UNSUPPORTED` */
84
+ network?: (deps: {
85
+ lastBundle: () => string | undefined;
86
+ relaunch: (bundleId: string) => DriverResult<void>;
87
+ }) => IosNetwork;
59
88
  }): IosDriver;
60
89
  /**
61
90
  * Open the iOS driver for `udid`. Writes `AutomationEnabled` (best effort) while the helper starts.
@@ -0,0 +1,46 @@
1
+ import type { ClockCondition, HostFault, NetworkCondition } from "../../../../core/src/conditions/conditions.types";
2
+ import type { Result } from "../../../../types/src/result";
3
+ import type { ClockState, DriverError, DriverResult, LaunchOptions, NetworkOptions, NetworkState } from "../driver.types";
4
+ import { type NetworkProxyPort } from "../network-proxy";
5
+ import { type InjectChannel } from "./inject";
6
+ /**
7
+ * iOS simulator network conditions: a simulator shares the host network, so nothing can shape one device. Instead
8
+ * the app is launched with `libwarden-inject.dylib` (see `inject.ts`), which routes its NSURLSession traffic
9
+ * through the lease's shaping proxy; the proxy does latency / bandwidth / loss / faults, and an `offline` flag in
10
+ * the dylib's state file makes the app's own reachability APIs say so. Presets and offline switch live over the
11
+ * proxy's control channel and the state file. Only the first enable needs a launch that carries the dylib.
12
+ */
13
+ /** How long the injected app gets to acknowledge a state write (the dylib applies it on a dispatch source). */
14
+ export declare const INJECT_ACK_MS = 3000;
15
+ /** How long a fresh launch gets to load the dylib and acknowledge its first state. */
16
+ export declare const INJECT_LOAD_MS = 8000;
17
+ export type IosNetworkDeps = {
18
+ proxy: NetworkProxyPort;
19
+ channel: InjectChannel;
20
+ /** the dylib path, resolved on first use so a run without conditions never looks for it */
21
+ dylib: () => Result<string, DriverError>;
22
+ /** is `bundleId` running now */
23
+ isRunning: (bundleId: string) => DriverResult<boolean>;
24
+ /** the last app this driver launched */
25
+ lastBundle: () => string | undefined;
26
+ /** launch `bundleId` again with the dylib (late-bound: the driver's own `app.launch`) */
27
+ relaunch: (bundleId: string) => DriverResult<void>;
28
+ debug?: boolean;
29
+ /** wall time, ms (default `Date.now`); `fixed` clocks are an offset from it */
30
+ now?: () => number;
31
+ };
32
+ export type IosNetwork = {
33
+ network(op: NetworkCondition, opts?: NetworkOptions): DriverResult<NetworkState>;
34
+ networkFaults(faults: readonly HostFault[]): DriverResult<{
35
+ notes: readonly string[];
36
+ }>;
37
+ /** Env to merge into a launch (`undefined`: launch plainly). Stages the state file the dylib reads at load. */
38
+ launchEnv(opts: LaunchOptions): DriverResult<Record<string, string> | undefined>;
39
+ /** After a launch that carried the dylib: wait until it loaded, so a missing injection fails here, not as a test that sees the wrong network. */
40
+ confirmLoaded(): DriverResult<void>;
41
+ /** Wall-clock offset for the injected app (COND-12). Live when the app is injected, else staged for the next launch. */
42
+ setClockOffset(ms: number): DriverResult<void>;
43
+ /** Mock the injected app's wall clock (`Driver.clock`). Live when the app is injected, else staged for the next launch. */
44
+ clock(op: ClockCondition, opts?: NetworkOptions): DriverResult<ClockState>;
45
+ };
46
+ export declare function createIosNetwork(deps: IosNetworkDeps): IosNetwork;
@@ -0,0 +1,44 @@
1
+ import type { PermissionName, PermissionState } from "../../../../core/src/conditions/conditions.types";
2
+ import type { LaunchSettings } from "../../../../core/src/conditions/launch-settings";
3
+ import { type Exec } from "../../../../core/src/exec";
4
+ import type { Driver, LaunchOptions } from "../driver.types";
5
+ /** Permission name to `simctl privacy` service; a name without one is skipped with the reason in `IOS_NO_SERVICE`. */
6
+ export declare const IOS_PRIVACY_SERVICE: Partial<Record<PermissionName, string>>;
7
+ export declare const IOS_NO_SERVICE: Partial<Record<PermissionName, string>>;
8
+ export declare const privacyArgv: (udid: string, state: PermissionState, service: string, bundleId: string) => string[];
9
+ export declare const contentSizeArgv: (udid: string, category: string) => string[];
10
+ export declare const contrastArgv: (udid: string, increased: boolean) => string[];
11
+ export declare const DEFAULT_CONTENT_SIZE = "large";
12
+ export declare function statusBarBatteryArgv(udid: string, op: {
13
+ level?: number;
14
+ charging?: boolean;
15
+ }): string[];
16
+ export declare const statusBarClearArgv: (udid: string) => string[];
17
+ /** `simctl push` reads the payload from stdin (`-`); a `sh -c` with positional args keeps it out of the command line quoting. */
18
+ export declare const pushArgv: (udid: string, bundleId: string, payload: unknown) => string[];
19
+ /** The launch env and args a staged timezone / locale becomes. */
20
+ export declare function launchExtras(settings: LaunchSettings): {
21
+ env: Record<string, string>;
22
+ args: string[];
23
+ };
24
+ /** `requested` with the staged timezone / locale merged in (the caller's own env and args win on a clash). */
25
+ export declare function withStagedSettings(requested: LaunchOptions | undefined, settings: LaunchSettings): LaunchOptions | undefined;
26
+ export type StagedSettings = {
27
+ read(): LaunchSettings;
28
+ write(settings: LaunchSettings): string | undefined;
29
+ };
30
+ export type IosSettingsDeps = {
31
+ udid: string;
32
+ exec: Exec;
33
+ /** the last app this driver launched */
34
+ lastBundle: () => string | undefined;
35
+ /** the session's app (`WARDEN_BUNDLE_ID`) when nothing was launched yet */
36
+ defaultBundle?: string;
37
+ /** where timezone / locale are staged between processes; absent: kept in this driver only */
38
+ staged?: StagedSettings;
39
+ };
40
+ export type IosSettings = Pick<Driver, "permissions" | "timezone" | "locale" | "a11y" | "battery" | "memoryWarning" | "push"> & {
41
+ /** the staged timezone / locale, for `app.launch` to merge into a launch */
42
+ launchSettings(): LaunchSettings;
43
+ };
44
+ export declare function createIosSettings(deps: IosSettingsDeps): IosSettings;
@@ -1,6 +1,7 @@
1
1
  import { type ResetFs } from "../../../../core/src/builds/install";
2
+ import type { LatLon, LocationCondition } from "../../../../core/src/conditions/conditions.types";
2
3
  import { type Exec, type ExecResult } from "../../../../core/src/exec";
3
- import type { DriverError, DriverResult, LaunchOptions } from "../driver.types";
4
+ import type { BiometricOp, BiometricState, DriverError, DriverResult, LaunchOptions } from "../driver.types";
4
5
  export declare const SIMCTL_CHILD_PREFIX = "SIMCTL_CHILD_";
5
6
  /** `xcrun simctl launch [--terminate-running-process] <udid> <bundle> <args…>` (env goes through `SIMCTL_CHILD_*`, see {@link launchEnv}). */
6
7
  export declare function launchArgv(udid: string, bundleId: string, opts?: LaunchOptions): string[];
@@ -16,6 +17,22 @@ export declare const appContainerArgv: (udid: string, bundleId: string) => strin
16
17
  export declare const launchctlListArgv: (udid: string) => string[];
17
18
  export declare const pbpasteArgv: (udid: string) => string[];
18
19
  export declare const appearanceArgv: (udid: string, set?: "light" | "dark") => string[];
20
+ /** Sets the enrollment state, then posts the change (`notifyutil -s <name> <state>` before `-p <name>`). */
21
+ export declare const biometricEnrollArgvs: (udid: string, enrolled: boolean) => string[][];
22
+ /** Prints `com.apple.BiometricKit.enrollmentChanged <0|1>`. */
23
+ export declare const biometricStateArgv: (udid: string) => string[];
24
+ /** The Face ID (`pearl`) or Touch ID (`fingerTouch`) match / nomatch notification the simulator's BiometricKit listens for. */
25
+ export declare const biometricEventArgv: (udid: string, modality: BiometricState["modality"], event: "match" | "nomatch") => string[];
26
+ /** The notifications one op posts, in order; `status` posts nothing. */
27
+ export declare function biometricArgvs(udid: string, modality: BiometricState["modality"], op: BiometricOp): string[][];
28
+ /** Model identifier (`iPhone14,7`) of a booted sim. */
29
+ export declare const modelIdentifierArgv: (udid: string) => string[];
30
+ /**
31
+ * Touch ID models by identifier: iPhone SE and everything up to the 8 (`iPhone10,1|2|4|5`; the X, `10,3|6`, has Face ID),
32
+ * and the iPads with a home or power-button sensor. Face ID iPads are the Pros from 2018 (`iPad8,*`, `iPad13,4-11`,
33
+ * `iPad14,3-6`, `iPad16,3-6`, `iPad17,*`). Unknown or empty falls back to Face ID.
34
+ */
35
+ export declare function biometricModalityOf(model: string): BiometricState["modality"];
19
36
  export declare const screenshotArgv: (udid: string, path: string) => string[];
20
37
  /** `AutomationEnabled` lets the AX bridge attach without the runner app; written once per open. */
21
38
  export declare const automationArgv: (udid: string) => string[];
@@ -38,6 +55,22 @@ export type StatusBarOverrides = {
38
55
  /** Clean 9:41 status bar by default (stable screenshots); pass `undefined` fields to drop them. */
39
56
  export declare function statusBarOverrideArgv(udid: string, o?: StatusBarOverrides): string[];
40
57
  export declare const statusBarClearArgv: (udid: string) => string[];
58
+ export declare const locationSetArgv: (udid: string, point: LatLon) => string[];
59
+ export declare const locationClearArgv: (udid: string) => string[];
60
+ /** `name` is Apple's scenario name (`City Run`), see {@link IOS_SCENARIO_NAMES}. */
61
+ export declare const locationRunArgv: (udid: string, name: string) => string[];
62
+ /** A route with more waypoints than this goes through stdin (`-`) instead of the argument list. */
63
+ export declare const LOCATION_ARGV_MAX_POINTS = 100;
64
+ export type LocationStartOptions = {
65
+ speedMps?: number;
66
+ intervalS?: number;
67
+ };
68
+ /**
69
+ * `simctl location <udid> start [--speed=] [--interval=] <lat,lon>…`. The sim plays the path itself
70
+ * after the command returns. Past {@link LOCATION_ARGV_MAX_POINTS} the waypoints ride stdin of a tiny
71
+ * `sh -c` (one `lat,lon` per line, never interpolated into the script) with the `-` operand.
72
+ */
73
+ export declare function locationStartArgv(udid: string, points: readonly LatLon[], o?: LocationStartOptions): string[];
41
74
  /**
42
75
  * `simctl pbcopy` reads stdin and `Exec` has none, so the text rides as `$1` of a tiny `sh -c`
43
76
  * (never interpolated into the script): `printf %s "$1" | xcrun simctl pbcopy "$2"`.
@@ -91,6 +124,13 @@ export type Simctl = {
91
124
  pbpaste(): DriverResult<string>;
92
125
  appearance(set?: "light" | "dark"): DriverResult<"light" | "dark">;
93
126
  statusBar(overrides?: StatusBarOverrides | "clear"): DriverResult<void>;
127
+ /** `simctl location`: point, route (also from a GPX file), Apple scenario, or clear. Loop is not supported. */
128
+ location(op: LocationCondition): DriverResult<void>;
129
+ /**
130
+ * Face ID / Touch ID through BiometricKit notifications inside the sim. Modality comes from the booted sim's
131
+ * `SIMULATOR_MODEL_IDENTIFIER` (read once; unreadable = Face ID); the answer is the state after the op.
132
+ */
133
+ biometrics(op: BiometricOp): DriverResult<BiometricState>;
94
134
  /** `simctl io screenshot` to `path`, or (no path) to a scratch file read back as PNG bytes */
95
135
  screenshot(path?: string): DriverResult<{
96
136
  path?: string;
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * Per-request budget for the helper's `typeText`: it holds, paces and (for a repeated key) waits per character.
3
3
  * The helper's defaults (pace 5, hold 20, repeat gap 50) cost ~38 ms per character on a quiet local simulator; the
4
- * budget allows ~100 ms (the EAS VirtualMac ran ~3.7x the nominal sleeps) and 200 ms per repeated key.
4
+ * budget allows ~100 ms (the EAS VirtualMac ran ~3.7x the nominal sleeps) and 200 ms per repeated key. The one
5
+ * lead-in pause after the first key (60 ms) fits in the base.
5
6
  */
6
7
  export declare const TYPE_BASE_MS = 10000;
7
8
  export declare const TYPE_PER_CHAR_MS = 100;
8
9
  export declare const TYPE_PER_REPEAT_MS = 200;
9
10
  /** Base + ~100 ms per character + 200 ms per character equal to the one before it. */
10
11
  export declare function typeTextBudgetMs(text: string): number;
12
+ /** The XCUITest runner on a real device types one character per `typeText` call, each waiting for the app to go idle. */
13
+ export declare const DEVICE_TYPE_PER_CHAR_MS = 600;
14
+ /** Base + ~600 ms per character: the runner's per-character typing on a real device. */
15
+ export declare function deviceTypeTextBudgetMs(text: string): number;
@@ -0,0 +1,52 @@
1
+ import type { HostFault, NetworkCondition } from "../../../core/src/conditions/conditions.types";
2
+ import type { ConditionsUpdate } from "../../../core/src/netproxy/control";
3
+ import { type LeaseProxyDeps } from "../../../core/src/netproxy/lease-proxy";
4
+ import { type ProxyConditions } from "../../../core/src/netproxy/shaping";
5
+ import type { Store } from "../../../core/src/store";
6
+ import type { Lease } from "../../../core/src/types";
7
+ import { type AsyncResult, type Result } from "../../../types/src/result";
8
+ /** Where a lease's shaping proxy listens (always loopback on the host). */
9
+ export type ProxyAddress = {
10
+ host: string;
11
+ port: number;
12
+ };
13
+ /**
14
+ * What a driver needs from the lease's shaping proxy (COND-4). The drivers never touch the lease store: whoever
15
+ * opens the driver builds this (`leaseNetworkProxy`) or passes a fake in tests.
16
+ */
17
+ export type NetworkProxyPort = {
18
+ /** The live proxy's address, `undefined` when this lease has none. */
19
+ current(): Promise<ProxyAddress | undefined>;
20
+ /** Start the proxy (or reuse the live one) with `conditions` applied; records `condition` as the lease's network condition. */
21
+ ensure(condition: NetworkCondition, conditions: ProxyConditions): AsyncResult<ProxyAddress>;
22
+ /** Reconfigure the live proxy without dropping connections. Fails when there is none. */
23
+ update(update: ConditionsUpdate): AsyncResult<void>;
24
+ /** Stop the proxy and free its port. */
25
+ stop(): Promise<void>;
26
+ };
27
+ /** `NetworkProxyPort` for one lease over the real store and the detached `netproxy` process. */
28
+ export declare function leaseNetworkProxy(deps: LeaseProxyDeps, lease: Pick<Lease, "id" | "owner" | "ttlMs">): NetworkProxyPort;
29
+ /** The proxy parameters for `condition` plus the faults to keep. Offline is `refuse`: connections close at accept. */
30
+ export declare function proxyConditionsFor(condition: NetworkCondition, faults: readonly HostFault[]): ProxyConditions;
31
+ /**
32
+ * Faults the proxy cannot honour as written. An HTTPS request is a CONNECT tunnel the proxy never reads inside
33
+ * (no MITM), so a `status` fault on a host reaches an https:// request as a reset, not a status code.
34
+ */
35
+ export declare function faultNotes(faults: readonly HostFault[]): string[];
36
+ export type LazyLeaseProxyOptions = {
37
+ leaseId: string;
38
+ env: Record<string, string | undefined>;
39
+ now: () => number;
40
+ /** opens the lease store; called only when the proxy has to be started or stopped */
41
+ openStore: () => Result<Store>;
42
+ /** argv that runs the proxy process (`WARDEN_NETPROXY_COMMAND`); default: `bun netproxy.main.ts` from source */
43
+ command?: readonly string[] | undefined;
44
+ /** test seams passed through to `ensureLeaseProxy` (spawn, pid liveness, port range…) */
45
+ proxyDeps?: Partial<Omit<LeaseProxyDeps, "store" | "env" | "now" | "command">>;
46
+ };
47
+ /**
48
+ * `NetworkProxyPort` for a spec's lease that touches nothing until a network condition needs it: the store is not
49
+ * opened (nor the lease read) before `ensure`, so a spec that never shapes the network costs no store access or
50
+ * proxy. `current` / `update` / `stop` work from the lease's handle file and stay off the store while there is none.
51
+ */
52
+ export declare function lazyLeaseNetworkProxy(o: LazyLeaseProxyOptions): NetworkProxyPort;
@@ -1,19 +1,37 @@
1
1
  import { type Exec } from "../../../core/src/exec";
2
+ import { type StoreOpener } from "../conditions/lease-store";
2
3
  import type { E2eEnv } from "../env";
3
4
  import type { DriverFactory } from "../session";
4
5
  import { type AndroidDriverOptions } from "./android/android-driver";
6
+ import { type ClockRecorder } from "./clock-recorder";
5
7
  import type { Driver, DriverResult } from "./driver.types";
8
+ import { type OpenIosDeviceDriverOptions } from "./ios/device/device-driver";
6
9
  import { type OpenIosDriverOptions } from "./ios/ios-driver";
10
+ import { type NetworkProxyPort } from "./network-proxy";
7
11
  /**
8
12
  * Test seams for {@link openDriver}: everything the platform openers inject except what the env supplies.
9
13
  * `android` carries the Android-only seams (`adb` path, resident-shell spawn, binary runner, clock).
10
14
  */
11
15
  export type OpenDriverDeps = Omit<OpenIosDriverOptions, "udid" | "env" | "exec"> & {
12
16
  exec?: Exec;
17
+ /** bundle ids warden installed on a physical iPhone earlier (the `installs` table): the only others `app.uninstall` may touch */
18
+ ownedBundles?: readonly string[];
13
19
  android?: Omit<AndroidDriverOptions, "serial" | "exec" | "log" | "adb"> & {
14
20
  adb?: string;
15
21
  };
22
+ /** opens the lease store for the shaping proxy; default: the spec process's shared connection */
23
+ openStore?: StoreOpener;
16
24
  };
25
+ /**
26
+ * The shaping proxy of the spec's lease (`WARDEN_LEASE_ID`), built lazily: nothing opens the store or starts a
27
+ * process until a network condition asks for it. None without a lease id, so `network` answers `E_UNSUPPORTED`.
28
+ */
29
+ export declare function leaseProxyFor(env: E2eEnv, deps?: OpenDriverDeps): NetworkProxyPort | undefined;
30
+ /**
31
+ * Records the clock the driver sets on the spec's lease (`leaseClockRecorder`), opening the store on the first
32
+ * change. Best effort: a store that fails is logged and never fails the clock call. None without a lease id.
33
+ */
34
+ export declare function leaseClockRecorderFor(env: E2eEnv, deps?: OpenDriverDeps): ClockRecorder | undefined;
17
35
  /** `WARDEN_E2E_DEBUG=1` logs every helper op and simctl call with its duration to stderr. */
18
36
  export declare const DEBUG_ENV = "WARDEN_E2E_DEBUG";
19
37
  /**
@@ -23,6 +41,8 @@ export declare const DEBUG_ENV = "WARDEN_E2E_DEBUG";
23
41
  * shell `spawn`, which is a test that must never reach a real `adb`), and the injected seams.
24
42
  */
25
43
  export declare function androidOptions(env: E2eEnv, deps: OpenDriverDeps, debug: boolean): AndroidDriverOptions;
44
+ /** The physical iPhone opener's options: the env's udid, runner socket, app and bundle id, and the injected seams. */
45
+ export declare function deviceOptions(env: E2eEnv, deps: OpenDriverDeps, debug: boolean): OpenIosDeviceDriverOptions;
26
46
  /** Open the `Driver` for the device in `env`, by platform. Android deep links go to `env.bundleId` only. */
27
47
  export declare function openDriver(env: E2eEnv, deps?: OpenDriverDeps): DriverResult<Driver>;
28
48
  /** What the session uses when no factory is passed; `preload.ts` registers it with `setDefaultDriverFactory`. */
@@ -0,0 +1,11 @@
1
+ import type { Driver } from "./driver.types";
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"];
4
+ type PhysicalConditionOps = Pick<Driver, (typeof PHYSICAL_CONDITION_OPS)[number]>;
5
+ /**
6
+ * The condition ops for a physical phone: all `E_UNSUPPORTED`. On a phone they would flip airplane mode (cutting
7
+ * wireless adb), set the system clock, timezone or font scale, or proxy the network of somebody's device; the
8
+ * mechanisms behind them (`simctl`, the emulator console, the injected dylib) are simulator / emulator only.
9
+ */
10
+ export declare function physicalConditionOps(device: string): PhysicalConditionOps;
11
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { Platform } from "./driver.types";
2
+ /**
3
+ * What a spec can ask of its device with `test(title, { requires: [...] })`:
4
+ * - `pinch`: two-finger `pinch` / `rotate` (`capabilities.pinch`)
5
+ * - `multiTouch`: a gesture path with more than one finger (`capabilities.maxPointers` of 2)
6
+ */
7
+ export type Requirement = "pinch" | "multiTouch";
8
+ /** The device a spec was collected for, as the env says (`WARDEN_PLATFORM`, `WARDEN_DEVICE_KIND`). */
9
+ export type RequirementTarget = {
10
+ platform: Platform | undefined;
11
+ deviceKind?: "physical" | undefined;
12
+ };
13
+ /** The `requires` the target's driver is known to lack: a spec with any of them is skipped when collected. */
14
+ export declare function missingRequirements(target: RequirementTarget, requires: readonly Requirement[] | undefined): Requirement[];
@@ -4,7 +4,7 @@ import type { Driver, DriverResult, KeyName } from "./driver.types";
4
4
  * fill`/`type` all go through it).
5
5
  *
6
6
  * iOS: `warden-simd` holds, paces and spaces repeated keys itself (`WARDEN_SIMD_PACE_MS`, `WARDEN_SIMD_HOLD_MS`,
7
- * `WARDEN_SIMD_REPEAT_GAP_MS`), so the whole string goes in one request and the client adds no gap. Before that
7
+ * `WARDEN_SIMD_REPEAT_GAP_MS`, `WARDEN_SIMD_LEAD_MS`), so the whole string goes in one request and the client adds no gap. Before that
8
8
  * the client sent one grapheme per request, 25 ms apart (set `KEY_PACE_MS` 25 and `SPLIT_TEXT` true to go back).
9
9
  */
10
10
  /** Gap after each key on iOS. */
@@ -1,3 +1,4 @@
1
+ import { type ConditionsConfig } from "../../core/src/conditions/conditions.config";
1
2
  import type { Result } from "../../types/src/result";
2
3
  /**
3
4
  * The env names `warden e2e` writes and the spec process reads (`apps/cli` imports these). `WARDEN_HEADLESS` and
@@ -20,7 +21,16 @@ export declare const ENV_NAMES: {
20
21
  readonly helperDir: "WARDEN_HELPER_DIR";
21
22
  readonly simd: "WARDEN_SIMD";
22
23
  readonly simdSocket: "WARDEN_SIMD_SOCKET";
24
+ /** unix socket of a physical iPhone's resident runner host (`warden __ios-runner-host`): the iOS device driver connects there instead of to `warden-simd` */
25
+ readonly iosRunnerSocket: "WARDEN_IOS_RUNNER_SOCKET";
23
26
  readonly logTimestamps: "WARDEN_LOG_TIMESTAMPS";
27
+ /** `physical` when the job's device is a real phone/tablet (set by `warden e2e` / `batch`; absent for a simulator or emulator) */
28
+ readonly deviceKind: "WARDEN_DEVICE_KIND";
29
+ /**
30
+ * JSON argv (`{udid}` is replaced) that restarts a physical iPhone's runner host (`warden __ios-runner-restart`); the
31
+ * session runs it once when the runner connection is lost, and never reboots the phone
32
+ */
33
+ readonly iosRunnerRestart: "WARDEN_IOS_RUNNER_RESTART";
24
34
  /**
25
35
  * JSON argv (`{udid}` is replaced) the session runs after it rebooted the sim following a lost HID connection;
26
36
  * `warden` sets it for non-headless runs so the sim's viewer window comes back (packages/e2e cannot import apps/cli)
@@ -34,6 +44,14 @@ export declare const ENV_NAMES: {
34
44
  readonly perf: "WARDEN_PERF";
35
45
  /** JSON of the suite's `perf.budgets`; a breach fails the test with `PERF_BUDGET` */
36
46
  readonly perfBudgets: "WARDEN_PERF_BUDGETS";
47
+ /** JSON of the suite's `conditions` (network / location / clock applied before each spec, the rest reported as not applied) */
48
+ readonly conditions: "WARDEN_CONDITIONS";
49
+ /** the id of the lease holding `WARDEN_UDID`: conditions a spec sets are recorded on it and its shaping proxy hangs off it */
50
+ readonly leaseId: "WARDEN_LEASE_ID";
51
+ /** JSON argv that runs the shaping proxy (`warden __netproxy`) when specs run from a compiled `warden`; the JSON argument is appended */
52
+ readonly netproxyCommand: "WARDEN_NETPROXY_COMMAND";
53
+ /** `1` when the suite's devices are biometrics devices: the session enrolls an iOS sim again before the app is installed */
54
+ readonly biometrics: "WARDEN_BIOMETRICS";
37
55
  };
38
56
  export declare const DEFAULT_TIMEOUT_MS = 120000;
39
57
  export declare const DEFAULT_ACTION_TIMEOUT_MS = 30000;
@@ -56,6 +74,12 @@ export type E2eEnv = {
56
74
  helperDir?: string;
57
75
  simd?: string;
58
76
  simdSocket?: string;
77
+ /** `WARDEN_IOS_RUNNER_SOCKET`: the runner host's socket for a physical iPhone */
78
+ iosRunnerSocket?: string;
79
+ /** `WARDEN_DEVICE_KIND`: `physical` for a real phone/tablet, absent for a simulator or emulator */
80
+ deviceKind?: "physical";
81
+ /** `WARDEN_IOS_RUNNER_RESTART`: argv that restarts the iPhone's runner host, `{udid}` not yet replaced */
82
+ iosRunnerRestart?: string[];
59
83
  /** warden already stamps the log lines: the spec reporter prints no clock of its own */
60
84
  logTimestamps: boolean;
61
85
  /** `WARDEN_AFTER_REBOOT`: argv run after a HID-loss reboot, `{udid}` not yet replaced (see `runAfterReboot`) */
@@ -68,6 +92,14 @@ export type E2eEnv = {
68
92
  perf?: string[];
69
93
  /** `WARDEN_PERF_BUDGETS`: raw JSON of the suite's budgets */
70
94
  perfBudgets?: string;
95
+ /** `WARDEN_CONDITIONS`: what every spec starts from (`conditions/suite-conditions.ts` applies it) */
96
+ conditions?: ConditionsConfig;
97
+ /** `WARDEN_LEASE_ID`: the device's lease; absent under a bare `bun test`, where network shaping is unsupported */
98
+ leaseId?: string;
99
+ /** `WARDEN_NETPROXY_COMMAND`: argv that starts the shaping proxy; absent from source (`bun netproxy.main.ts`) */
100
+ netproxyCommand?: string[];
101
+ /** `WARDEN_BIOMETRICS`: this is a biometrics device (profile `<p>+bio`); iOS enrollment does not survive a reboot or a spec's `unenroll` */
102
+ biometrics?: boolean;
71
103
  };
72
104
  /**
73
105
  * Reads the spec env contract. `undefined` data = not running under warden (no `WARDEN_UDID`); an
@@ -9,6 +9,10 @@ import type { TraceEvent } from "./runner/trace";
9
9
  export type FixtureSession = {
10
10
  /** absolute `WARDEN_E2E_OUT` (screenshot fallback when no attempt is running) */
11
11
  readonly out: string;
12
+ /** the process's `results.jsonl`; `device.network` / `location` / `clock` append condition events to it */
13
+ readonly results?: {
14
+ readonly path: string;
15
+ };
12
16
  /** the running attempt's deadline / cancel signal; every driver call honours it */
13
17
  readonly signal?: AbortSignal;
14
18
  /** `<out>/<specId>/<title-slug>-<hash6>` of the running attempt */
@@ -1,6 +1,11 @@
1
- import { type Device } from "./device";
1
+ import { type AlertAction, type Device } from "./device";
2
2
  import type { BaseFixtures, FixtureContext } from "./runner/fixtures.types";
3
3
  import type { ScreenHost } from "./screen/context";
4
+ /**
5
+ * `device.alert(action)` for a read that answers sheets on the side: a sheet that stays up after every press
6
+ * (`ACTION_FAILED`) is left for the read to go on with, not thrown out of an unrelated `isVisible()`.
7
+ */
8
+ export declare function answerAlertQuietly(device: Pick<Device, "alert">, action: AlertAction): Promise<boolean>;
4
9
  /**
5
10
  * What `screen` needs from one attempt: the live driver, its timeouts and deadline, and reported steps. With
6
11
  * `systemAlerts` set (`test.use`) and a `device` to press with, reads and waits answer the alerts they see.
@@ -1,4 +1,6 @@
1
1
  export type { OpenOptions, OpenWaitUntil } from "./app";
2
+ export type { ClockInput, ConditionRun, DeviceClock, DeviceLocation, DeviceNetwork, DevicePermissions, Duration, FlapOptions, LocationPoint, NetworkSetOptions, NetworkStatus, RouteOptions, TimelineStepInput, } from "./conditions/device-conditions";
3
+ export type { BiometricState, DeviceBiometrics } from "./device";
2
4
  export { E2eError } from "./errors";
3
5
  export { expect } from "./expect/expect";
4
6
  export type { PerfBreach, PerfBudgets } from "./perf/budgets";
@@ -57,6 +57,7 @@ export declare function registrationDefaults(env?: Readonly<Record<string, strin
57
57
  actionTimeoutMs: number;
58
58
  assertionTimeoutMs: number;
59
59
  platform: "ios" | "android" | undefined;
60
+ deviceKind: "physical" | undefined;
60
61
  };
61
62
  /** Records that a `.only` was collected (kept on `globalThis`: the preload and the specs may be separate bundles). */
62
63
  export declare function markOnly(): void;
@@ -1,4 +1,5 @@
1
1
  import type { Platform } from "../driver/driver.types";
2
+ import { type Requirement } from "../driver/requirements";
2
3
  import { type BaseFixtures } from "./fixtures.types";
3
4
  import { type Hook, type UseOptions } from "./scope";
4
5
  export type TestOptions = {
@@ -8,6 +9,11 @@ export type TestOptions = {
8
9
  only?: boolean;
9
10
  /** run only on these platforms (decided at collection time from `WARDEN_PLATFORM`) */
10
11
  platforms?: readonly Platform[];
12
+ /**
13
+ * driver capabilities the test needs (`pinch`, `multiTouch`). The test is skipped, when collected, on a device whose
14
+ * driver is known to lack one: a physical iPhone has neither. Elsewhere the driver decides at run time.
15
+ */
16
+ requires?: readonly Requirement[];
11
17
  /** extra attempts after a failure; only the last attempt writes to `results.jsonl` */
12
18
  retry?: number;
13
19
  };
@@ -2,7 +2,8 @@ import type { Device } from "./device";
2
2
  import type { SpanRunner } from "./runner/span";
3
3
  /**
4
4
  * `device` with every call timed as a `device.<name>` span in the attempt's `trace.jsonl` (trace only: no console
5
- * line, since a settle or an alert is not a step). The driver calls a device call makes are `driver.*` spans of
5
+ * line, since a settle or an alert is not a step). The condition calls are `device.network.<call>`,
6
+ * `device.location.<call>`, `device.clock.<call>` and `device.timeline`. The driver calls a device call makes are `driver.*` spans of
6
7
  * their own, so a slow `device.openLink` shows what it spent its time on. Without a span runner (a bare
7
8
  * `FixtureSession`) the device is returned unchanged.
8
9
  */