@delacour/warden-e2e 0.5.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 (88) hide show
  1. package/{chunk-j9b17ade.mjs → chunk-empqr9n0.mjs} +1366 -55
  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/screen/field.d.ts +5 -3
  87. package/types/e2e/src/screen/locator.d.ts +3 -2
  88. package/types/e2e/src/traced-device.d.ts +2 -1
@@ -0,0 +1,170 @@
1
+ import { type NetworkInput } from "../../../core/src/conditions/conditions.config";
2
+ import type { ClockCondition, HostFault, LatLon, LocationCondition, LocationScenario, NetworkCondition } from "../../../core/src/conditions/conditions.types";
3
+ import type { A11yOp, A11yState, BatteryOp, BatteryState, Driver, LocaleState, NetworkState, PermissionGrants, PermissionsState, PushPayload, SettingOptions, TimezoneState } from "../driver/driver.types";
4
+ import type { E2eEnv } from "../env";
5
+ import { type ConditionSink } from "./condition-events";
6
+ import type { ConditionRecorder } from "./lease-recorder";
7
+ import { type TickerClock } from "./route-ticker";
8
+ import { type SchedulerOutcome, type SchedulerStatus } from "./scheduler";
9
+ /** A duration in ms, or text like `"10s"`, `"2m"`, `"2d"`. */
10
+ export type Duration = string | number;
11
+ export type NetworkSetOptions = {
12
+ /** iOS: relaunch the app when it runs without the warden network dylib (the default is to fail and say so) */
13
+ relaunch?: boolean;
14
+ };
15
+ export type FlapOptions = {
16
+ /** how long the network stays up per cycle */
17
+ up: Duration;
18
+ /** how long it stays down (offline) per cycle */
19
+ down: Duration;
20
+ /** each phase lasts its length +/- this percent, drawn from `seed` */
21
+ jitterPct?: number;
22
+ /** same seed, same sequence; default 1 */
23
+ seed?: number;
24
+ /** the "up" condition and what `stop()` puts back; default `"online"` */
25
+ base?: NetworkInput;
26
+ };
27
+ /** A running flap or timeline. It is stopped by `stop()` and, at the latest, when the test ends. */
28
+ export type ConditionRun = {
29
+ /** stops it (a flap puts its base back first) and resolves with how it ended */
30
+ stop(): Promise<SchedulerOutcome>;
31
+ /** resolves when it ended; never rejects */
32
+ readonly done: Promise<SchedulerOutcome>;
33
+ status(): SchedulerStatus;
34
+ };
35
+ export type NetworkStatus = {
36
+ /** what is in effect (`flap` while one runs) */
37
+ condition: NetworkCondition;
38
+ faults: readonly HostFault[];
39
+ flapping: boolean;
40
+ };
41
+ export type DeviceNetwork = {
42
+ /**
43
+ * Puts the network into `condition`: `"online"`, `"offline"`, a preset (`"edge"`, `"3g"`, `"4g"`, `"lte"`,
44
+ * `"wifi"`, `"slow"`, `"lossy"`) or a full `NetworkCondition`. A running flap stops first. On iOS the app must
45
+ * run with the warden network dylib: a suite that declares `conditions.network` launches it that way, else the
46
+ * call fails until you pass `{ relaunch: true }`.
47
+ */
48
+ set(condition: NetworkInput, options?: NetworkSetOptions): Promise<NetworkState>;
49
+ /** Runs `fn` under `condition` and puts the previous condition (and flap) back after, also when `fn` throws. */
50
+ during<T>(condition: NetworkInput, fn: () => Promise<T> | T, options?: NetworkSetOptions): Promise<T>;
51
+ /** Alternates up and down until `stop()` or the end of the test; resolves once the first phase is in place. */
52
+ flap(options: FlapOptions): Promise<ConditionRun>;
53
+ /** Replaces the per-host faults (status, reset, delay, blackhole by host glob); `[]` clears. */
54
+ fault(rules: HostFault | readonly HostFault[]): Promise<{
55
+ notes: readonly string[];
56
+ }>;
57
+ status(): Promise<NetworkStatus>;
58
+ /** Stops a flap, clears the faults and returns to the suite's `conditions.network` (online when it has none). */
59
+ reset(): Promise<void>;
60
+ };
61
+ export type LocationPoint = {
62
+ lat: number;
63
+ lon: number;
64
+ altM?: number;
65
+ };
66
+ export type RouteOptions = {
67
+ speedMps?: number;
68
+ intervalS?: number;
69
+ loop?: boolean;
70
+ };
71
+ export type DeviceLocation = {
72
+ set(point: LocationPoint): Promise<void>;
73
+ /** Plays waypoints (or a `{ gpx }` file, relative to the working directory) at `speedMps`. */
74
+ route(points: readonly LatLon[] | {
75
+ gpx: string;
76
+ }, options?: RouteOptions): Promise<void>;
77
+ scenario(name: LocationScenario): Promise<void>;
78
+ clear(): Promise<void>;
79
+ };
80
+ /** An ISO string, a `Date` or epoch milliseconds. */
81
+ export type ClockInput = string | number | Date;
82
+ export type DeviceClock = {
83
+ /** Sets the device's wall clock; it runs on from there unless `frozen`. */
84
+ set(at: ClockInput, options?: {
85
+ frozen?: boolean;
86
+ }): Promise<void>;
87
+ /** Moves the clock on by `by` (`"2d"`, or ms). */
88
+ advance(by: Duration): Promise<void>;
89
+ /** Runs `fn` with the clock at `at`, then puts the previous clock back, also when `fn` throws. */
90
+ during<T>(at: ClockInput, fn: () => Promise<T> | T, options?: {
91
+ frozen?: boolean;
92
+ }): Promise<T>;
93
+ /** Back to the suite's `conditions.clock`, else real time. */
94
+ reset(): Promise<void>;
95
+ status(): ClockCondition;
96
+ };
97
+ export type DevicePermissions = {
98
+ /**
99
+ * Sets permissions of the app, per name (`location`, `camera`, `photos`, `contacts`, `notifications`, `microphone`,
100
+ * ...): `"granted"`, `"denied"` or `"unset"` (asks again). The result lists the names this platform skipped. Set
101
+ * them before `app.open()`: iOS terminates a running app when its permission changes.
102
+ */
103
+ set(grants: PermissionGrants, options?: SettingOptions): Promise<PermissionsState>;
104
+ /** Puts every permission this test set back to the suite's `conditions.permissions`, else `"unset"`. */
105
+ reset(options?: SettingOptions): Promise<void>;
106
+ };
107
+ export type TimelineStepInput = {
108
+ /** when, from the start of the timeline */
109
+ at: Duration;
110
+ network?: NetworkInput;
111
+ location?: LocationCondition | LatLon;
112
+ };
113
+ /** What the runner needs of a device's conditions; reached through {@link conditionsOf}, not on `Device`. */
114
+ export type ConditionsInternals = {
115
+ /** the suite declares a network or clock condition, or one is active: iOS must launch with the dylib */
116
+ wantsInject(): boolean;
117
+ /** puts the suite's declared network, location and clock in place; warns once about areas not applied yet */
118
+ applyBaseline(): Promise<void>;
119
+ /** something changed or still runs: `teardown` has work to do */
120
+ dirty(): boolean;
121
+ /** stops flaps and timelines, and puts every area this device changed back to the baseline */
122
+ teardown(): Promise<void>;
123
+ /** the suite declares something to apply */
124
+ readonly declared: boolean;
125
+ };
126
+ export type DeviceConditions = {
127
+ network: DeviceNetwork;
128
+ location: DeviceLocation;
129
+ clock: DeviceClock;
130
+ permissions: DevicePermissions;
131
+ /** Device timezone (`"Asia/Tokyo"`); `null` puts it back. iOS: applies to the app launched next. */
132
+ timezone(tz: string | null): Promise<TimezoneState>;
133
+ /** The app's locale (`"fr-FR"`); `null` puts it back. iOS: applies to the app launched next. Android 13+. */
134
+ locale(tag: string | null, options?: SettingOptions): Promise<LocaleState>;
135
+ /** Text size (a name like `"accessibility-large"` or a scale like `1.5`), bold text, contrast, reduce motion; `null` resets. */
136
+ a11y(op: A11yOp | null): Promise<A11yState>;
137
+ /** Battery level and charging. Android emulators: real. iOS: the status bar only (`cosmetic: true`). `null` resets. */
138
+ battery(op: BatteryOp | null): Promise<BatteryState>;
139
+ /** Android: a critical trim-memory signal to the running app. iOS: `E_UNSUPPORTED` (no delivery mechanism works). */
140
+ memoryWarning(options?: SettingOptions): Promise<void>;
141
+ /** iOS: delivers an APNs payload (`{ aps: { alert: ... } }`) to the app. Android: `E_UNSUPPORTED`. */
142
+ push(payload: PushPayload, options?: SettingOptions): Promise<void>;
143
+ timeline(steps: readonly TimelineStepInput[]): Promise<ConditionRun>;
144
+ internals: ConditionsInternals;
145
+ };
146
+ export type DeviceConditionsDeps = {
147
+ driver: Driver;
148
+ env: E2eEnv;
149
+ /** the running attempt's signal, read per call */
150
+ signal: () => AbortSignal | undefined;
151
+ /** where condition events go (`results.jsonl`); default none */
152
+ sink?: ConditionSink;
153
+ /** time for flaps, timelines and event stamps */
154
+ tickerClock?: TickerClock;
155
+ /** notes (a restore that failed, areas not applied yet); default stderr */
156
+ log?: (line: string) => void;
157
+ /** where changes are written on the device's lease so release can undo them after a crash; default none */
158
+ recorder?: ConditionRecorder;
159
+ };
160
+ /** Ties a device (or a wrapper of it) to its conditions' runner hooks; `Device` itself does not expose them. */
161
+ export declare function registerConditions(device: object, internals: ConditionsInternals): void;
162
+ /** The runner hooks of a device made by `createDevice` (or a `traceDevice` wrapper of one). */
163
+ export declare function conditionsOf(device: object): ConditionsInternals | undefined;
164
+ /** The condition sink for a session: events append to its `results.jsonl`; none without one. */
165
+ export declare function conditionSinkFor(session: {
166
+ results?: {
167
+ path: string;
168
+ };
169
+ }): ConditionSink | undefined;
170
+ export declare function createDeviceConditions(deps: DeviceConditionsDeps): DeviceConditions;
@@ -0,0 +1,15 @@
1
+ import type { LatLon } from "../../../core/src/conditions/conditions.types";
2
+ /** Great-circle distance in metres. */
3
+ export declare function haversineM(a: LatLon, b: LatLon): number;
4
+ /** Initial bearing from `a` to `b`, degrees clockwise from north in [0, 360). */
5
+ export declare function bearingDeg(a: LatLon, b: LatLon): number;
6
+ /** The point a fraction `f` (0..1) of the way along the great circle from `a` to `b`. */
7
+ export declare function interpolate(a: LatLon, b: LatLon, f: number): LatLon;
8
+ export declare function pathLengthM(points: readonly LatLon[]): number;
9
+ /**
10
+ * One fix per interval along `points`: a fix every `speedMps * intervalS` metres of path, the first
11
+ * waypoint first and the last waypoint last (no duplicate when the length is an exact multiple).
12
+ */
13
+ export declare function routeSteps(points: readonly LatLon[], speedMps: number, intervalS: number): LatLon[];
14
+ /** 7 decimals (about 1 cm), no trailing zeros, never an exponent. */
15
+ export declare function formatCoord(n: number): string;
@@ -0,0 +1,8 @@
1
+ import type { LatLon } from "../../../core/src/conditions/conditions.types";
2
+ import { type Result } from "../../../types/src/result";
3
+ /**
4
+ * Tolerant, dependency-free GPX reader: every `<trkpt>`, `<rtept>` and `<wpt>` with a valid `lat` and
5
+ * `lon` attribute, in document order. Comments are dropped, a point with a bad or missing coordinate is
6
+ * skipped, and truncated XML yields the points before the break. No points at all is an error.
7
+ */
8
+ export declare function parseGpx(xml: string): Result<LatLon[]>;
@@ -0,0 +1,27 @@
1
+ import type { ConditionArea, ConditionValues } from "../../../core/src/conditions/conditions.types";
2
+ import { type PidAlive } from "../../../core/src/liveness";
3
+ import type { E2eEnv } from "../env";
4
+ import { type StoreOpener } from "./lease-store";
5
+ /**
6
+ * Writes what a spec changed on its device to the lease (`lease_conditions`), so `warden release` / gc can undo it
7
+ * when the spec process dies before its own teardown. Best effort: a failure is logged and never reaches the test.
8
+ */
9
+ export type ConditionRecorder = {
10
+ /** the device now has `value` in `area` */
11
+ record<A extends ConditionArea>(area: A, value: ConditionValues[A]): void;
12
+ /**
13
+ * the device is back at its baseline `value`: when that is `neutral` (nothing to undo) the area is forgotten,
14
+ * unless a process is still held for it (a live shaping proxy), which release must still stop
15
+ */
16
+ settle<A extends ConditionArea>(area: A, value: ConditionValues[A], neutral: boolean): void;
17
+ };
18
+ export type LeaseRecorderOptions = {
19
+ leaseId: string;
20
+ openStore?: StoreOpener;
21
+ now?: () => number;
22
+ log?: (line: string) => void;
23
+ pidAlive?: PidAlive;
24
+ };
25
+ export declare function createLeaseRecorder(o: LeaseRecorderOptions): ConditionRecorder;
26
+ /** The recorder for a spec under a lease; none without `WARDEN_LEASE_ID` (a bare `bun test` has no lease to record on). */
27
+ export declare function leaseRecorderFor(env: Pick<E2eEnv, "leaseId">): ConditionRecorder | undefined;
@@ -0,0 +1,9 @@
1
+ import { type Store } from "../../../core/src/store";
2
+ import { type Result } from "../../../types/src/result";
3
+ /** Opens the lease store; a failure is a `Result`, never a throw. */
4
+ export type StoreOpener = () => Result<Store>;
5
+ /**
6
+ * The spec process's one connection to the lease store, opened on first use: a run that sets no condition and
7
+ * shapes no network never opens it. Connections are kept per db path for the life of the process.
8
+ */
9
+ export declare function sharedStoreOpener(env?: Readonly<Record<string, string | undefined>>): StoreOpener;
@@ -0,0 +1,17 @@
1
+ import type { LatLon, LocationCondition } from "../../../core/src/conditions/conditions.types";
2
+ import { type Result } from "../../../types/src/result";
3
+ export type ReadText = (path: string) => Promise<string>;
4
+ export type RoutePlan = {
5
+ points: readonly LatLon[];
6
+ speedMps: number;
7
+ intervalS: number;
8
+ loop: boolean;
9
+ };
10
+ export declare function loadGpxPoints(path: string, readText: ReadText): Promise<Result<LatLon[]>>;
11
+ /**
12
+ * Everything a ticker needs for the moving location ops (`route`, `gpx`, `scenario`): waypoints, pace,
13
+ * loop. Used where the platform has no player of its own (Android); iOS hands the waypoints to simctl.
14
+ */
15
+ export declare function planRoute(op: Extract<LocationCondition, {
16
+ kind: "route" | "gpx" | "scenario";
17
+ }>, readText: ReadText): Promise<Result<RoutePlan>>;
@@ -0,0 +1,17 @@
1
+ import type { LatLon, LocationScenario } from "../../../core/src/conditions/conditions.types";
2
+ /**
3
+ * Apple's names for the three simulated-location scenarios `simctl location <udid> run` knows (verify
4
+ * with `xcrun simctl location <udid> list`).
5
+ */
6
+ export declare const IOS_SCENARIO_NAMES: Record<LocationScenario, string>;
7
+ export type ScenarioRoute = {
8
+ points: readonly LatLon[];
9
+ speedMps: number;
10
+ loop: boolean;
11
+ };
12
+ /**
13
+ * Android parity: a similar path and pace for each scenario, played by the route ticker. These are
14
+ * warden's own waypoints around Apple Park, Cupertino, not Apple's data: same character (walking
15
+ * pace loop, cycling loop, highway stretch), not identical coordinates.
16
+ */
17
+ export declare const ANDROID_SCENARIOS: Record<LocationScenario, ScenarioRoute>;
@@ -0,0 +1,53 @@
1
+ import type { LatLon } from "../../../core/src/conditions/conditions.types";
2
+ import { type Result } from "../../../types/src/result";
3
+ /** simctl's own default pace, so a route moves the same on both platforms. */
4
+ export declare const DEFAULT_SPEED_MPS = 20;
5
+ export declare const DEFAULT_INTERVAL_S = 1;
6
+ export type TickerClock = {
7
+ now(): number;
8
+ /** resolves after `ms`, or as soon as `signal` aborts */
9
+ sleep(ms: number, signal: AbortSignal): Promise<void>;
10
+ };
11
+ export declare const realTickerClock: TickerClock;
12
+ export type TickerRoute = {
13
+ points: readonly LatLon[];
14
+ speedMps?: number;
15
+ intervalS?: number;
16
+ loop?: boolean;
17
+ };
18
+ export type TickerDeps = TickerClock & {
19
+ /** send one fix; return an error string (or throw) to count a failure, nothing when it landed */
20
+ fix(point: LatLon): Promise<string | undefined>;
21
+ };
22
+ export type TickerOutcome = {
23
+ ticks: number;
24
+ stopped: "finished" | "aborted";
25
+ } | {
26
+ ticks: number;
27
+ stopped: "failed";
28
+ error: string;
29
+ };
30
+ export type RouteTicker = {
31
+ /** idempotent; the loop exits before sending another fix */
32
+ stop(): void;
33
+ done: Promise<TickerOutcome>;
34
+ };
35
+ /**
36
+ * Plays `route` as one fix per `intervalS`, paced against the clock (a slow fix shortens the next
37
+ * sleep). Runs in this process: specs hold the handle and `stop()` it, a detached child runs the same
38
+ * loop from `route-ticker.main.ts`. Ends when the route is done (never, with `loop`), when stopped, or
39
+ * after {@link MAX_CONSECUTIVE_FAILURES} failed fixes in a row.
40
+ */
41
+ export declare function startRouteTicker(route: TickerRoute, deps: TickerDeps): RouteTicker;
42
+ /** What the detached child needs: the adb to call and the route to play. */
43
+ export type TickerConfig = TickerRoute & {
44
+ adb: string;
45
+ serial: string;
46
+ };
47
+ export declare function parseTickerConfig(raw: string): Result<TickerConfig>;
48
+ /**
49
+ * argv that runs the ticker as its own process: `[runtime, route-ticker.main.ts, <config JSON>]`. The
50
+ * caller spawns it detached and stores the child's pid as an `OwnedProcess` with `recordCondition`, so
51
+ * COND-1 undo (`warden release`, gc) kills it.
52
+ */
53
+ export declare function detachedTickerArgv(runtime: string, mainPath: string, config: TickerConfig): string[];
@@ -0,0 +1,83 @@
1
+ import type { LocationCondition, NetworkCondition } from "../../../core/src/conditions/conditions.types";
2
+ import type { ConditionSink } from "./condition-events";
3
+ import type { TickerClock } from "./route-ticker";
4
+ export type StepResult = {
5
+ success: true;
6
+ } | {
7
+ success: false;
8
+ error: {
9
+ message: string;
10
+ };
11
+ };
12
+ /** The slice of a `Driver` the scheduler drives. A driver satisfies it; the detached child builds a smaller one. */
13
+ export type SchedulerTarget = {
14
+ network?(condition: NetworkCondition): Promise<StepResult>;
15
+ location?(op: LocationCondition): Promise<StepResult>;
16
+ };
17
+ export type SchedulerDeps = TickerClock & {
18
+ /** uniform [0, 1) for jitter; default is a generator seeded from `seed` */
19
+ rng?: () => number;
20
+ /** told about every transition that landed */
21
+ sink?: ConditionSink;
22
+ };
23
+ export type FlapOptions = {
24
+ upMs: number;
25
+ downMs: number;
26
+ /** each phase lasts its length +/- this percent, drawn from the seeded rng */
27
+ jitterPct?: number;
28
+ /** the "up" condition, default online; also what stop restores */
29
+ base?: NetworkCondition;
30
+ seed?: number;
31
+ /** false when the caller already put the device in `base`; the first phase then just waits */
32
+ applyInitial?: boolean;
33
+ };
34
+ export type TimelineStep = {
35
+ atMs: number;
36
+ network?: NetworkCondition;
37
+ location?: LocationCondition;
38
+ };
39
+ export type SchedulerOutcome = {
40
+ transitions: number;
41
+ stopped: "finished" | "aborted";
42
+ } | {
43
+ transitions: number;
44
+ stopped: "failed";
45
+ error: string;
46
+ };
47
+ export type SchedulerStatus = {
48
+ state: "running";
49
+ transitions: number;
50
+ phase?: "up" | "down";
51
+ step?: number;
52
+ } | {
53
+ state: "finished" | "stopped";
54
+ transitions: number;
55
+ } | {
56
+ state: "failed";
57
+ transitions: number;
58
+ error: string;
59
+ };
60
+ export type Scheduler = {
61
+ /** idempotent; flap puts `base` back before `done` resolves, a timeline just stops */
62
+ stop(): void;
63
+ /** resolves with the outcome, never rejects */
64
+ done: Promise<SchedulerOutcome>;
65
+ current(): SchedulerStatus;
66
+ };
67
+ /** mulberry32: small, fast, and the same sequence for the same seed. */
68
+ export declare function seededRng(seed: number): () => number;
69
+ /**
70
+ * Alternates the network between `base` (up, `upMs`) and offline (down, `downMs`), each phase stretched or
71
+ * shrunk by up to `jitterPct` using a seeded rng, paced against the clock. Starts in the up phase. In this
72
+ * process: the driver holds the handle and stops it on the next network op or `close()`; a detached child runs
73
+ * the same loop (`scheduler.main.ts`). 3 failed transitions in a row end it as `failed`. However it ends, `base`
74
+ * is put back before `done` resolves (best effort when it ended on a failure).
75
+ */
76
+ export declare function startFlap(target: SchedulerTarget, options: FlapOptions, deps: SchedulerDeps): Scheduler;
77
+ /**
78
+ * Runs `steps` in `atMs` order (ties keep the given order), each at `atMs` after the start, paced against the
79
+ * clock. A step with both `network` and `location` applies the network first, then the location. A step counts
80
+ * as failed when either part fails; 3 failed steps in a row end it as `failed`. Stopping leaves the device as
81
+ * the last step left it (restoring is the spec API's job: it knows the previous conditions).
82
+ */
83
+ export declare function startTimeline(target: SchedulerTarget, steps: readonly TimelineStep[], deps: SchedulerDeps): Scheduler;
@@ -1,5 +1,7 @@
1
+ import { type ConditionRun, type DeviceClock, type DeviceConditions, type DeviceLocation, type DeviceNetwork, type DevicePermissions, type TimelineStepInput } from "./conditions/device-conditions";
2
+ import { type ConditionRecorder } from "./conditions/lease-recorder";
1
3
  import { type DeviceDevtools } from "./device-devtools";
