@delacour/warden-e2e 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{chunk-tnr47n9c.mjs → chunk-e3cry5br.mjs} +828 -255
- package/index.mjs +144 -14
- package/package.json +2 -2
- package/preload.mjs +375 -63
- package/types/core/src/affected/affected.schema.d.ts +5 -0
- package/types/core/src/builds/builds.defaults.d.ts +8 -0
- package/types/core/src/builds/config.schema.d.ts +9 -0
- package/types/core/src/builds/define-config.d.ts +9 -1
- package/types/core/src/conditions/conditions.config.d.ts +6 -1
- package/types/core/src/conditions/conditions.types.d.ts +21 -0
- package/types/core/src/conditions/inject-state.d.ts +3 -2
- package/types/e2e/src/a11y/a11y-target.d.ts +2 -0
- package/types/e2e/src/conditions/condition-events.d.ts +6 -1
- package/types/e2e/src/conditions/device-conditions.d.ts +8 -2
- package/types/e2e/src/device.d.ts +10 -2
- package/types/e2e/src/driver/android/android-settings.d.ts +9 -2
- package/types/e2e/src/driver/ax-wedge.d.ts +38 -0
- package/types/e2e/src/driver/bridge-stall.d.ts +41 -0
- package/types/e2e/src/driver/driver.types.d.ts +35 -2
- package/types/e2e/src/driver/ios/inject.d.ts +17 -2
- package/types/e2e/src/driver/ios/ios-driver.d.ts +17 -2
- package/types/e2e/src/driver/ios/ios-keyboard.d.ts +39 -0
- package/types/e2e/src/driver/ios/simd-client.d.ts +2 -0
- package/types/e2e/src/driver/ios/simd-protocol.d.ts +129 -0
- package/types/e2e/src/driver/keyboard-state.d.ts +9 -0
- package/types/e2e/src/driver/physical-conditions.d.ts +1 -1
- package/types/e2e/src/driver/read-deadline.d.ts +4 -0
- package/types/e2e/src/driver/settle.d.ts +11 -1
- package/types/e2e/src/errors.d.ts +3 -1
- package/types/e2e/src/expect/a11y-matchers.d.ts +7 -0
- package/types/e2e/src/expect/matchers.d.ts +5 -0
- package/types/e2e/src/index.d.ts +2 -1
- package/types/e2e/src/runner/bridge-evidence.d.ts +12 -0
- package/types/e2e/src/runner/results.d.ts +29 -0
- package/types/e2e/src/screen/actions.d.ts +7 -0
- package/types/e2e/src/screen/context.d.ts +11 -12
- package/types/e2e/src/screen/degraded-read.d.ts +23 -0
- package/types/e2e/src/screen/keyboard-cover.d.ts +26 -0
- package/types/e2e/src/screen/locator.d.ts +61 -7
- package/types/e2e/src/screen/react-locator.d.ts +17 -2
- package/types/e2e/src/screen/react-merge.d.ts +10 -0
- package/types/e2e/src/session.d.ts +6 -0
|
@@ -167,6 +167,7 @@ export declare const e2eSuiteSchema: z.ZodObject<{
|
|
|
167
167
|
except: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
168
168
|
keep: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
169
169
|
}, z.core.$strict>]>>;
|
|
170
|
+
golden: z.ZodOptional<z.ZodBoolean>;
|
|
170
171
|
biometrics: z.ZodOptional<z.ZodBoolean>;
|
|
171
172
|
reclaim: z.ZodOptional<z.ZodEnum<{
|
|
172
173
|
none: "none";
|
|
@@ -262,6 +263,10 @@ export declare const e2eSuiteSchema: z.ZodObject<{
|
|
|
262
263
|
}>>;
|
|
263
264
|
reduceMotion: z.ZodOptional<z.ZodBoolean>;
|
|
264
265
|
}, z.core.$strict>>;
|
|
266
|
+
keyboard: z.ZodOptional<z.ZodEnum<{
|
|
267
|
+
software: "software";
|
|
268
|
+
hardware: "hardware";
|
|
269
|
+
}>>;
|
|
265
270
|
}, z.core.$strict>>;
|
|
266
271
|
a11y: z.ZodOptional<z.ZodObject<{
|
|
267
272
|
ignore: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -12,6 +12,12 @@ export type BuildConfiguration = "Debug" | "Release";
|
|
|
12
12
|
export declare const IOS_NATIVE_PREPARE: string;
|
|
13
13
|
/** Local build command (run in the project root) when `build.<platform>` is not configured. */
|
|
14
14
|
export declare function defaultBuildCommand(platform: Platform, configuration: BuildConfiguration, framework?: AppFramework): string;
|
|
15
|
+
/**
|
|
16
|
+
* Android build: prebuild a CNG checkout that has no `android/`, then Gradle alone. Never `expo run:android`: it has no
|
|
17
|
+
* build-only mode (its `--no-install` skips dependency install, not the app's) and installs the APK on whichever
|
|
18
|
+
* emulator is booted, warden's or not. warden installs the cached build itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare function androidGradleCommand(configuration: BuildConfiguration, framework?: AppFramework): string;
|
|
15
21
|
/** What an iOS device build is signed with: the team (always) and, when set, the bundle id that replaces the project's. */
|
|
16
22
|
export type IosDeviceBuild = {
|
|
17
23
|
team: string;
|
|
@@ -55,6 +61,8 @@ export declare const EAS_TIMEOUT_MS: number;
|
|
|
55
61
|
export declare const EAS_TRIGGER_GRACE_MS: number;
|
|
56
62
|
/** Local native build timeout. */
|
|
57
63
|
export declare const LOCAL_BUILD_TIMEOUT_MS: number;
|
|
64
|
+
/** How often a local build nobody watches says it is still running (`still building ios (6m 0s): …`). */
|
|
65
|
+
export declare const BUILD_HEARTBEAT_MS = 60000;
|
|
58
66
|
/** Fingerprint command timeout. */
|
|
59
67
|
export declare const FINGERPRINT_TIMEOUT_MS: number;
|
|
60
68
|
/** Build-lock lease ttl; the lease also carries the holder's pid so it lives as long as the process. */
|
|
@@ -358,6 +358,7 @@ export declare const wardenConfigSchema: z.ZodObject<{
|
|
|
358
358
|
except: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
359
359
|
keep: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
360
360
|
}, z.core.$strict>]>>;
|
|
361
|
+
golden: z.ZodOptional<z.ZodBoolean>;
|
|
361
362
|
biometrics: z.ZodOptional<z.ZodBoolean>;
|
|
362
363
|
reclaim: z.ZodOptional<z.ZodEnum<{
|
|
363
364
|
none: "none";
|
|
@@ -453,6 +454,10 @@ export declare const wardenConfigSchema: z.ZodObject<{
|
|
|
453
454
|
}>>;
|
|
454
455
|
reduceMotion: z.ZodOptional<z.ZodBoolean>;
|
|
455
456
|
}, z.core.$strict>>;
|
|
457
|
+
keyboard: z.ZodOptional<z.ZodEnum<{
|
|
458
|
+
software: "software";
|
|
459
|
+
hardware: "hardware";
|
|
460
|
+
}>>;
|
|
456
461
|
}, z.core.$strict>>;
|
|
457
462
|
a11y: z.ZodOptional<z.ZodObject<{
|
|
458
463
|
ignore: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -606,6 +611,10 @@ export declare const wardenConfigSchema: z.ZodObject<{
|
|
|
606
611
|
}>>;
|
|
607
612
|
reduceMotion: z.ZodOptional<z.ZodBoolean>;
|
|
608
613
|
}, z.core.$strict>>;
|
|
614
|
+
keyboard: z.ZodOptional<z.ZodEnum<{
|
|
615
|
+
software: "software";
|
|
616
|
+
hardware: "hardware";
|
|
617
|
+
}>>;
|
|
609
618
|
}, z.core.$strict>>;
|
|
610
619
|
a11y: z.ZodOptional<z.ZodObject<{
|
|
611
620
|
ignore: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -579,7 +579,7 @@ export interface WardenE2eSuite {
|
|
|
579
579
|
* `"idle"`: leave them booted, so the next run skips the 10-30 s boot, and let `warden gc` shut
|
|
580
580
|
* them down once they have sat idle. A failed run always leaves them up. `--no-shutdown` wins.
|
|
581
581
|
*
|
|
582
|
-
* @default "always"
|
|
582
|
+
* @default "always" locally, "idle" under `CI` / `GITHUB_ACTIONS` (the runner is thrown away)
|
|
583
583
|
*/
|
|
584
584
|
shutdown?: "always" | "idle" | undefined;
|
|
585
585
|
/**
|
|
@@ -734,6 +734,14 @@ export interface WardenE2eSuite {
|
|
|
734
734
|
* `biometrics`. Default false.
|
|
735
735
|
*/
|
|
736
736
|
biometrics?: boolean | undefined;
|
|
737
|
+
/**
|
|
738
|
+
* iOS: simulators the run creates are cloned from a golden image (a first-booted, settled simulator built once
|
|
739
|
+
* per profile and runtime). Off: each is a fresh `simctl create` and first boot, and `warden ci cache --suite`
|
|
740
|
+
* prints no golden group. Existing pool devices are reused either way; `WARDEN_GOLDEN=0` turns it off everywhere.
|
|
741
|
+
*
|
|
742
|
+
* @default false
|
|
743
|
+
*/
|
|
744
|
+
golden?: boolean | undefined;
|
|
737
745
|
/**
|
|
738
746
|
* What happens to the state of the devices of this run when they are released: `"erase"` (iOS: shutdown +
|
|
739
747
|
* `simctl erase`; Android: uninstalls the third-party apps), `"uninstall"` (removes the user-installed apps) or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Result } from "../../../types/src/result";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { type A11yCondition, type ClockCondition, type LocationCondition, type NetworkCondition, type NetworkPreset, type PermissionName, type PermissionState } from "./conditions.types";
|
|
3
|
+
import { type A11yCondition, type ClockCondition, type KeyboardCondition, type LocationCondition, type NetworkCondition, type NetworkPreset, type PermissionName, type PermissionState } from "./conditions.types";
|
|
4
4
|
/** `network: { latencyMs, ... }`: the same fields as a `custom` network condition, without its `kind`. */
|
|
5
5
|
declare const customNetworkSchema: z.ZodObject<{
|
|
6
6
|
latencyMs: z.ZodOptional<z.ZodNumber>;
|
|
@@ -91,6 +91,10 @@ export declare const conditionsConfigSchema: z.ZodObject<{
|
|
|
91
91
|
}>>;
|
|
92
92
|
reduceMotion: z.ZodOptional<z.ZodBoolean>;
|
|
93
93
|
}, z.core.$strict>>;
|
|
94
|
+
keyboard: z.ZodOptional<z.ZodEnum<{
|
|
95
|
+
software: "software";
|
|
96
|
+
hardware: "hardware";
|
|
97
|
+
}>>;
|
|
94
98
|
}, z.core.$strict>;
|
|
95
99
|
export type ConditionsConfig = z.input<typeof conditionsConfigSchema>;
|
|
96
100
|
/**
|
|
@@ -129,6 +133,7 @@ export type SuiteConditions = {
|
|
|
129
133
|
locale?: string;
|
|
130
134
|
timezone?: string;
|
|
131
135
|
a11y?: A11yCondition;
|
|
136
|
+
keyboard?: KeyboardCondition;
|
|
132
137
|
};
|
|
133
138
|
/** A config as conditions: shaped values, with a text size resolved. */
|
|
134
139
|
export declare function normalizeConditions(config: ConditionsConfig): Result<{
|
|
@@ -127,6 +127,26 @@ export type BatteryCondition = {
|
|
|
127
127
|
charging?: boolean;
|
|
128
128
|
cosmetic?: boolean;
|
|
129
129
|
};
|
|
130
|
+
export type KeyboardMode = "software" | "hardware";
|
|
131
|
+
export declare const KEYBOARD_MODES: readonly KeyboardMode[];
|
|
132
|
+
/** What every warden-leased device starts on and what release restores: the on-screen keyboard shows. */
|
|
133
|
+
export declare const DEFAULT_KEYBOARD_MODE: KeyboardMode;
|
|
134
|
+
/** `software`: the on-screen keyboard shows; `hardware`: it is hidden, input arrives as from a connected keyboard. */
|
|
135
|
+
export type KeyboardCondition = {
|
|
136
|
+
mode: KeyboardMode;
|
|
137
|
+
previousIme?: ImeState;
|
|
138
|
+
};
|
|
139
|
+
/** Global settings holding the IME state `hardware` mode switched off (survives the driver process). */
|
|
140
|
+
export declare const IME_STASH_ENABLED = "warden_ime_enabled";
|
|
141
|
+
export declare const IME_STASH_DEFAULT = "warden_ime_default";
|
|
142
|
+
/**
|
|
143
|
+
* Android only: the input methods `hardware` mode switched off, recorded so release puts them back exactly.
|
|
144
|
+
* `enabled` is `settings secure enabled_input_methods` and `default` is `default_input_method` (empty = none).
|
|
145
|
+
*/
|
|
146
|
+
export type ImeState = {
|
|
147
|
+
enabled: string;
|
|
148
|
+
default: string;
|
|
149
|
+
};
|
|
130
150
|
export type CameraFacing = "back" | "front";
|
|
131
151
|
export declare const CAMERA_FACINGS: readonly CameraFacing[];
|
|
132
152
|
/** A staged asset the simulated camera plays; `path` is the staged copy under the device's camera dir. */
|
|
@@ -150,6 +170,7 @@ export type ConditionValues = {
|
|
|
150
170
|
a11y: A11yCondition;
|
|
151
171
|
battery: BatteryCondition;
|
|
152
172
|
camera: CameraCondition;
|
|
173
|
+
keyboard: KeyboardCondition;
|
|
153
174
|
};
|
|
154
175
|
export type ConditionArea = keyof ConditionValues;
|
|
155
176
|
/** Display/iteration order; also the order undo resets areas in. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConditionArea } from "./conditions.types";
|
|
1
2
|
/**
|
|
2
3
|
* The state file an iOS app launched with `libwarden-inject.dylib` watches (offline flag, wall-clock offset /
|
|
3
4
|
* freeze). The e2e driver owns the full format (`packages/e2e/src/driver/ios/inject.ts`); core only knows where it
|
|
@@ -7,7 +8,7 @@
|
|
|
7
8
|
export declare function injectStatePath(wardenHome: string, udid: string): string;
|
|
8
9
|
/**
|
|
9
10
|
* Write `{ seq: previous + 1, offline: false, clockOffsetMs: 0, camera: {} }` (no freeze, no camera feeds) when a state file exists; the dylib
|
|
10
|
-
* applies any higher `seq`. No file = no injected app was ever launched on the device: nothing to do.
|
|
11
|
+
* applies any higher `seq`. `keyboard` stays what the file had, except when `area` is `keyboard`: then it goes back to the default. No file = no injected app was ever launched on the device: nothing to do.
|
|
11
12
|
* Returns an error message, or undefined.
|
|
12
13
|
*/
|
|
13
|
-
export declare function neutralizeInjectState(path: string): string | undefined;
|
|
14
|
+
export declare function neutralizeInjectState(path: string, area?: ConditionArea): string | undefined;
|
|
@@ -13,6 +13,8 @@ export type A11yTarget = {
|
|
|
13
13
|
}): Promise<Result<AnnouncementMatch, DriverError>>;
|
|
14
14
|
/** the current screen as text with the `--a11y` columns (failure evidence); undefined when it cannot be read */
|
|
15
15
|
a11yScreen(): Promise<string | undefined>;
|
|
16
|
+
/** whether the on-screen keyboard is up right now (one fresh read) */
|
|
17
|
+
keyboardVisible(): Promise<boolean>;
|
|
16
18
|
/** the assertion timeout in ms */
|
|
17
19
|
assertionTimeout(): number;
|
|
18
20
|
/** one reported step */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { A11yCondition, BatteryCondition, CameraCondition, ClockCondition, HostFault, LocaleCondition, LocationCondition, NetworkCondition, PermissionCondition } from "../../../core/src/conditions/conditions.types";
|
|
1
|
+
import type { A11yCondition, BatteryCondition, CameraCondition, ClockCondition, HostFault, KeyboardCondition, LocaleCondition, LocationCondition, NetworkCondition, PermissionCondition } from "../../../core/src/conditions/conditions.types";
|
|
2
2
|
/**
|
|
3
3
|
* One applied condition transition, the evidence line that lets a failure show which network/location was
|
|
4
4
|
* active. Written to `results.jsonl` beside the test results (`readResults` skips lines that are not a
|
|
@@ -39,6 +39,11 @@ export type ConditionEvent = {
|
|
|
39
39
|
area: "battery";
|
|
40
40
|
value: BatteryCondition;
|
|
41
41
|
at: number;
|
|
42
|
+
} | {
|
|
43
|
+
type: "condition";
|
|
44
|
+
area: "keyboard";
|
|
45
|
+
value: KeyboardCondition;
|
|
46
|
+
at: number;
|
|
42
47
|
} | {
|
|
43
48
|
type: "condition";
|
|
44
49
|
area: "camera";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type NetworkInput } from "../../../core/src/conditions/conditions.config";
|
|
2
|
-
import type
|
|
3
|
-
import type { A11yOp, A11yState, BatteryOp, BatteryState, CameraState, Driver, LocaleState, NetworkState, PermissionGrants, PermissionsState, PushPayload, SettingOptions, TimezoneState } from "../driver/driver.types";
|
|
2
|
+
import { type CameraFacing, type ClockCondition, type HostFault, type KeyboardMode, type LatLon, type LocationCondition, type LocationScenario, type NetworkCondition } from "../../../core/src/conditions/conditions.types";
|
|
3
|
+
import type { A11yOp, A11yState, BatteryOp, BatteryState, CameraState, Driver, KeyboardResult, LocaleState, NetworkState, PermissionGrants, PermissionsState, PushPayload, SettingOptions, TimezoneState } from "../driver/driver.types";
|
|
4
4
|
import type { E2eEnv } from "../env";
|
|
5
5
|
import { type ConditionSink } from "./condition-events";
|
|
6
6
|
import type { ConditionRecorder } from "./lease-recorder";
|
|
@@ -160,6 +160,12 @@ export type DeviceConditions = {
|
|
|
160
160
|
a11y(op: A11yOp | null): Promise<A11yState>;
|
|
161
161
|
/** Battery level and charging. Android emulators: real. iOS: the status bar only (`cosmetic: true`). `null` resets. */
|
|
162
162
|
battery(op: BatteryOp | null): Promise<BatteryState>;
|
|
163
|
+
/**
|
|
164
|
+
* The on-screen keyboard: `software` (the default) keeps it up while the spec types, `hardware` hides it (input as from
|
|
165
|
+
* a connected keyboard), `toggle` flips the mode this spec has in place. Live, no relaunch. Put back to the suite's
|
|
166
|
+
* `conditions.keyboard` (else `software`) when the test ends.
|
|
167
|
+
*/
|
|
168
|
+
keyboard(mode: KeyboardMode | "toggle"): Promise<KeyboardResult>;
|
|
163
169
|
/** Android: a critical trim-memory signal to the running app. iOS: `E_UNSUPPORTED` (no delivery mechanism works). */
|
|
164
170
|
memoryWarning(options?: SettingOptions): Promise<void>;
|
|
165
171
|
/** iOS: delivers an APNs payload (`{ aps: { alert: ... } }`) to the app. Android: `E_UNSUPPORTED`. */
|
|
@@ -81,8 +81,8 @@ export type Device = DeviceA11y & {
|
|
|
81
81
|
clipboard(): Promise<string>;
|
|
82
82
|
setClipboard(text: string): Promise<void>;
|
|
83
83
|
/**
|
|
84
|
-
* Dismisses the soft keyboard when the snapshot shows one, else does nothing. iOS
|
|
85
|
-
* (which may submit the
|
|
84
|
+
* Dismisses the soft keyboard when the snapshot shows one, else does nothing. iOS resigns the first responder
|
|
85
|
+
* (an app launched without the injected library gets Enter, which may submit the field); Android presses back.
|
|
86
86
|
*/
|
|
87
87
|
hideKeyboard(): Promise<void>;
|
|
88
88
|
/** Saves `<attemptDir>/screenshots/NNN-<name>.png` and returns the path. */
|
|
@@ -125,6 +125,14 @@ export type Device = DeviceA11y & {
|
|
|
125
125
|
a11y: DeviceConditions["a11y"];
|
|
126
126
|
/** Battery level and charging: real on Android emulators, status bar only on iOS (`cosmetic: true`). */
|
|
127
127
|
battery: DeviceConditions["battery"];
|
|
128
|
+
/**
|
|
129
|
+
* The on-screen keyboard mode: `"software"` (the default: it stays up while the spec types), `"hardware"` (hidden,
|
|
130
|
+
* as with a connected keyboard) or `"toggle"`. Live. Put back to the suite's `conditions.keyboard` when the test
|
|
131
|
+
* ends. Physical iPhones: `E_UNSUPPORTED`.
|
|
132
|
+
*/
|
|
133
|
+
keyboard: DeviceConditions["keyboard"];
|
|
134
|
+
/** Whether the on-screen keyboard is up now, read from the accessibility tree. `expect(device).toHaveKeyboard()` polls it. */
|
|
135
|
+
keyboardVisible(): Promise<boolean>;
|
|
128
136
|
/**
|
|
129
137
|
* Simulated camera: `set("fixtures/qr.png", { facing })`, `clear`, `status`. The asset (png, jpg, heic, mp4, mov; path relative
|
|
130
138
|
* to the spec file) plays in the app's real camera. iOS: live in an app launched with the camera, else from the next launch.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type KeyboardMode, type PermissionName, type PermissionState } from "../../../../core/src/conditions/conditions.types";
|
|
2
2
|
import type { Driver } from "../driver.types";
|
|
3
3
|
import type { AdbSession } from "./adb-session";
|
|
4
4
|
import { type TalkBackControl } from "./android-talkback";
|
|
@@ -35,7 +35,14 @@ export type AndroidSettingsDeps = {
|
|
|
35
35
|
/** is `warden-uiad` how this driver reads the screen (TalkBack mode needs it: the dump fallback unbinds TalkBack); default false */
|
|
36
36
|
uiadAvailable?: () => boolean;
|
|
37
37
|
};
|
|
38
|
-
export type AndroidSettings = Pick<Driver, "permissions" | "timezone" | "locale" | "a11y" | "battery" | "memoryWarning" | "push">;
|
|
38
|
+
export type AndroidSettings = Pick<Driver, "permissions" | "timezone" | "locale" | "a11y" | "battery" | "memoryWarning" | "push" | "keyboard" | "keyboardState">;
|
|
39
|
+
/**
|
|
40
|
+
* Whether the IME window is up: `dumpsys window InputMethod` says `isVisible=true` while it is (the uiautomator dump
|
|
41
|
+
* carries the app's window only, so the keyboard is never in the snapshot on Android).
|
|
42
|
+
*/
|
|
43
|
+
export declare function imeVisibleIn(dump: string): boolean;
|
|
39
44
|
export declare function createAndroidSettings(deps: AndroidSettingsDeps): AndroidSettings & {
|
|
40
45
|
talkback: TalkBackControl;
|
|
46
|
+
keyboardMode(): Promise<KeyboardMode>;
|
|
47
|
+
keyboardVisible(): Promise<boolean>;
|
|
41
48
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Driver, DriverResult } from "./driver.types";
|
|
2
|
+
/**
|
|
3
|
+
* The iOS helper's check for a wedged accessibility service (`warden-simd` `AXWedgeRecovery`): when both AX readers
|
|
4
|
+
* stay silent and the sim log names the wedge, it restarts backboardd (which quits the foreground app), ends its stale
|
|
5
|
+
* guests and reads on. `last` is the latest check; `agoMs` counts from its end.
|
|
6
|
+
*/
|
|
7
|
+
export type AxWedgeState = {
|
|
8
|
+
restarts: number;
|
|
9
|
+
maxRestarts: number;
|
|
10
|
+
recovering: boolean;
|
|
11
|
+
/** `recover: true` only: what the check that ask ran (or joined) found; absent when none ran (budget spent) */
|
|
12
|
+
checked?: "restarted" | "not-wedged" | "failed";
|
|
13
|
+
last?: {
|
|
14
|
+
outcome: "restarted";
|
|
15
|
+
agoMs: number;
|
|
16
|
+
ms?: number;
|
|
17
|
+
} | {
|
|
18
|
+
outcome: "not-wedged";
|
|
19
|
+
agoMs: number;
|
|
20
|
+
} | {
|
|
21
|
+
outcome: "failed";
|
|
22
|
+
agoMs: number;
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Optional capability next to the frozen `Driver` (iOS simulators): read the helper's wedge state, or with
|
|
28
|
+
* `recover: true` run the check now and answer once it is over. A helper built before it answers `E_UNSUPPORTED`
|
|
29
|
+
* `detail.reason: "helper_outdated"`.
|
|
30
|
+
*/
|
|
31
|
+
export type RecoversAxWedge = {
|
|
32
|
+
axWedge(opts?: {
|
|
33
|
+
recover?: boolean;
|
|
34
|
+
}): DriverResult<AxWedgeState>;
|
|
35
|
+
};
|
|
36
|
+
export declare function canRecoverAxWedge(driver: Driver): driver is Driver & RecoversAxWedge;
|
|
37
|
+
/** Whether the helper restarted backboardd within the last `windowMs` (e.g. during the test that just failed). */
|
|
38
|
+
export declare function restartedWithin(state: AxWedgeState, windowMs: number): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type SpanDetail } from "./span-detail";
|
|
3
|
+
/**
|
|
4
|
+
* What one iOS `axbridge` fallback or stall looked like, as `warden-simd` reports it (`fallback.stall` on a snapshot,
|
|
5
|
+
* `detail.stall` on a failed read). Recorded in `trace.jsonl` (the `stall*` span fields), `results.jsonl` (`bridge`)
|
|
6
|
+
* and under the header of `screen-at-failure.txt`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const bridgeStallSchema: z.ZodObject<{
|
|
9
|
+
stage: z.ZodString;
|
|
10
|
+
readMs: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
deadlineMs: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
guestAgeMs: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
cold: z.ZodBoolean;
|
|
14
|
+
cooldownRemainingMs: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
probes: z.ZodNumber;
|
|
16
|
+
recycles: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
recyclesExhausted: z.ZodOptional<z.ZodBoolean>;
|
|
18
|
+
axWedge: z.ZodOptional<z.ZodString>;
|
|
19
|
+
axWedgeRestarts: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
}, z.core.$loose>;
|
|
21
|
+
export type BridgeStall = {
|
|
22
|
+
stage: string;
|
|
23
|
+
readMs?: number;
|
|
24
|
+
deadlineMs?: number;
|
|
25
|
+
guestAgeMs?: number;
|
|
26
|
+
cold: boolean;
|
|
27
|
+
cooldownRemainingMs?: number;
|
|
28
|
+
probes: number;
|
|
29
|
+
recycles?: number;
|
|
30
|
+
recyclesExhausted?: boolean;
|
|
31
|
+
axWedge?: string;
|
|
32
|
+
axWedgeRestarts?: number;
|
|
33
|
+
};
|
|
34
|
+
/** Only the fields of the schema, so a newer helper's extra keys stay off the span and out of the results. */
|
|
35
|
+
export declare function toBridgeStall(wire: z.infer<typeof bridgeStallSchema>): BridgeStall;
|
|
36
|
+
/** Leaves the stall on the span timing the read (`stallStage`, `stallReadMs`, …). */
|
|
37
|
+
export declare function noteBridgeStall(stall: BridgeStall): void;
|
|
38
|
+
/** The stall a span recorded, or undefined when it has none. */
|
|
39
|
+
export declare function bridgeStallFromSpan(detail: SpanDetail | undefined): BridgeStall | undefined;
|
|
40
|
+
/** One line for a log or a report: `stage read · read 4012 ms of a 4000 ms deadline · warm · …`. */
|
|
41
|
+
export declare function formatBridgeStall(stall: BridgeStall): string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { CameraCondition, ClockCondition, HostFault, LocationCondition, NetworkCondition, PermissionName, PermissionState, TextSize } from "../../../core/src/conditions/conditions.types";
|
|
1
|
+
import type { CameraCondition, ClockCondition, HostFault, ImeState, KeyboardMode, LocationCondition, NetworkCondition, PermissionName, PermissionState, TextSize } from "../../../core/src/conditions/conditions.types";
|
|
2
2
|
import type { AsyncResult } from "../../../types/src/result";
|
|
3
3
|
import type { ScreenReaderState } from "../a11y/a11y.types";
|
|
4
|
+
import type { BridgeStall } from "./bridge-stall";
|
|
4
5
|
/**
|
|
5
6
|
* The device driver contract every layer of warden-e2e builds on: the spec API (`screen`, `device`,
|
|
6
7
|
* `app`), `warden ui`, specs and the devtools facet all talk to a `Driver`. Implementations:
|
|
@@ -136,9 +137,12 @@ export type Snapshot = {
|
|
|
136
137
|
modal?: ModalInfo;
|
|
137
138
|
/**
|
|
138
139
|
* Set when `backend` answered because this reader failed or was skipped first (iOS: `axbridge` stalled, `ax` read in
|
|
139
|
-
* its place). The fallback reader can lack what the first one has, so a missing node on such a snapshot is not final
|
|
140
|
+
* its place). The fallback reader can lack what the first one has, so a missing node on such a snapshot is not final:
|
|
141
|
+
* the iOS helper probes the first reader in the background, and a later read is back on it once it answers.
|
|
140
142
|
*/
|
|
141
143
|
fellBackFrom?: SnapshotBackend;
|
|
144
|
+
/** iOS: what the bridge failure behind `fellBackFrom` looked like, when the helper reported it */
|
|
145
|
+
bridgeStall?: BridgeStall;
|
|
142
146
|
};
|
|
143
147
|
export type Orientation = "portrait" | "portraitUpsideDown" | "landscapeLeft" | "landscapeRight";
|
|
144
148
|
export type HardwareButton = "home" | "back" | "lock" | "volumeUp" | "volumeDown" | "shake";
|
|
@@ -340,6 +344,21 @@ export interface Driver {
|
|
|
340
344
|
* OS and the app. iOS: status bar override only (`cosmetic: true`): the app still reads the real battery.
|
|
341
345
|
*/
|
|
342
346
|
battery(op: BatteryOp | null): DriverResult<BatteryState>;
|
|
347
|
+
/**
|
|
348
|
+
* The on-screen keyboard mode (KBD-2): `software` keeps it up while warden types, `hardware` hides it (input as from
|
|
349
|
+
* a connected keyboard). Live, no relaunch. iOS simulator: the injected dylib (`appliesOnLaunch` when the running
|
|
350
|
+
* app was launched without it). Android: disables the soft IMEs (stashed on the device, restored by `software` and on release). Physical iOS:
|
|
351
|
+
* `E_UNSUPPORTED`.
|
|
352
|
+
*/
|
|
353
|
+
keyboard(mode: KeyboardMode): DriverResult<KeyboardResult>;
|
|
354
|
+
/** The mode in force and whether a keyboard is on screen now (a `keyboard` element in the accessibility tree). */
|
|
355
|
+
keyboardState(): DriverResult<KeyboardState>;
|
|
356
|
+
/**
|
|
357
|
+
* Takes the on-screen keyboard down without a keystroke that could submit a form. iOS simulator: the injected
|
|
358
|
+
* dylib resigns the first responder (`via: "resign"`); an app launched without it falls back to Enter (`via:
|
|
359
|
+
* "key"`, with a warning in the log). Android: back. Physical iPhone: Enter.
|
|
360
|
+
*/
|
|
361
|
+
dismissKeyboard(): DriverResult<DismissKeyboardResult>;
|
|
343
362
|
/** Android: `am send-trim-memory RUNNING_CRITICAL` to the running app. iOS: `E_UNSUPPORTED` (no way verified to reach an app). */
|
|
344
363
|
memoryWarning(opts?: SettingOptions): DriverResult<void>;
|
|
345
364
|
/** iOS: `simctl push` of an APNs payload (needs `aps`) to the app. Android: `E_UNSUPPORTED`. */
|
|
@@ -439,6 +458,20 @@ export type A11yState = {
|
|
|
439
458
|
enabled: string | null;
|
|
440
459
|
};
|
|
441
460
|
};
|
|
461
|
+
/** `previousIme` (Android `hardware`): the input methods switched off, for the lease record. */
|
|
462
|
+
export type KeyboardResult = {
|
|
463
|
+
mode: KeyboardMode;
|
|
464
|
+
appliesOnLaunch?: boolean;
|
|
465
|
+
previousIme?: ImeState;
|
|
466
|
+
};
|
|
467
|
+
/** `via`: how the keyboard was dismissed; `key` (Enter) can submit the focused field. */
|
|
468
|
+
export type DismissKeyboardResult = {
|
|
469
|
+
via: "resign" | "key" | "back";
|
|
470
|
+
};
|
|
471
|
+
export type KeyboardState = {
|
|
472
|
+
mode: KeyboardMode;
|
|
473
|
+
visible: boolean;
|
|
474
|
+
};
|
|
442
475
|
export type BatteryOp = {
|
|
443
476
|
level?: number;
|
|
444
477
|
charging?: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CameraCondition, type KeyboardMode } from "../../../../core/src/conditions/conditions.types";
|
|
2
2
|
import type { Result } from "../../../../types/src/result";
|
|
3
3
|
import type { DriverError } from "../driver.types";
|
|
4
4
|
import type { ProxyAddress } from "../network-proxy";
|
|
@@ -25,6 +25,17 @@ export type InjectState = {
|
|
|
25
25
|
/** one-shot request (A11Y-1): walk the accessibility tree and write `<state>.a11y` before acknowledging this `seq` */
|
|
26
26
|
a11yRead?: boolean;
|
|
27
27
|
camera?: CameraCondition;
|
|
28
|
+
/**
|
|
29
|
+
* The on-screen keyboard (KBD-2): `software` keeps it up while warden types, `hardware` lets iOS minimise it. Unlike
|
|
30
|
+
* the other fields a message that leaves it out keeps what the file had (see `createInjectChannel`), so a network or
|
|
31
|
+
* a11y write never undoes it.
|
|
32
|
+
*/
|
|
33
|
+
keyboard?: KeyboardMode;
|
|
34
|
+
/**
|
|
35
|
+
* one-shot request: resign the first responder (the keyboard goes away without a Return that could submit the form)
|
|
36
|
+
* before acknowledging this `seq`. Written, never read back, so a later write cannot repeat it.
|
|
37
|
+
*/
|
|
38
|
+
dismissKeyboard?: boolean;
|
|
28
39
|
};
|
|
29
40
|
/** What the dylib answers in `<state>.ack` once it applied `seq` (`bundleId`: the app it loaded into; `camera`: launched with `WARDEN_CAMERA=1`). */
|
|
30
41
|
export type InjectAck = {
|
|
@@ -56,6 +67,8 @@ export declare function injectLaunchEnv(input: {
|
|
|
56
67
|
clockFrozenMs?: number;
|
|
57
68
|
/** fake the capture stack from the state file's `camera` feeds (`WARDEN_CAMERA=1`) */
|
|
58
69
|
camera?: boolean;
|
|
70
|
+
/** the keyboard mode the app starts in (`WARDEN_KEYBOARD`); the state file's `keyboard` overrides it live */
|
|
71
|
+
keyboard?: KeyboardMode;
|
|
59
72
|
debug?: boolean;
|
|
60
73
|
}): Record<string, string>;
|
|
61
74
|
/**
|
|
@@ -69,6 +82,8 @@ export declare function shouldInject(input: {
|
|
|
69
82
|
clockActive: boolean;
|
|
70
83
|
/** the lease has a camera feed: the dylib must be loaded at launch to fake the capture stack */
|
|
71
84
|
cameraActive?: boolean;
|
|
85
|
+
/** the lease's keyboard mode is `software` (the default): the dylib keeps the on-screen keyboard up while warden types */
|
|
86
|
+
keyboardActive?: boolean;
|
|
72
87
|
/** the caller asked for injection (`LaunchOptions.inject`, a suite that declares `conditions.network`) */
|
|
73
88
|
requested: boolean;
|
|
74
89
|
}): boolean;
|
|
@@ -111,7 +126,7 @@ export type InjectChannel = {
|
|
|
111
126
|
attached(): InjectAck | undefined;
|
|
112
127
|
/** Wait until the app acknowledged `seq`; `false` on timeout. */
|
|
113
128
|
acked(seq: number, timeoutMs: number): Promise<boolean>;
|
|
114
|
-
/** Forget the state and ack of a previous app (before a launch). */
|
|
129
|
+
/** Forget the state and ack of a previous app (before a launch); the keyboard mode is the lease's, so it stays. */
|
|
115
130
|
reset(): void;
|
|
116
131
|
};
|
|
117
132
|
export declare function createInjectChannel(input: {
|
|
@@ -3,6 +3,7 @@ import type { E2eEnv } from "../../env";
|
|
|
3
3
|
import type { ReadsA11yEvents } from "../a11y-events";
|
|
4
4
|
import type { ReadsA11y } from "../a11y-read";
|
|
5
5
|
import type { ProbesFrontmost, ReadsAlertOn } from "../alerts";
|
|
6
|
+
import type { RecoversAxWedge } from "../ax-wedge";
|
|
6
7
|
import type { ClockRecorder } from "../clock-recorder";
|
|
7
8
|
import type { Driver, DriverResult, KeyName, Logger } from "../driver.types";
|
|
8
9
|
import type { AuditsNatively } from "../native-audit";
|
|
@@ -11,6 +12,7 @@ import type { PastesText, SelectsAll } from "../typing";
|
|
|
11
12
|
import { type InjectFs } from "./inject";
|
|
12
13
|
import { type IosA11y } from "./ios-a11y";
|
|
13
14
|
import { type IosNativeAudit, type IosNativeAuditOptions } from "./ios-audit";
|
|
15
|
+
import { type IosKeyboard } from "./ios-keyboard";
|
|
14
16
|
import { type IosNetwork } from "./ios-network";
|
|
15
17
|
import { type SwappableClient } from "./ios-session";
|
|
16
18
|
import { type IosSettings, type StagedSettings } from "./ios-settings";
|
|
@@ -27,7 +29,7 @@ import type { SimdSpawn } from "./simd-transport";
|
|
|
27
29
|
*/
|
|
28
30
|
export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir"> & Partial<Pick<E2eEnv, "wardenVersion">>;
|
|
29
31
|
/** The iOS driver is a `Driver` plus the optional capabilities of `driver/alerts.ts`. */
|
|
30
|
-
export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost & ReadsA11y & ReadsA11yEvents & AuditsNatively;
|
|
32
|
+
export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost & RecoversAxWedge & ReadsA11y & ReadsA11yEvents & AuditsNatively;
|
|
31
33
|
export type OpenIosDriverOptions = {
|
|
32
34
|
udid: string;
|
|
33
35
|
env?: IosDriverEnv;
|
|
@@ -88,9 +90,20 @@ export type OpenIosDriverOptions = {
|
|
|
88
90
|
* How long the first snapshot of a session waits for an in-sim bridge that is up but silent. A freshly cloned
|
|
89
91
|
* simulator on a loaded host can take longer than one read's budget (axbridge 4 s + AX fallback 5 s) to answer the
|
|
90
92
|
* first time, and every read after it is fast, so the first one retries while the failure says "not ready yet".
|
|
91
|
-
* Once a read
|
|
93
|
+
* Once a read has succeeded the normal per-call budgets apply. Inside a screen-layer poll (`withReadDeadline`) the
|
|
94
|
+
* wait also ends at that poll's deadline, so a 5 s assertion is not held for the whole budget.
|
|
92
95
|
*/
|
|
93
96
|
export declare const COLD_BRIDGE_BUDGET_MS = 60000;
|
|
97
|
+
/**
|
|
98
|
+
* warden-simd's deadline for the first `axbridge` read of a cold guest (helper start, a recycled guest, an app just
|
|
99
|
+
* launched): `BridgeDeadline.defaultColdReadMs`, or `WARDEN_SIMD_BRIDGE_COLD_DEADLINE_MS`. On a 3-vCPU runner that
|
|
100
|
+
* first read takes 4-11 s; warm reads keep the 4 s deadline.
|
|
101
|
+
*/
|
|
102
|
+
export declare const BRIDGE_COLD_DEADLINE_MS = 12000;
|
|
103
|
+
/** The request timeout of a read while the bridge is cold, so the client never gives up before the helper's deadline. */
|
|
104
|
+
export declare const COLD_READ_TIMEOUT_MS: number;
|
|
105
|
+
/** {@link COLD_READ_TIMEOUT_MS}, following a `WARDEN_SIMD_BRIDGE_COLD_DEADLINE_MS` the helper also reads. */
|
|
106
|
+
export declare function coldReadTimeoutMs(env?: Readonly<Record<string, string | undefined>>): number;
|
|
94
107
|
/** USB HID keyboard-page usage ids for the keys `Driver.key` offers. */
|
|
95
108
|
export declare const KEY_USAGE: Readonly<Record<KeyName, number>>;
|
|
96
109
|
/** The fields of a failed snapshot's `detail` the helper sets (which reader, why, both readers' errors), on its span. */
|
|
@@ -112,6 +125,8 @@ export declare function createIosDriver(swap: SwappableClient, simctl: Simctl, o
|
|
|
112
125
|
}) => IosSettings;
|
|
113
126
|
/** screen-reader facts from the injected dylib (`ios-a11y.ts`); absent: `readA11y` answers `E_UNSUPPORTED` */
|
|
114
127
|
a11y?: () => IosA11y;
|
|
128
|
+
/** the on-screen keyboard mode (`ios-keyboard.ts`); absent: `keyboard` answers `E_UNSUPPORTED` */
|
|
129
|
+
keyboard?: () => IosKeyboard;
|
|
115
130
|
/** per-app network conditions (`ios-network.ts`); absent: `network` answers `E_UNSUPPORTED` */
|
|
116
131
|
/** the XCUITest audit (`ios-audit.ts`); absent: `auditNatively` skips with a reason */
|
|
117
132
|
nativeAudit?: (ctx: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type KeyboardMode } from "../../../../core/src/conditions/conditions.types";
|
|
2
|
+
import type { Result } from "../../../../types/src/result";
|
|
3
|
+
import type { Driver, DriverError, DriverResult } from "../driver.types";
|
|
4
|
+
import { type InjectChannel } from "./inject";
|
|
5
|
+
/**
|
|
6
|
+
* The on-screen keyboard of an iOS simulator app (KBD-2). HID typing makes iOS treat a hardware keyboard as attached
|
|
7
|
+
* and minimise the software keyboard; the injected dylib answers `-[UIKeyboardImpl isMinimized]` with NO while the mode
|
|
8
|
+
* is `software`, and re-lays the input views on every change. The mode lives in the state file's `keyboard` field (the
|
|
9
|
+
* lease's, so it survives an app relaunch) and in `WARDEN_KEYBOARD` at launch.
|
|
10
|
+
*/
|
|
11
|
+
export type IosKeyboardDeps = {
|
|
12
|
+
channel: InjectChannel;
|
|
13
|
+
/** where `libwarden-inject.dylib` is */
|
|
14
|
+
dylib: () => Result<string, DriverError>;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type IosKeyboard = {
|
|
18
|
+
/** the mode in force: the state file's, else the default */
|
|
19
|
+
mode(): KeyboardMode;
|
|
20
|
+
/**
|
|
21
|
+
* Env that loads the dylib with the keyboard mode, for a launch no other layer already injects (`undefined`: those
|
|
22
|
+
* layers carry the dylib; `WARDEN_KEYBOARD` goes through `launchMode`). A dylib that cannot be found is no launch
|
|
23
|
+
* failure: the app runs plain, with the keyboard as iOS decides.
|
|
24
|
+
*/
|
|
25
|
+
launchEnv(): Record<string, string> | undefined;
|
|
26
|
+
/** `WARDEN_KEYBOARD` to merge into the env of a launch that carries the dylib already */
|
|
27
|
+
launchMode(): Record<string, string>;
|
|
28
|
+
/** after a launch that carried the dylib on its own: wait until it loaded */
|
|
29
|
+
confirmLoaded(): DriverResult<void>;
|
|
30
|
+
keyboard: Driver["keyboard"];
|
|
31
|
+
/**
|
|
32
|
+
* Resign the first responder in the app (the dylib, on the main thread), acknowledged through the channel.
|
|
33
|
+
* `attached: false`: no app with the dylib is running, nothing was written and the caller falls back.
|
|
34
|
+
*/
|
|
35
|
+
dismiss(): DriverResult<{
|
|
36
|
+
attached: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
export declare function createIosKeyboard(deps: IosKeyboardDeps): IosKeyboard;
|
|
@@ -21,6 +21,8 @@ export type SimdEventMap = {
|
|
|
21
21
|
};
|
|
22
22
|
export type RequestOptions = {
|
|
23
23
|
timeoutMs?: number;
|
|
24
|
+
/** a floor for this request's timeout (a cold bridge read): raises the default or `timeoutMs`, never lowers it */
|
|
25
|
+
minTimeoutMs?: number;
|
|
24
26
|
};
|
|
25
27
|
export type SimdClient = {
|
|
26
28
|
/** the handshake of the current helper process (refreshed after a respawn) */
|