@delacour/warden-e2e 0.6.0 → 0.8.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.
- package/{chunk-yb1hcjqm.mjs → chunk-vs5npy59.mjs} +3370 -218
- package/index.mjs +247 -16
- package/package.json +2 -2
- package/preload.mjs +8264 -2387
- package/types/core/src/a11y/a11y.config.d.ts +47 -0
- package/types/core/src/affected/affected.schema.d.ts +142 -1
- package/types/core/src/allocate.d.ts +2 -1
- package/types/core/src/android-keyguard.d.ts +36 -0
- package/types/core/src/batch/preset.schema.d.ts +13 -1
- package/types/core/src/biometrics.d.ts +15 -0
- package/types/core/src/builds/app-option.schema.d.ts +1 -1
- package/types/core/src/builds/builds.defaults.d.ts +15 -0
- package/types/core/src/builds/config.d.ts +5 -1
- package/types/core/src/builds/config.schema.d.ts +275 -2
- package/types/core/src/builds/define-config.d.ts +159 -0
- package/types/core/src/builds/install-device.d.ts +37 -0
- package/types/core/src/builds/install.d.ts +17 -3
- package/types/core/src/builds/kinds.d.ts +21 -3
- package/types/core/src/builds/lock.d.ts +38 -0
- package/types/core/src/builds/resolve.d.ts +12 -2
- package/types/core/src/camera-mode.d.ts +17 -0
- package/types/core/src/camera-placeholder.d.ts +9 -0
- package/types/core/src/camera-profile.d.ts +12 -0
- package/types/core/src/claim-errors.d.ts +34 -0
- package/types/core/src/conditions/a11y-keys.d.ts +45 -0
- package/types/core/src/conditions/camera-media.d.ts +30 -0
- package/types/core/src/conditions/conditions.config.d.ts +141 -0
- package/types/core/src/conditions/conditions.d.ts +22 -0
- package/types/core/src/conditions/conditions.types.d.ts +171 -0
- package/types/core/src/conditions/inject-state.d.ts +13 -0
- package/types/core/src/conditions/launch-settings.d.ts +14 -0
- package/types/core/src/conditions/network.presets.d.ts +42 -0
- package/types/core/src/conditions/text-size.d.ts +19 -0
- package/types/core/src/golden/android-biometrics.d.ts +35 -0
- package/types/core/src/ios-runner/build.d.ts +28 -0
- package/types/core/src/ios-runner/cache.d.ts +44 -0
- package/types/core/src/ios-runner/classify.d.ts +7 -0
- package/types/core/src/ios-runner/pack.d.ts +17 -0
- package/types/core/src/ios-runner/runner-build.types.d.ts +76 -0
- package/types/core/src/ios-runner/signing.d.ts +40 -0
- package/types/core/src/ios-runner/sim-build.d.ts +36 -0
- package/types/core/src/ios-runner/stage.d.ts +26 -0
- package/types/core/src/lease-events.d.ts +126 -0
- package/types/core/src/netproxy/control.d.ts +75 -0
- package/types/core/src/netproxy/decide.d.ts +22 -0
- package/types/core/src/netproxy/handle-file.d.ts +12 -0
- package/types/core/src/netproxy/lease-proxy.d.ts +53 -0
- package/types/core/src/netproxy/netproxy.main.d.ts +40 -0
- package/types/core/src/netproxy/pipe.d.ts +37 -0
- package/types/core/src/netproxy/proxy.d.ts +48 -0
- package/types/core/src/netproxy/shaping.d.ts +43 -0
- package/types/core/src/ownership.d.ts +44 -0
- package/types/core/src/providers/android.d.ts +46 -2
- package/types/core/src/providers/provider.types.d.ts +9 -0
- package/types/core/src/queue.d.ts +43 -0
- package/types/core/src/store.d.ts +61 -5
- package/types/core/src/types.d.ts +57 -0
- package/types/core/src/xcode.d.ts +22 -0
- package/types/e2e/src/a11y/a11y-target.d.ts +23 -0
- package/types/e2e/src/a11y/a11y.types.d.ts +81 -0
- package/types/e2e/src/a11y/announcements.d.ts +24 -0
- package/types/e2e/src/a11y/audit.d.ts +22 -0
- package/types/e2e/src/a11y/native/atf.d.ts +51 -0
- package/types/e2e/src/a11y/native/xcuitest.d.ts +60 -0
- package/types/e2e/src/a11y/order.d.ts +5 -0
- package/types/e2e/src/a11y/rules/duplicate-name.d.ts +6 -0
- package/types/e2e/src/a11y/rules/hidden-focusable.d.ts +3 -0
- package/types/e2e/src/a11y/rules/image-no-name.d.ts +6 -0
- package/types/e2e/src/a11y/rules/label-in-name.d.ts +7 -0
- package/types/e2e/src/a11y/rules/missing-name.d.ts +3 -0
- package/types/e2e/src/a11y/rules/role-in-name.d.ts +3 -0
- package/types/e2e/src/a11y/rules/rule.context.d.ts +22 -0
- package/types/e2e/src/a11y/rules/target-size.d.ts +21 -0
- package/types/e2e/src/a11y/rules/text-clipped.d.ts +9 -0
- package/types/e2e/src/a11y/speech.d.ts +26 -0
- package/types/e2e/src/a11y/talkback-log.d.ts +13 -0
- package/types/e2e/src/alert-buttons.d.ts +9 -0
- package/types/e2e/src/app.d.ts +3 -2
- package/types/e2e/src/conditions/condition-events.d.ts +68 -0
- package/types/e2e/src/conditions/device-conditions.d.ts +198 -0
- package/types/e2e/src/conditions/geo.d.ts +15 -0
- package/types/e2e/src/conditions/gpx.d.ts +8 -0
- package/types/e2e/src/conditions/lease-recorder.d.ts +27 -0
- package/types/e2e/src/conditions/lease-store.d.ts +9 -0
- package/types/e2e/src/conditions/location-plan.d.ts +17 -0
- package/types/e2e/src/conditions/location-scenarios.data.d.ts +17 -0
- package/types/e2e/src/conditions/route-ticker.d.ts +53 -0
- package/types/e2e/src/conditions/scheduler.d.ts +83 -0
- package/types/e2e/src/device-a11y.d.ts +97 -0
- package/types/e2e/src/device.d.ts +67 -4
- package/types/e2e/src/driver/a11y-events.d.ts +30 -0
- package/types/e2e/src/driver/a11y-read.d.ts +12 -0
- package/types/e2e/src/driver/android/android-animations.d.ts +7 -0
- package/types/e2e/src/driver/android/android-app.d.ts +4 -0
- package/types/e2e/src/driver/android/android-argv.d.ts +21 -0
- package/types/e2e/src/driver/android/android-camera-host.d.ts +23 -0
- package/types/e2e/src/driver/android/android-camera.d.ts +28 -0
- package/types/e2e/src/driver/android/android-clock.d.ts +28 -0
- package/types/e2e/src/driver/android/android-device.d.ts +8 -0
- package/types/e2e/src/driver/android/android-driver.d.ts +22 -0
- package/types/e2e/src/driver/android/android-network.d.ts +66 -0
- package/types/e2e/src/driver/android/android-phone.d.ts +47 -0
- package/types/e2e/src/driver/android/android-screen-reader.d.ts +23 -0
- package/types/e2e/src/driver/android/android-settings.d.ts +41 -0
- package/types/e2e/src/driver/android/android-spoken.d.ts +18 -0
- package/types/e2e/src/driver/android/android-state.d.ts +10 -0
- package/types/e2e/src/driver/android/android-talkback.d.ts +57 -0
- package/types/e2e/src/driver/android/atf-device.d.ts +23 -0
- package/types/e2e/src/driver/android/atf-dex.generated.d.ts +7 -0
- package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +5 -5
- package/types/e2e/src/driver/android/uiad-ops.d.ts +27 -1
- package/types/e2e/src/driver/android/uiad-protocol.d.ts +8 -0
- package/types/e2e/src/driver/android/uiad-screen-reader.d.ts +33 -0
- package/types/e2e/src/driver/clock-recorder.d.ts +9 -0
- package/types/e2e/src/driver/device-settings.d.ts +10 -0
- package/types/e2e/src/driver/driver.types.d.ts +242 -1
- package/types/e2e/src/driver/element-actions.d.ts +16 -0
- package/types/e2e/src/driver/ios/device/capabilities.d.ts +3 -0
- package/types/e2e/src/driver/ios/device/device-driver.d.ts +49 -0
- package/types/e2e/src/driver/ios/device/devicectl.d.ts +38 -0
- package/types/e2e/src/driver/ios/device/runner-protocol.d.ts +21 -0
- package/types/e2e/src/driver/ios/hid-recovery.d.ts +15 -0
- package/types/e2e/src/driver/ios/inject.d.ts +124 -0
- package/types/e2e/src/driver/ios/ios-a11y.d.ts +73 -0
- package/types/e2e/src/driver/ios/ios-audit.d.ts +36 -0
- package/types/e2e/src/driver/ios/ios-driver.d.ts +50 -1
- package/types/e2e/src/driver/ios/ios-network.d.ts +51 -0
- package/types/e2e/src/driver/ios/ios-settings.d.ts +44 -0
- package/types/e2e/src/driver/ios/sim-runner.d.ts +41 -0
- package/types/e2e/src/driver/ios/simctl.d.ts +41 -1
- package/types/e2e/src/driver/ios/simd-transport.d.ts +11 -2
- package/types/e2e/src/driver/ios/type-budget.d.ts +6 -1
- package/types/e2e/src/driver/native-audit.d.ts +26 -0
- package/types/e2e/src/driver/network-proxy.d.ts +52 -0
- package/types/e2e/src/driver/open.d.ts +20 -0
- package/types/e2e/src/driver/physical-conditions.d.ts +11 -0
- package/types/e2e/src/driver/requirements.d.ts +14 -0
- package/types/e2e/src/driver/screen-reader.d.ts +26 -0
- package/types/e2e/src/driver/typing.d.ts +1 -1
- package/types/e2e/src/env.d.ts +47 -0
- package/types/e2e/src/expect/a11y-matchers.d.ts +33 -0
- package/types/e2e/src/expect/expect.d.ts +7 -2
- package/types/e2e/src/expect/matchers.d.ts +14 -0
- package/types/e2e/src/fixture-session.d.ts +4 -0
- package/types/e2e/src/fixtures.d.ts +6 -1
- package/types/e2e/src/index.d.ts +6 -0
- package/types/e2e/src/runner/callsite.d.ts +3 -1
- package/types/e2e/src/runner/scope.d.ts +1 -0
- package/types/e2e/src/runner/test.d.ts +6 -0
- package/types/e2e/src/screen/render.d.ts +8 -0
- package/types/e2e/src/traced-device.d.ts +2 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { CameraCondition, ClockCondition, HostFault, LocationCondition, NetworkCondition, PermissionName, PermissionState, TextSize } from "../../../core/src/conditions/conditions.types";
|
|
1
2
|
import type { AsyncResult } from "../../../types/src/result";
|
|
3
|
+
import type { ScreenReaderState } from "../a11y/a11y.types";
|
|
2
4
|
/**
|
|
3
5
|
* The device driver contract every layer of warden-e2e builds on: the spec API (`screen`, `device`,
|
|
4
6
|
* `app`), `warden ui`, specs and the devtools facet all talk to a `Driver`. Implementations:
|
|
@@ -8,7 +10,7 @@ import type { AsyncResult } from "../../../types/src/result";
|
|
|
8
10
|
* Frozen interface — change it only together with `docs/plans/wdn-e2e.md`.
|
|
9
11
|
*/
|
|
10
12
|
export type Platform = "ios" | "android";
|
|
11
|
-
export type DriverErrorCode = "E_HELPER_MISSING" | "E_HELPER_CRASHED" | "E_TIMEOUT" | "E_NOT_BOOTED" | "E_UNKNOWN_DEVICE" | "E_AX_UNAVAILABLE" | "E_APP_UNAVAILABLE" | "E_APP_NOT_RESPONDING" | "E_NO_ELEMENT" | "E_STALE_REF" | "E_UNSUPPORTED" | "E_UNSUPPORTED_TEXT" | "E_HID" | "E_IO" | "E_BAD_REQUEST" | "E_INTERNAL";
|
|
13
|
+
export type DriverErrorCode = "E_HELPER_MISSING" | "E_HELPER_CRASHED" | "E_TIMEOUT" | "E_NOT_BOOTED" | "E_UNKNOWN_DEVICE" | "E_AX_UNAVAILABLE" | "E_APP_UNAVAILABLE" | "E_APP_NOT_RESPONDING" | "E_NO_ELEMENT" | "E_STALE_REF" | "E_UNSUPPORTED" | "E_UNSUPPORTED_TEXT" | "E_HID" | "E_IO" | "E_BAD_REQUEST" | "E_INTERNAL" | "E_DEVICE_LOCKED" | "E_REFUSED" | "E_NETWORK_RELAUNCH" | "E_CAMERA_DEVICE";
|
|
12
14
|
export type DriverError = {
|
|
13
15
|
code: DriverErrorCode;
|
|
14
16
|
message: string;
|
|
@@ -84,9 +86,32 @@ export type Element = {
|
|
|
84
86
|
/** where to tap: iOS `interactable.actionable.at`, else the frame centre */
|
|
85
87
|
tapPoint?: Point;
|
|
86
88
|
pid?: number;
|
|
89
|
+
/** screen-reader facts beyond the base tree; absent when the backend can't read them */
|
|
90
|
+
a11y?: ElementA11y;
|
|
87
91
|
depth: number;
|
|
88
92
|
children: Element[];
|
|
89
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* Screen-reader facts. Android: from warden-uiad's `AccessibilityNodeInfo`. iOS: from the ios-inject UIView walk,
|
|
96
|
+
* merged by identifier / frame when an a11y read asks for it.
|
|
97
|
+
*/
|
|
98
|
+
export type ElementA11y = {
|
|
99
|
+
/** iOS accessibilityHint / RN accessibilityHint */
|
|
100
|
+
hint?: string;
|
|
101
|
+
/** iOS custom action names / Android node action labels (standard actions left out) */
|
|
102
|
+
actions?: readonly string[];
|
|
103
|
+
/** reachable by VoiceOver / TalkBack navigation */
|
|
104
|
+
focusable?: boolean;
|
|
105
|
+
/** iOS isAccessibilityElement / Android importantForAccessibility; `false` = skipped by screen readers */
|
|
106
|
+
important?: boolean;
|
|
107
|
+
/** Android: holds TalkBack's accessibility focus */
|
|
108
|
+
a11yFocused?: boolean;
|
|
109
|
+
liveRegion?: "polite" | "assertive";
|
|
110
|
+
/** iOS: index in the container's `accessibilityElements` order, when the app sets one */
|
|
111
|
+
order?: number;
|
|
112
|
+
/** iOS: position in the injected walk, which follows VoiceOver's own traversal across the whole screen */
|
|
113
|
+
traversal?: number;
|
|
114
|
+
};
|
|
90
115
|
export type SnapshotBackend = "axbridge" | "ax" | "uiautomator";
|
|
91
116
|
export type ScreenInfo = {
|
|
92
117
|
width: number;
|
|
@@ -114,6 +139,13 @@ export type Orientation = "portrait" | "portraitUpsideDown" | "landscapeLeft" |
|
|
|
114
139
|
export type HardwareButton = "home" | "back" | "lock" | "volumeUp" | "volumeDown" | "shake";
|
|
115
140
|
export type KeyName = "Enter" | "Backspace" | "Tab" | "Space" | "Escape" | "Delete" | "Up" | "Down" | "Left" | "Right";
|
|
116
141
|
export type Modifier = "shift" | "cmd" | "alt" | "ctrl";
|
|
142
|
+
/** What `Driver.biometrics` does: read the enrollment, (un)enroll, or present a matching / non-matching biometric. */
|
|
143
|
+
export type BiometricOp = "status" | "enroll" | "unenroll" | "match" | "fail";
|
|
144
|
+
/** `modality`: what the device scans, iOS Face ID (`face`) or Touch ID and Android (`fingerprint`). */
|
|
145
|
+
export type BiometricState = {
|
|
146
|
+
enrolled: boolean;
|
|
147
|
+
modality: "face" | "fingerprint";
|
|
148
|
+
};
|
|
117
149
|
export type AlertInfo = {
|
|
118
150
|
title?: string;
|
|
119
151
|
message?: string;
|
|
@@ -136,6 +168,19 @@ export type LaunchOptions = {
|
|
|
136
168
|
env?: Record<string, string>;
|
|
137
169
|
/** iOS: `--terminate-running-process`; Android: `am force-stop` first. Default true. */
|
|
138
170
|
relaunch?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* iOS: load the warden network dylib even though no condition is set yet, so `network()` can take effect later
|
|
173
|
+
* without a relaunch (a suite that declares `conditions.network` launches this way). Conditions set before the
|
|
174
|
+
* launch inject on their own, a plain launch pays nothing. Ignored on Android.
|
|
175
|
+
*/
|
|
176
|
+
inject?: boolean;
|
|
177
|
+
};
|
|
178
|
+
/** Options of `Driver.network`. */
|
|
179
|
+
export type NetworkOptions = {
|
|
180
|
+
/** iOS: the app to check for the dylib (default: the last app this driver launched) */
|
|
181
|
+
bundleId?: string;
|
|
182
|
+
/** iOS: relaunch a running app that lacks the dylib instead of failing with `E_NETWORK_RELAUNCH` */
|
|
183
|
+
relaunch?: boolean;
|
|
139
184
|
};
|
|
140
185
|
export type DriverCapabilities = {
|
|
141
186
|
/** two-finger `pinch` and `rotate` */
|
|
@@ -225,7 +270,203 @@ export interface Driver {
|
|
|
225
270
|
alert(): DriverResult<AlertInfo | null>;
|
|
226
271
|
appearance(set?: "light" | "dark"): DriverResult<"light" | "dark">;
|
|
227
272
|
clipboard(set?: string): DriverResult<string>;
|
|
273
|
+
/**
|
|
274
|
+
* Put the device's network into `op` (offline / online / a preset / custom speed and latency) and return what
|
|
275
|
+
* is now in effect. Android emulator: native, verified through `dumpsys connectivity`. iOS: per app, the app
|
|
276
|
+
* is launched with a dylib that routes it through the lease proxy (`E_NETWORK_RELAUNCH` when it runs without).
|
|
277
|
+
* `flap` alternates the base with offline in this process (`conditions/scheduler.ts`) until the next network op
|
|
278
|
+
* or `close()` (Android; iOS goes through the scheduler directly).
|
|
279
|
+
*/
|
|
280
|
+
network(op: NetworkCondition, opts?: NetworkOptions): DriverResult<NetworkState>;
|
|
281
|
+
/**
|
|
282
|
+
* Replace the per-host faults (status / reset / delay / blackhole by host glob). Both platforms route the app
|
|
283
|
+
* through the lease's shaping proxy for this. `notes` carries what the proxy cannot do as written (an https://
|
|
284
|
+
* request matching a `status` fault is reset, the proxy does not decrypt TLS). `[]` clears them.
|
|
285
|
+
*/
|
|
286
|
+
networkFaults(faults: readonly HostFault[]): DriverResult<{
|
|
287
|
+
notes: readonly string[];
|
|
288
|
+
}>;
|
|
289
|
+
/**
|
|
290
|
+
* Simulated location: a point, a route (waypoints or GPX, played at `speedMps`), a bundled scenario, or clear.
|
|
291
|
+
* iOS: `simctl location` (the sim plays routes itself, no loop). Android: emulator `geo fix`, routes ticked by
|
|
292
|
+
* warden until the next op or `close()`; `clear` keeps the last fix. Physical devices: `E_UNSUPPORTED`.
|
|
293
|
+
*/
|
|
294
|
+
location(op: LocationCondition): DriverResult<void>;
|
|
295
|
+
/**
|
|
296
|
+
* Mock the device's wall clock (monotonic clocks are never touched). `offset` shifts it from the real time,
|
|
297
|
+
* `fixed` sets it to an instant and lets it run, `frozen` holds it there, `real` puts it back. Android emulator:
|
|
298
|
+
* system time (`settings put global auto_time 0` + `cmd alarm set-time`), device-wide; no `frozen`
|
|
299
|
+
* (`E_UNSUPPORTED`). iOS: the injected dylib shifts or holds the clocks of the injected app only (live, no
|
|
300
|
+
* relaunch); an app without the dylib gets it from the next launch (`appliesOnLaunch`), or from a relaunch with
|
|
301
|
+
* `relaunch: true`. Large offsets break TLS validity (HTTPS fails once the clock passes a certificate's dates).
|
|
302
|
+
*/
|
|
303
|
+
clock(op: ClockCondition, opts?: NetworkOptions): DriverResult<ClockState>;
|
|
304
|
+
/**
|
|
305
|
+
* Set runtime permissions of an app, per name (`location`, `camera`, `photos`, `notifications`, ...): `granted`,
|
|
306
|
+
* `denied` (revoked) or `unset` (back to asking). Names a platform has no service for are listed in `skipped`;
|
|
307
|
+
* when none applies it is `E_UNSUPPORTED`. iOS: `simctl privacy` (terminates a running app). Android: `pm
|
|
308
|
+
* grant|revoke` of the permissions the app declares. Apply before the first launch.
|
|
309
|
+
*/
|
|
310
|
+
permissions(grants: PermissionGrants, opts?: SettingOptions): DriverResult<PermissionsState>;
|
|
311
|
+
/**
|
|
312
|
+
* Device timezone (IANA name); `null` puts it back. Android: `cmd alarm set-timezone`, device-wide, live. iOS: a
|
|
313
|
+
* simulator has none per device, so it is staged and the app launched next gets it as `TZ` (`appliesOnLaunch`).
|
|
314
|
+
* `previous` is the Android timezone to restore on `null` (default: the host's).
|
|
315
|
+
*/
|
|
316
|
+
timezone(tz: string | null, opts?: {
|
|
317
|
+
previous?: string;
|
|
318
|
+
}): DriverResult<TimezoneState>;
|
|
319
|
+
/**
|
|
320
|
+
* Locale of an app (BCP 47, `fr-FR`); `null` puts it back. Android 13+: per-app locale (`cmd locale
|
|
321
|
+
* set-app-locales`), live. iOS: staged for the next launch as `-AppleLanguages` / `-AppleLocale`.
|
|
322
|
+
*/
|
|
323
|
+
locale(tag: string | null, opts?: SettingOptions): DriverResult<LocaleState>;
|
|
324
|
+
/**
|
|
325
|
+
* Accessibility settings; `null` puts them back. iOS: `simctl ui content_size` / `increase_contrast`, and
|
|
326
|
+
* `simctl spawn defaults write com.apple.Accessibility <Key>` (live) for reduce motion, bold text, reduce
|
|
327
|
+
* transparency, button shapes, differentiate without colour, on/off labels and the fake VoiceOver flag.
|
|
328
|
+
* Android: `font_scale`, `font_weight_adjustment`, `secure high_text_contrast_enabled` /
|
|
329
|
+
* `accessibility_display_inversion_enabled`, and reduce motion as `transition_animation_scale` 0 (what RN reads;
|
|
330
|
+
* off by default). No contrast switch on Android, no TalkBack until A11Y-7: `skipped` with the reason.
|
|
331
|
+
*/
|
|
332
|
+
a11y(op: A11yOp | null): DriverResult<A11yState>;
|
|
333
|
+
/**
|
|
334
|
+
* Battery level and charging; `null` puts the real battery back. Android emulator: `dumpsys battery`, seen by the
|
|
335
|
+
* OS and the app. iOS: status bar override only (`cosmetic: true`): the app still reads the real battery.
|
|
336
|
+
*/
|
|
337
|
+
battery(op: BatteryOp | null): DriverResult<BatteryState>;
|
|
338
|
+
/** Android: `am send-trim-memory RUNNING_CRITICAL` to the running app. iOS: `E_UNSUPPORTED` (no way verified to reach an app). */
|
|
339
|
+
memoryWarning(opts?: SettingOptions): DriverResult<void>;
|
|
340
|
+
/** iOS: `simctl push` of an APNs payload (needs `aps`) to the app. Android: `E_UNSUPPORTED`. */
|
|
341
|
+
push(payload: PushPayload, opts?: SettingOptions): DriverResult<void>;
|
|
342
|
+
/** Simulated biometrics (simulators / emulators only, else `E_UNSUPPORTED`); answers the state after the op. */
|
|
343
|
+
biometrics(op: BiometricOp): DriverResult<BiometricState>;
|
|
344
|
+
/**
|
|
345
|
+
* The simulated camera: `op` is the whole next state (a facing left out has no feed), `null` takes every feed off.
|
|
346
|
+
* Feeds are staged copies (`stageCameraFeed`). iOS: the injected dylib fakes the AVFoundation capture stack of the
|
|
347
|
+
* injected app (live; an app launched without the dylib gets it from the next launch, `appliesOnLaunch`). Android
|
|
348
|
+
* emulator: the emulator's `imagefile:` / `videofile:` camera. Physical devices: `E_UNSUPPORTED`.
|
|
349
|
+
*/
|
|
350
|
+
camera(op: CameraCondition | null, opts?: SettingOptions): DriverResult<CameraState>;
|
|
228
351
|
readonly app: AppCommands;
|
|
229
352
|
close(): Promise<void>;
|
|
230
353
|
}
|
|
354
|
+
/** Parts of a network condition no native mechanism does; the per-lease shaping proxy (COND-4) carries them. */
|
|
355
|
+
export type NetworkProxyFeature = "loss";
|
|
356
|
+
/**
|
|
357
|
+
* What `Driver.network` left on the device. `online` is what the OS reports (verified, not assumed);
|
|
358
|
+
* `needsProxy` lists the asked-for parts that were NOT applied because they need the shaping proxy.
|
|
359
|
+
*/
|
|
360
|
+
export type NetworkState = {
|
|
361
|
+
online: boolean;
|
|
362
|
+
condition: NetworkCondition;
|
|
363
|
+
needsProxy: readonly NetworkProxyFeature[];
|
|
364
|
+
/**
|
|
365
|
+
* iOS: the condition is staged (proxy configured, state written) but no running app carries the dylib yet; it
|
|
366
|
+
* reaches the app launched next. Absent = in effect now.
|
|
367
|
+
*/
|
|
368
|
+
appliesOnLaunch?: true;
|
|
369
|
+
};
|
|
370
|
+
/** The app a setting is for (default: the last app this driver launched, else the one the session names). */
|
|
371
|
+
export type SettingOptions = {
|
|
372
|
+
bundleId?: string;
|
|
373
|
+
};
|
|
374
|
+
export type PermissionGrants = Partial<Record<PermissionName, PermissionState>>;
|
|
375
|
+
export type PermissionsState = {
|
|
376
|
+
bundleId: string;
|
|
377
|
+
applied: readonly {
|
|
378
|
+
permission: PermissionName;
|
|
379
|
+
state: PermissionState;
|
|
380
|
+
}[];
|
|
381
|
+
/** names this platform has no service for, with why */
|
|
382
|
+
skipped: readonly {
|
|
383
|
+
permission: PermissionName;
|
|
384
|
+
reason: string;
|
|
385
|
+
}[];
|
|
386
|
+
};
|
|
387
|
+
export type TimezoneState = {
|
|
388
|
+
/** the timezone in effect (or staged), absent after a reset on iOS */
|
|
389
|
+
timezone?: string;
|
|
390
|
+
/** Android: the timezone before this change */
|
|
391
|
+
previous?: string;
|
|
392
|
+
/** iOS: staged for the next launch */
|
|
393
|
+
appliesOnLaunch?: true;
|
|
394
|
+
};
|
|
395
|
+
export type LocaleState = {
|
|
396
|
+
locale?: string;
|
|
397
|
+
bundleId?: string;
|
|
398
|
+
appliesOnLaunch?: true;
|
|
399
|
+
};
|
|
400
|
+
export type A11yOp = {
|
|
401
|
+
textSize?: TextSize;
|
|
402
|
+
boldText?: boolean;
|
|
403
|
+
contrast?: "increased" | "normal";
|
|
404
|
+
reduceMotion?: boolean;
|
|
405
|
+
/** iOS: Reduce Transparency */
|
|
406
|
+
reduceTransparency?: boolean;
|
|
407
|
+
/** iOS: Button Shapes */
|
|
408
|
+
buttonShapes?: boolean;
|
|
409
|
+
/** iOS: Differentiate Without Color */
|
|
410
|
+
differentiateWithoutColor?: boolean;
|
|
411
|
+
/** iOS: On/Off Labels */
|
|
412
|
+
onOffLabels?: boolean;
|
|
413
|
+
/** Android: High contrast text */
|
|
414
|
+
highContrastText?: boolean;
|
|
415
|
+
/** Android: colour inversion */
|
|
416
|
+
colorInversion?: boolean;
|
|
417
|
+
/** iOS: fake VoiceOver flag (`mode: "simulated"`); Android: real TalkBack (A11Y-7) */
|
|
418
|
+
screenReader?: boolean;
|
|
419
|
+
};
|
|
420
|
+
export type A11yState = {
|
|
421
|
+
applied: readonly string[];
|
|
422
|
+
/** asked for but this platform has no mechanism that works, with why */
|
|
423
|
+
skipped: readonly {
|
|
424
|
+
setting: string;
|
|
425
|
+
reason: string;
|
|
426
|
+
}[];
|
|
427
|
+
/** the Android font scale was clamped to the platform's range */
|
|
428
|
+
notes: readonly string[];
|
|
429
|
+
/** present when the op asked for `screenReader` and it was applied */
|
|
430
|
+
screenReader?: ScreenReaderState;
|
|
431
|
+
/** Android: the accessibility services / master switch the device had before TalkBack was turned on (`null` = unset), for the lease's condition record */
|
|
432
|
+
screenReaderPrevious?: {
|
|
433
|
+
services: string | null;
|
|
434
|
+
enabled: string | null;
|
|
435
|
+
};
|
|
436
|
+
};
|
|
437
|
+
export type BatteryOp = {
|
|
438
|
+
level?: number;
|
|
439
|
+
charging?: boolean;
|
|
440
|
+
};
|
|
441
|
+
export type BatteryState = {
|
|
442
|
+
level?: number;
|
|
443
|
+
charging?: boolean;
|
|
444
|
+
cosmetic: boolean;
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* The feeds now playing. `appliesOnLaunch`: no running app has the camera yet, the next launch picks it up (iOS);
|
|
448
|
+
* `appliesOnCameraOpen`: an open camera keeps the old frames until the app opens the camera again (Android emulator,
|
|
449
|
+
* which reads the file per camera open); `rebooted`: the emulator restarted to switch its camera source (image ↔ video).
|
|
450
|
+
*/
|
|
451
|
+
export type CameraState = {
|
|
452
|
+
feeds: CameraCondition;
|
|
453
|
+
appliesOnLaunch?: boolean;
|
|
454
|
+
appliesOnCameraOpen?: boolean;
|
|
455
|
+
rebooted?: boolean;
|
|
456
|
+
};
|
|
457
|
+
/** An APNs payload: an object with an `aps` key (4096 bytes at most). */
|
|
458
|
+
export type PushPayload = {
|
|
459
|
+
aps: Record<string, unknown>;
|
|
460
|
+
} & Record<string, unknown>;
|
|
461
|
+
/** What `Driver.clock` left on the device. */
|
|
462
|
+
export type ClockState = {
|
|
463
|
+
condition: ClockCondition;
|
|
464
|
+
/** wall clock minus the host's, ms (0 for `real` and `frozen`) */
|
|
465
|
+
offsetMs: number;
|
|
466
|
+
frozen: boolean;
|
|
467
|
+
/** what the device's wall clock read when the op returned, ms since the epoch */
|
|
468
|
+
nowMs: number;
|
|
469
|
+
/** iOS: staged for the next launch of the app; absent = in effect now */
|
|
470
|
+
appliesOnLaunch?: true;
|
|
471
|
+
};
|
|
231
472
|
export type Logger = (line: string) => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Driver, DriverResult, Element } from "./driver.types";
|
|
2
|
+
/** The way content moves for a scroll: `down` reveals what is below (forward), `up` what is above (backward). */
|
|
3
|
+
export type ScrollDirection = "up" | "down" | "left" | "right";
|
|
4
|
+
/**
|
|
5
|
+
* Optional capability next to the frozen `Driver` interface (pattern: `SetsText`): act on a resolved element through
|
|
6
|
+
* the platform's accessibility actions instead of touches, for when a touch means something else. Android with
|
|
7
|
+
* TalkBack on: a tap only moves TalkBack's focus and a one-finger swipe navigates, so a locator `tap` / `swipe` asks
|
|
8
|
+
* here first. Each answers `true` when it did the action, `false` when the driver is not in that mode and the caller
|
|
9
|
+
* should send the touch as before. A raw coordinate tap never comes here: it stays a touch.
|
|
10
|
+
*/
|
|
11
|
+
export type ActsOnElements = {
|
|
12
|
+
pressElement(element: Element): DriverResult<boolean>;
|
|
13
|
+
/** `within`: the scrolling element; absent = the largest scrollable on screen */
|
|
14
|
+
scrollElement(direction: ScrollDirection, within?: Element): DriverResult<boolean>;
|
|
15
|
+
};
|
|
16
|
+
export declare function canActOnElements(driver: Driver): driver is Driver & ActsOnElements;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Exec } from "../../../../../core/src/exec";
|
|
2
|
+
import type { E2eEnv } from "../../../env";
|
|
3
|
+
import type { ProbesFrontmost, ReadsAlertOn } from "../../alerts";
|
|
4
|
+
import type { Driver, DriverResult, Logger } from "../../driver.types";
|
|
5
|
+
import { type SimdClient } from "../simd-client";
|
|
6
|
+
import { type Devicectl } from "./devicectl";
|
|
7
|
+
/**
|
|
8
|
+
* The `Driver` for a physical iPhone / iPad: input, accessibility and app lifecycle through `warden-runner` (the
|
|
9
|
+
* XCUITest server `warden __ios-runner-host` keeps running on the phone, spoken to over the host's unix socket in the
|
|
10
|
+
* `warden-simd` protocol, so the client, normalizer and alert reader are simd's), and `xcrun devicectl` for what the
|
|
11
|
+
* runner cannot do: install / uninstall / probe, orientation changes, appearance and pasteboard reads, the screenshot
|
|
12
|
+
* fallback. There is no select-all or paste (a field is cleared with counted Backspace), no native settle (snapshot
|
|
13
|
+
* hashes), no multi-touch, and nothing that changes a phone beyond the app under test: an uninstall or data wipe of
|
|
14
|
+
* a bundle warden did not install is `E_REFUSED`.
|
|
15
|
+
*/
|
|
16
|
+
export type IosDeviceEnv = Pick<E2eEnv, "iosRunnerSocket" | "appPath" | "bundleId">;
|
|
17
|
+
export type OpenIosDeviceDriverOptions = {
|
|
18
|
+
udid: string;
|
|
19
|
+
exec: Exec;
|
|
20
|
+
env?: Partial<IosDeviceEnv>;
|
|
21
|
+
/** the runner host's socket (wins over `env.iosRunnerSocket`) */
|
|
22
|
+
socketPath?: string;
|
|
23
|
+
/** an already connected client: used instead of connecting; the driver owns it from here */
|
|
24
|
+
client?: SimdClient;
|
|
25
|
+
/** `WARDEN_BUNDLE_ID`: the app under test, always allowed to be uninstalled / wiped */
|
|
26
|
+
bundleId?: string;
|
|
27
|
+
/** `WARDEN_APP_PATH`: what `clearData` reinstalls */
|
|
28
|
+
appPath?: string;
|
|
29
|
+
/** bundles warden installed on this phone earlier (the `installs` table) */
|
|
30
|
+
ownedBundles?: readonly string[];
|
|
31
|
+
log?: Logger;
|
|
32
|
+
now?: () => number;
|
|
33
|
+
sleep?: (ms: number) => Promise<void>;
|
|
34
|
+
defaultTimeoutMs?: number;
|
|
35
|
+
/** test seams */
|
|
36
|
+
devicectl?: Devicectl;
|
|
37
|
+
writeFile?: (path: string, bytes: Uint8Array) => Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
export type IosDeviceDriver = Driver & ReadsAlertOn & ProbesFrontmost;
|
|
40
|
+
export declare function createIosDeviceDriver(client: SimdClient, devicectl: Devicectl, opts: Omit<OpenIosDeviceDriverOptions, "client" | "exec" | "devicectl" | "env" | "socketPath"> & {
|
|
41
|
+
now: () => number;
|
|
42
|
+
sleep: (ms: number) => Promise<void>;
|
|
43
|
+
write: (path: string, bytes: Uint8Array) => Promise<void>;
|
|
44
|
+
}): IosDeviceDriver;
|
|
45
|
+
/**
|
|
46
|
+
* Open the driver for the phone `udid` against its runner host's socket (`socketPath` / `env.iosRunnerSocket` /
|
|
47
|
+
* `client`). `E_HELPER_MISSING` without any: the host is started by `warden ui` / `warden e2e`, never by the driver.
|
|
48
|
+
*/
|
|
49
|
+
export declare function openIosDeviceDriver(o: OpenIosDeviceDriverOptions): DriverResult<Driver>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Exec, ExecResult } from "../../../../../core/src/exec";
|
|
2
|
+
import type { DriverError, DriverResult, Orientation } from "../../driver.types";
|
|
3
|
+
export declare const installArgv: (udid: string, appPath: string, json: string) => string[];
|
|
4
|
+
export declare const uninstallArgv: (udid: string, bundleId: string, json: string) => string[];
|
|
5
|
+
/** `--include-default-apps`: system apps (Settings) are listed too, so a probe of one is not a false negative. */
|
|
6
|
+
export declare const appsArgv: (udid: string, bundleId: string, json: string) => string[];
|
|
7
|
+
export declare const appearanceArgv: (udid: string, json: string) => string[];
|
|
8
|
+
export declare const orientationArgv: (udid: string, orientation: Orientation) => string[];
|
|
9
|
+
export declare const screenshotArgv: (udid: string, destination: string) => string[];
|
|
10
|
+
export declare const pasteArgv: (udid: string) => string[];
|
|
11
|
+
export type DevicectlOptions = {
|
|
12
|
+
udid: string;
|
|
13
|
+
exec: Exec;
|
|
14
|
+
/** a fresh private directory for `--json-output` files and screenshots */
|
|
15
|
+
makeDir?: () => Promise<string>;
|
|
16
|
+
read?: (path: string) => Promise<string>;
|
|
17
|
+
readBytes?: (path: string) => Promise<Uint8Array>;
|
|
18
|
+
remove?: (dir: string) => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
export type Devicectl = {
|
|
21
|
+
install(appPath: string): DriverResult<{
|
|
22
|
+
bundleIds: string[];
|
|
23
|
+
}>;
|
|
24
|
+
uninstall(bundleId: string): DriverResult<void>;
|
|
25
|
+
isInstalled(bundleId: string): DriverResult<boolean>;
|
|
26
|
+
appearance(): DriverResult<"light" | "dark">;
|
|
27
|
+
setOrientation(orientation: Orientation): DriverResult<void>;
|
|
28
|
+
/** to `path` when given, else the PNG bytes */
|
|
29
|
+
screenshot(path?: string): DriverResult<{
|
|
30
|
+
path?: string;
|
|
31
|
+
png?: Uint8Array;
|
|
32
|
+
}>;
|
|
33
|
+
pasteboardCopy(text: string): DriverResult<void>;
|
|
34
|
+
pasteboardPaste(): DriverResult<string>;
|
|
35
|
+
};
|
|
36
|
+
/** A failed devicectl call: a locked phone cannot be retried, anything else is an I/O failure with devicectl's own words. */
|
|
37
|
+
export declare function devicectlError(what: string, result: ExecResult): DriverError;
|
|
38
|
+
export declare function createDevicectl(o: DevicectlOptions): Devicectl;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* What the `warden-runner` (XCUITest) wire adds to the `warden-simd` protocol of `simd-protocol.ts` (the shared
|
|
4
|
+
* schemas and `SimdClient` are reused unchanged): the `appState` op, and the facts about the runner the driver acts on.
|
|
5
|
+
* The runner's own contract is `native/ios-runner/AGENTS.md`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const appStateSchema: z.ZodEnum<{
|
|
8
|
+
unknown: "unknown";
|
|
9
|
+
notRunning: "notRunning";
|
|
10
|
+
runningBackgroundSuspended: "runningBackgroundSuspended";
|
|
11
|
+
runningBackground: "runningBackground";
|
|
12
|
+
runningForeground: "runningForeground";
|
|
13
|
+
}>;
|
|
14
|
+
export type AppState = z.infer<typeof appStateSchema>;
|
|
15
|
+
export declare const appStateDataSchema: z.ZodObject<{
|
|
16
|
+
state: z.ZodString;
|
|
17
|
+
}, z.core.$loose>;
|
|
18
|
+
/** `appState` reply data, or undefined when it is not shaped like one. */
|
|
19
|
+
export declare function parseAppState(data: unknown): AppState | undefined;
|
|
20
|
+
/** The app is alive in some form (a suspended background app still has its process). */
|
|
21
|
+
export declare function isRunning(state: AppState): boolean;
|
|
@@ -23,6 +23,21 @@ export declare function stopResidentHelper(deps: HidRecoveryDeps, socketPath: st
|
|
|
23
23
|
* the next call then fails `E_NOT_BOOTED`.
|
|
24
24
|
*/
|
|
25
25
|
export declare function isHidLost(error: unknown): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The error of a call on a physical iPhone whose runner host is gone (the host or the runner exited, or the socket
|
|
28
|
+
* closed): the driver's `E_HELPER_CRASHED`, however the error was wrapped.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isRunnerLost(error: unknown): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether a failed attempt is worth one recovery and the test running again: a simulator that lost its HID connection
|
|
33
|
+
* (reboot), or a physical iPhone that lost its runner host (restart the host; the phone is never rebooted).
|
|
34
|
+
*/
|
|
35
|
+
export declare function isRecoverableLoss(error: unknown, deviceKind: "physical" | undefined): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Restarts a physical iPhone's runner host (`WARDEN_IOS_RUNNER_RESTART`, `{udid}` replaced): stop the host, start a
|
|
38
|
+
* new one on the same socket and wait until it answers. Never touches the phone itself (no reboot, no `simctl`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function restartRunnerHost(deps: HidRecoveryDeps, argv: readonly string[] | undefined, udid: string): AsyncResult<void>;
|
|
26
41
|
/**
|
|
27
42
|
* Shuts the sim down and boots it again: `bootstatus -b` boots a shut-down sim itself and waits for it, and opens
|
|
28
43
|
* no Simulator.app window (the same windowless path warden claims use). The caller reconnects the helper and
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { CameraCondition } from "../../../../core/src/conditions/conditions.types";
|
|
2
|
+
import type { Result } from "../../../../types/src/result";
|
|
3
|
+
import type { DriverError } from "../driver.types";
|
|
4
|
+
import type { ProxyAddress } from "../network-proxy";
|
|
5
|
+
/**
|
|
6
|
+
* The injected dylib for iOS simulator apps (`native/ios-inject`): routes NSURLSession through the lease's shaping
|
|
7
|
+
* proxy and reports offline / a clock offset to the app, live, through a state file the dylib watches. This module is
|
|
8
|
+
* the host side: where the dylib is, the launch env that loads it, and the state file protocol. COND-12 (mock clock)
|
|
9
|
+
* reuses all of it and only sets `clockOffsetMs` (shift) or `clockFrozenMs` (hold).
|
|
10
|
+
*/
|
|
11
|
+
export declare const INJECT_DYLIB = "libwarden-inject.dylib";
|
|
12
|
+
export declare const INJECT_DYLIB_ENV = "WARDEN_INJECT_DYLIB";
|
|
13
|
+
/**
|
|
14
|
+
* The one message of the control channel. `seq` rises with every write (the dylib ignores a state whose `seq` it
|
|
15
|
+
* already applied); a field left out means "off" (`offline` false, `clockOffsetMs` 0), so a message is always the
|
|
16
|
+
* whole state, never a delta. `clockFrozenMs` (ms since the epoch) holds the wall clocks at one instant and wins over
|
|
17
|
+
* `clockOffsetMs`; left out, they run. `camera`: the feeds of an app launched with `WARDEN_CAMERA=1` (a facing left out
|
|
18
|
+
* has no camera); left out, no feeds.
|
|
19
|
+
*/
|
|
20
|
+
export type InjectState = {
|
|
21
|
+
seq: number;
|
|
22
|
+
offline?: boolean;
|
|
23
|
+
clockOffsetMs?: number;
|
|
24
|
+
clockFrozenMs?: number;
|
|
25
|
+
/** one-shot request (A11Y-1): walk the accessibility tree and write `<state>.a11y` before acknowledging this `seq` */
|
|
26
|
+
a11yRead?: boolean;
|
|
27
|
+
camera?: CameraCondition;
|
|
28
|
+
};
|
|
29
|
+
/** What the dylib answers in `<state>.ack` once it applied `seq` (`bundleId`: the app it loaded into; `camera`: launched with `WARDEN_CAMERA=1`). */
|
|
30
|
+
export type InjectAck = {
|
|
31
|
+
seq: number;
|
|
32
|
+
pid: number;
|
|
33
|
+
proxy?: boolean;
|
|
34
|
+
camera?: boolean;
|
|
35
|
+
bundleId?: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function encodeInjectState(state: InjectState): string;
|
|
38
|
+
export declare function parseInjectState(text: string): InjectState | undefined;
|
|
39
|
+
export declare function parseInjectAck(text: string): InjectAck | undefined;
|
|
40
|
+
/** 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). */
|
|
41
|
+
export declare const injectStatePath: (wardenHome: string, udid: string) => string;
|
|
42
|
+
/** Where the dylib writes an accessibility read (`a11yRead`): next to the state file. */
|
|
43
|
+
export declare const injectA11yPath: (statePath: string) => string;
|
|
44
|
+
/** Where the dylib appends accessibility events (A11Y-8): JSON lines next to the state file. */
|
|
45
|
+
export declare const injectEventsPath: (statePath: string) => string;
|
|
46
|
+
export declare const injectAckPath: (statePath: string) => string;
|
|
47
|
+
/**
|
|
48
|
+
* Launch env that loads the dylib. `proxy` is fixed for the life of the process (the lease's proxy keeps its port
|
|
49
|
+
* for the whole lease); everything that changes later goes through the state file.
|
|
50
|
+
*/
|
|
51
|
+
export declare function injectLaunchEnv(input: {
|
|
52
|
+
dylib: string;
|
|
53
|
+
statePath: string;
|
|
54
|
+
proxy?: ProxyAddress;
|
|
55
|
+
clockOffsetMs?: number;
|
|
56
|
+
clockFrozenMs?: number;
|
|
57
|
+
/** fake the capture stack from the state file's `camera` feeds (`WARDEN_CAMERA=1`) */
|
|
58
|
+
camera?: boolean;
|
|
59
|
+
debug?: boolean;
|
|
60
|
+
}): Record<string, string>;
|
|
61
|
+
/**
|
|
62
|
+
* Whether a launch loads the dylib. Never without a reason: a run with no condition keeps the plain launch (no
|
|
63
|
+
* dyld insertion, no watcher thread, no proxy hop).
|
|
64
|
+
*/
|
|
65
|
+
export declare function shouldInject(input: {
|
|
66
|
+
/** the lease has a live shaping proxy, or a non-trivial network condition was set */
|
|
67
|
+
networkActive: boolean;
|
|
68
|
+
/** a clock offset is set (COND-12) */
|
|
69
|
+
clockActive: boolean;
|
|
70
|
+
/** the lease has a camera feed: the dylib must be loaded at launch to fake the capture stack */
|
|
71
|
+
cameraActive?: boolean;
|
|
72
|
+
/** the caller asked for injection (`LaunchOptions.inject`, a suite that declares `conditions.network`) */
|
|
73
|
+
requested: boolean;
|
|
74
|
+
}): boolean;
|
|
75
|
+
export type LocateInjectInput = {
|
|
76
|
+
/** `WARDEN_INJECT_DYLIB` (a file) and `WARDEN_HELPER_DIR` (a directory that also holds the dylib) */
|
|
77
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
78
|
+
execPath: string;
|
|
79
|
+
/** directory of this module (`packages/e2e/src/driver/ios`) */
|
|
80
|
+
importDir: string;
|
|
81
|
+
wardenHome: string;
|
|
82
|
+
version?: string;
|
|
83
|
+
exists: (path: string) => boolean;
|
|
84
|
+
realpath?: (path: string) => string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Find `libwarden-inject.dylib`. Order mirrors `locateHelper`: `WARDEN_INJECT_DYLIB` → `WARDEN_HELPER_DIR` → next to
|
|
88
|
+
* the running `warden` → `<packages/e2e>/native/dist` → `$WARDEN_HOME/simd/<version>`. A set variable that points at
|
|
89
|
+
* nothing is an error.
|
|
90
|
+
*/
|
|
91
|
+
export declare function locateInjectDylib(input: LocateInjectInput): Result<string, DriverError>;
|
|
92
|
+
/** File operations of the state channel, injected. */
|
|
93
|
+
export type InjectFs = {
|
|
94
|
+
mkdir(path: string): void;
|
|
95
|
+
/** write `text` to `path` so a reader sees all of it or none (tmp + rename) */
|
|
96
|
+
writeAtomic(path: string, text: string): void;
|
|
97
|
+
read(path: string): string | undefined;
|
|
98
|
+
remove(path: string): void;
|
|
99
|
+
/** is `pid` a live process */
|
|
100
|
+
pidAlive(pid: number): boolean;
|
|
101
|
+
};
|
|
102
|
+
export declare const nodeInjectFs: InjectFs;
|
|
103
|
+
/** The host end of the control channel for one device. */
|
|
104
|
+
export type InjectChannel = {
|
|
105
|
+
readonly statePath: string;
|
|
106
|
+
/** Last state written (read back from disk, so another warden process's writes count). */
|
|
107
|
+
current(): InjectState;
|
|
108
|
+
/** Write the whole next state (`seq` assigned here). */
|
|
109
|
+
write(next: Omit<InjectState, "seq">): InjectState;
|
|
110
|
+
/** The injected process that answered last, when it is still alive and is the app under test. */
|
|
111
|
+
attached(): InjectAck | undefined;
|
|
112
|
+
/** Wait until the app acknowledged `seq`; `false` on timeout. */
|
|
113
|
+
acked(seq: number, timeoutMs: number): Promise<boolean>;
|
|
114
|
+
/** Forget the state and ack of a previous app (before a launch). */
|
|
115
|
+
reset(): void;
|
|
116
|
+
};
|
|
117
|
+
export declare function createInjectChannel(input: {
|
|
118
|
+
statePath: string;
|
|
119
|
+
fs?: InjectFs;
|
|
120
|
+
now: () => number;
|
|
121
|
+
sleep: (ms: number) => Promise<void>;
|
|
122
|
+
/** the app under test; an ack from another app (another injected bundle still running) is ignored. None = any */
|
|
123
|
+
bundleId?: () => string | undefined;
|
|
124
|
+
}): InjectChannel;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Result } from "../../../../types/src/result";
|
|
2
|
+
import { type A11yEventsRead } from "../a11y-events";
|
|
3
|
+
import type { DriverError, DriverResult, LaunchOptions, Rect, Snapshot } from "../driver.types";
|
|
4
|
+
import { type InjectChannel, type InjectFs } from "./inject";
|
|
5
|
+
/**
|
|
6
|
+
* iOS screen-reader facts (WDN-A11Y, A11Y-1). `warden-simd` (idb) has no hint, custom actions, `isAccessibilityElement`
|
|
7
|
+
* or order, so they come from the injected dylib's UIKit walk (`native/ios-inject`, op `a11yRead` on the state/ack file
|
|
8
|
+
* channel of `inject.ts`) and are merged into a snapshot's elements. The dylib is loaded only when asked for (the
|
|
9
|
+
* driver option `a11y` or `WARDEN_A11Y=1`); an app launched without it answers `E_UNSUPPORTED` with that hint, never a
|
|
10
|
+
* fabricated value.
|
|
11
|
+
*/
|
|
12
|
+
/** One entry of the dylib's walk, traversal order. */
|
|
13
|
+
export type IosA11yNode = {
|
|
14
|
+
cls: string;
|
|
15
|
+
/** `isAccessibilityElement`: VoiceOver stops on it */
|
|
16
|
+
element: boolean;
|
|
17
|
+
label?: string;
|
|
18
|
+
hint?: string;
|
|
19
|
+
value?: string;
|
|
20
|
+
identifier?: string;
|
|
21
|
+
/** `UIAccessibilityTraits` bitmask (a hex string on the wire: bits above 2^53 do not survive JSON numbers) */
|
|
22
|
+
traits: bigint;
|
|
23
|
+
/** screen points */
|
|
24
|
+
frame: Rect;
|
|
25
|
+
/** `accessibilityCustomActions` names */
|
|
26
|
+
actions: readonly string[];
|
|
27
|
+
/** index in the container's `accessibilityElements`, only when the app set that array */
|
|
28
|
+
order?: number;
|
|
29
|
+
};
|
|
30
|
+
/** The dylib's `<state>.a11y` for request `req`, or why it is unusable (the message is for the user). */
|
|
31
|
+
export declare function parseA11yDump(text: string, req: number): Result<IosA11yNode[], string>;
|
|
32
|
+
/** Frames overlapping less than this (intersection over union) are not the same node. */
|
|
33
|
+
export declare const MIN_FRAME_OVERLAP = 0.6;
|
|
34
|
+
export type MergedA11y = {
|
|
35
|
+
snapshot: Snapshot;
|
|
36
|
+
matched: number;
|
|
37
|
+
unmatched: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Merge a walk into a snapshot (a new snapshot; the input is not touched). Rule, applied once per element in
|
|
41
|
+
* document order:
|
|
42
|
+
* 1. identifier: an element with an `identifier` takes the unused node with the same `id` (several: the larger frame
|
|
43
|
+
* overlap wins);
|
|
44
|
+
* 2. frame: every element still without a node takes the unused node with the largest frame overlap of at least
|
|
45
|
+
* {@link MIN_FRAME_OVERLAP}, greedily from the best pair down. A node whose `id` is set but different from the
|
|
46
|
+
* element's identifier never matches by frame.
|
|
47
|
+
* Each node serves one element. Elements with no node keep `a11y` as it was (never invented).
|
|
48
|
+
*/
|
|
49
|
+
export declare function mergeA11y(snapshot: Snapshot, nodes: readonly IosA11yNode[]): MergedA11y;
|
|
50
|
+
export type IosA11yDeps = {
|
|
51
|
+
channel: InjectChannel;
|
|
52
|
+
fs?: InjectFs;
|
|
53
|
+
/** the dylib path, resolved on first use so a run that never asks does not look for it */
|
|
54
|
+
dylib: () => Result<string, DriverError>;
|
|
55
|
+
/** was an a11y read asked for (driver option `a11y`, `WARDEN_A11Y=1`) */
|
|
56
|
+
requested: () => boolean;
|
|
57
|
+
debug?: boolean;
|
|
58
|
+
/** how long the app gets to answer a read (default {@link INJECT_ACK_MS}) */
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
/** how long a launch gets to load the dylib (default {@link INJECT_LOAD_MS}) */
|
|
61
|
+
loadMs?: number;
|
|
62
|
+
};
|
|
63
|
+
export type IosA11y = {
|
|
64
|
+
/** Env that loads the dylib (no proxy) when an a11y read was asked for; `undefined`: launch plainly. */
|
|
65
|
+
launchEnv(opts: LaunchOptions): Promise<Result<Record<string, string> | undefined, DriverError>>;
|
|
66
|
+
/** After a launch that carried the dylib: wait until it loaded. */
|
|
67
|
+
confirmLoaded(): DriverResult<void>;
|
|
68
|
+
/** Read the app's UIKit accessibility tree now and merge it into `snapshot`. */
|
|
69
|
+
read(snapshot: Snapshot): DriverResult<Snapshot>;
|
|
70
|
+
/** Announcements and screen/layout notifications the app posted since byte offset `since` (A11Y-8). */
|
|
71
|
+
events(since?: number): DriverResult<A11yEventsRead>;
|
|
72
|
+
};
|
|
73
|
+
export declare function createIosA11y(deps: IosA11yDeps): IosA11y;
|