2
- import type { Driver, Orientation } from "./driver/driver.types";
4
+ import type { BiometricState, Driver, Orientation } from "./driver/driver.types";
3
5
  import { type SettleDetail } from "./driver/settle";
4
6
  import type { E2eEnv } from "./env";
5
7
  import { type Clock, type FixtureSession } from "./fixture-session";
@@ -29,12 +31,33 @@ export type SettleOptions = {
29
31
  /** give up after this long. Default 3000 ms. */
30
32
  timeout?: number;
31
33
  };
34
+ export type { BiometricState };
35
+ /**
36
+ * Simulated Face ID / Touch ID / fingerprint (simulators and emulators only). Every method answers the state after the op.
37
+ * iOS: the first prompt per app shows an "Allow X to use Face ID?" alert; answer it with `device.alert("accept")`.
38
+ * Enroll before installing or launching the app: a new enrollment invalidates biometry-bound keys.
39
+ * Android: `enroll` / `unenroll` throw `ACTION_FAILED` (driver `E_UNSUPPORTED`); enrollment comes from the golden snapshot, so set
40
+ * the profile's `biometrics: true`.
41
+ */
42
+ export type DeviceBiometrics = {
43
+ status(): Promise<BiometricState>;
44
+ /** iOS: enrolls Face ID / Touch ID. Android: unsupported, use profile `biometrics: true`. */
45
+ enroll(): Promise<BiometricState>;
46
+ /** iOS: removes the enrollment. Android: unsupported. */
47
+ unenroll(): Promise<BiometricState>;
48
+ /** Presents a matching face / finger to the pending prompt. */
49
+ match(): Promise<BiometricState>;
50
+ /** Presents a non-matching face / finger to the pending prompt. */
51
+ fail(): Promise<BiometricState>;
52
+ };
32
53
  export type Device = {
33
54
  /**
34
55
  * Answers the system alert / sheet on top. `accept` presses the first present of OK, Allow, Open,
35
56
  * Continue, Yes, Done, Allow While Using App, Allow Once (else the last button); `dismiss` the first
36
57
  * of Cancel, Don't Allow, Not Now, Dismiss, No, Close (else the first button); `{button}` the button
37
- * with that name (`ACTION_FAILED` when absent). Resolves false when no alert is showing.
58
+ * with that name (`ACTION_FAILED` when absent). The Android autofill "Save password?" sheet is closed with Back for
59
+ * `accept` and `dismiss` alike (never its Never / Continue buttons, which write to the user's account). Resolves false
60
+ * when no alert is showing.
38
61
  */
39
62
  alert(action: AlertAction): Promise<boolean>;
40
63
  /**
@@ -50,6 +73,8 @@ export type Device = {
50
73
  back(): Promise<void>;
51
74
  orientation(set?: Orientation): Promise<Orientation>;
52
75
  appearance(set?: "light" | "dark"): Promise<"light" | "dark">;
76
+ /** Simulated biometrics: `status`, `enroll`, `unenroll`, `match`, `fail`; see {@link DeviceBiometrics}. */
77
+ readonly biometrics: DeviceBiometrics;
53
78
  clipboard(): Promise<string>;
54
79
  setClipboard(text: string): Promise<void>;
55
80
  /**
@@ -78,6 +103,31 @@ export type Device = {
78
103
  * typed report. The report is copied under the attempt's `perf/`; budgets from `warden.config.ts` fail `stop`.
79
104
  */
80
105
  readonly perf: DevicePerf;
106
+ /**
107
+ * Network conditions: `set("offline" | "3g" | …)`, `during`, `flap`, `fault`, `status`, `reset`. Everything a test
108
+ * changes is put back when it ends, to the suite's `conditions` (`warden.config.ts`).
109
+ */
110
+ readonly network: DeviceNetwork;
111
+ /** Simulated location: `set`, `route`, `scenario`, `clear`. Put back when the test ends. */
112
+ readonly location: DeviceLocation;
113
+ /** The device's wall clock: `set`, `advance("2d")`, `during`, `reset`. Needs a driver with a clock op. */
114
+ readonly clock: DeviceClock;
115
+ /** Runtime permissions of the app: `set({ location: "denied", camera: "granted" })`, `reset`. Set before `app.open()`. */
116
+ readonly permissions: DevicePermissions;
117
+ /** Device timezone (`"Asia/Tokyo"`, `null` resets). iOS: for the app launched next. */
118
+ timezone: DeviceConditions["timezone"];
119
+ /** The app's locale (`"fr-FR"`, `null` resets). iOS: for the app launched next. Android 13+. */
120
+ locale: DeviceConditions["locale"];
121
+ /** Text size, bold text, contrast, reduce motion (`null` resets); the result names what the platform skipped. */
122
+ a11y: DeviceConditions["a11y"];
123
+ /** Battery level and charging: real on Android emulators, status bar only on iOS (`cosmetic: true`). */
124
+ battery: DeviceConditions["battery"];
125
+ /** Android: a critical trim-memory signal to the app. iOS: `E_UNSUPPORTED`. */
126
+ memoryWarning: DeviceConditions["memoryWarning"];
127
+ /** iOS: delivers an APNs payload to the app. Android: `E_UNSUPPORTED`. */
128
+ push: DeviceConditions["push"];
129
+ /** Applies network and location changes at times from now; stopped when the test ends. */
130
+ timeline(steps: readonly TimelineStepInput[]): Promise<ConditionRun>;
81
131
  };
82
132
  export type DeviceDeps = {
83
133
  session: FixtureSession;
@@ -86,5 +136,9 @@ export type DeviceDeps = {
86
136
  clock?: Clock;
87
137
  /** runs `warden …` for `device.perf`; default spawns `WARDEN_BIN` */
88
138
  perfExec?: PerfExec;
139
+ /** shared with `app` so a launch knows to load the network dylib; default: made here */
140
+ conditions?: DeviceConditions;
89
141
  };
90
- export declare function createDevice({ session, driver, env, clock, perfExec }: DeviceDeps): Device;
142
+ export declare function createDevice({ session, driver, env, clock, perfExec, conditions, }: DeviceDeps): Device;
143
+ /** The conditions of one attempt's device: events go to the session's `results.jsonl`, calls honour its signal. */
144
+ export declare function createSessionConditions(session: FixtureSession, driver: Driver, env: E2eEnv, recorder?: ConditionRecorder | undefined): DeviceConditions;
@@ -9,6 +9,10 @@ export type AppDeps = {
9
9
  log?: Logger;
10
10
  /** the app under test: `openUrl` is addressed to it (`am start -p`) so no other handler or chooser can take the link */
11
11
  appPackage?: string;
12
+ /** a real phone: `uninstall` and `clearData` only touch `appPackage` and `ownedPackages` (`E_REFUSED` otherwise) */
13
+ physical?: boolean;
14
+ /** packages warden installed on this device besides `appPackage` */
15
+ ownedPackages?: readonly string[];
12
16
  /** how long `am start -W` may wait for the first frame (default `START_WAIT_TIMEOUT_MS`); tests shorten it */
13
17
  startWaitTimeoutMs?: number;
14
18
  /** how long the `pidof` after a timed-out start may take (default `START_PROBE_TIMEOUT_MS`); tests shorten it */
@@ -1,3 +1,4 @@
1
+ import type { LatLon } from "../../../../core/src/conditions/conditions.types";
1
2
  import type { Result } from "../../../../types/src/result";
2
3
  import type { DriverError, HardwareButton, KeyName, Point } from "../driver.types";
3
4
  import type { GesturePlan } from "../gesture-geometry";
@@ -14,6 +15,13 @@ export type MotionAction = "DOWN" | "MOVE" | "UP" | "CANCEL";
14
15
  export declare function motionEventArgv(action: MotionAction, point: Point): string[];
15
16
  /** Lowest sdk with `input motionevent` (Android 8.0). */
16
17
  export declare const MOTIONEVENT_MIN_SDK = 26;
18
+ /** Host-side `adb -s <serial> emu finger touch <id>`: the emulator console presents fingerprint `id` to the sensor. */
19
+ export declare function emuFingerTouchArgv(adb: string, serial: string, fingerId: number): string[];
20
+ /** An id no enrollment holds: the emulator reports a non-matching touch. */
21
+ export declare const UNENROLLED_FINGER_ID = 99;
22
+ export declare function dumpsysFingerprintArgv(): string[];
23
+ /** Emulator serials (`emulator-5554`); the console commands exist nowhere else. */
24
+ export declare const isEmulatorSerial: (serial: string) => boolean;
17
25
  /** `input keycombination` (and so select-all by Ctrl+A) exists from Android 13. */
18
26
  export declare const SELECT_ALL_MIN_SDK = 33;
19
27
  export declare const DRAG_STEPS = 8;
@@ -104,6 +112,17 @@ export declare function dumpTtyHostArgv(adb: string, serial: string): string[];
104
112
  export declare function dumpFileCommand(path?: string): string;
105
113
  /** Host argv: `adb -s <serial> install -r <apk>`. */
106
114
  export declare function installArgv(adb: string, serial: string, apkPath: string): string[];
115
+ /**
116
+ * Host argv: `adb -s <serial> emu geo fix <lon> <lat> [alt]`. The emulator console takes LONGITUDE first,
117
+ * the reverse of `simctl location set lat,lon` and of everything else here.
118
+ */
119
+ export declare function geoFixArgv(adb: string, serial: string, point: LatLon, altM?: number): string[];
120
+ /** `adb emu` exits 0 even when the console answers `KO: …`; the reason, or undefined when it took. */
121
+ export declare function geoFixFailure(res: {
122
+ exitCode: number;
123
+ stdout: string;
124
+ stderr: string;
125
+ }): string | undefined;
107
126
  export declare function uninstallArgv(adb: string, serial: string, packageName: string): string[];
108
127
  /** Exit 0 and `package:/…` when installed, exit 1 and nothing otherwise. */
109
128
  export declare function pmPathArgv(packageName: string): string[];
@@ -0,0 +1,28 @@
1
+ import type { ClockCondition } from "../../../../core/src/conditions/conditions.types";
2
+ import type { Exec } from "../../../../core/src/exec";
3
+ import type { ClockState, DriverResult } from "../driver.types";
4
+ import type { AdbSession } from "./adb-session";
5
+ /**
6
+ * Android mock clock: system time, device-wide. `settings put global auto_time 0` stops the network time sync from
7
+ * undoing it, `cmd alarm set-time <epoch ms>` sets it (a shell-uid command on emulator images, verified on the
8
+ * device's own `date`); where that does not take, `adb root` + `date -s @<secs>`. `real` is the undo in
9
+ * `core/conditions/undo.ts`: `auto_time 1` then the host's time. There is no way to freeze Android's clock, so
10
+ * `frozen` is `E_UNSUPPORTED`. Monotonic clocks are not affected by a wall-clock set.
11
+ */
12
+ export declare const AUTO_TIME_OFF_ARGV: readonly string[];
13
+ export declare const AUTO_TIME_ON_ARGV: readonly string[];
14
+ export declare const DATE_ARGV: readonly string[];
15
+ export declare const setTimeArgv: (epochMs: number) => string[];
16
+ /** How far the device's `date` may be from the target (it only reports whole seconds, and a call takes time). */
17
+ export declare const CLOCK_TOLERANCE_MS = 5000;
18
+ export type AndroidClockDeps = {
19
+ session: AdbSession;
20
+ /** host runner for the `adb root` fallback */
21
+ exec: Exec;
22
+ adb: string;
23
+ serial: string;
24
+ /** host wall time, ms (default `Date.now`) */
25
+ wallNow?: () => number;
26
+ };
27
+ /** Put the emulator's wall clock at `condition`; reads it back and fails with `E_IO` when it did not move. */
28
+ export declare function applyAndroidClock(deps: AndroidClockDeps, condition: ClockCondition): DriverResult<ClockState>;
@@ -1,6 +1,7 @@
1
1
  import type { DriverResult, Logger, Orientation, ScreenInfo } from "../driver.types";
2
2
  import type { AdbSession } from "./adb-session";
3
3
  import { type RotationValue } from "./android-argv";
4
+ import { type PhoneFacts } from "./android-phone";
4
5
  /** Device facts read once at open: boot flag, screen size/density. Pure parsers + small session helpers. */
5
6
  export declare const BOOT_TIMEOUT_MS = 30000;
6
7
  export type Clock = {
@@ -15,11 +16,18 @@ export declare function parseWmSize(text: string): {
15
16
  } | undefined;
16
17
  /** `wm density`: `Physical density: 420` / `Override density: 320`; scale = dpi / 160. */
17
18
  export declare function parseWmDensity(text: string): number | undefined;
19
+ /**
20
+ * The probe for `serial`. A console `emulator-N` is an emulator by construction. Anything else (a phone, or a
21
+ * network emulator) also reads the qemu props, wakefulness and keyguard, in the same round trip.
22
+ */
23
+ export declare function bootProbeFor(serial: string): string;
18
24
  /** The sdk level on the probe's second line, if numeric. */
19
25
  export declare function sdkFromProbe(stdout: string): number | undefined;
26
+ /** `physical`: set only for a real phone (its qemu props are empty); an emulator, or an unprobed serial, has none. */
20
27
  export type BootInfo = {
21
28
  screen: ScreenInfo | undefined;
22
29
  sdk: number | undefined;
30
+ physical: PhoneFacts | undefined;
23
31
  };
24
32
  /** The screen out of a boot probe's output, if `wm` answered. */
25
33
  export declare function screenFromProbe(stdout: string): ScreenInfo | undefined;