@delacour/warden-e2e 0.0.0-stage → 0.1.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/chunk-e6pn5ygy.mjs +5832 -0
  3. package/index.d.ts +1 -0
  4. package/index.mjs +1047 -0
  5. package/package.json +49 -4
  6. package/preload.d.ts +1 -0
  7. package/preload.mjs +4788 -0
  8. package/types/core/src/allocate.d.ts +23 -0
  9. package/types/core/src/builds/app-option.schema.d.ts +37 -0
  10. package/types/core/src/builds/install.d.ts +95 -0
  11. package/types/core/src/builds/resolve.d.ts +74 -0
  12. package/types/core/src/config.defaults.d.ts +25 -0
  13. package/types/core/src/exec.d.ts +18 -0
  14. package/types/core/src/liveness.d.ts +12 -0
  15. package/types/core/src/providers/android.d.ts +132 -0
  16. package/types/core/src/providers/health.d.ts +45 -0
  17. package/types/core/src/providers/provider.types.d.ts +56 -0
  18. package/types/core/src/store.d.ts +105 -0
  19. package/types/core/src/types.d.ts +126 -0
  20. package/types/e2e/src/alert-buttons.d.ts +41 -0
  21. package/types/e2e/src/alert-reader.d.ts +25 -0
  22. package/types/e2e/src/app.d.ts +62 -0
  23. package/types/e2e/src/device-devtools.d.ts +26 -0
  24. package/types/e2e/src/device-link.d.ts +43 -0
  25. package/types/e2e/src/device.d.ts +90 -0
  26. package/types/e2e/src/devtools/cdp.d.ts +65 -0
  27. package/types/e2e/src/devtools/console.d.ts +59 -0
  28. package/types/e2e/src/devtools/devtools.types.d.ts +169 -0
  29. package/types/e2e/src/devtools/facet.d.ts +28 -0
  30. package/types/e2e/src/devtools/inspector.d.ts +48 -0
  31. package/types/e2e/src/devtools/jsonl.d.ts +4 -0
  32. package/types/e2e/src/devtools/native-log.d.ts +50 -0
  33. package/types/e2e/src/devtools/network.d.ts +33 -0
  34. package/types/e2e/src/devtools/network.script.d.ts +24 -0
  35. package/types/e2e/src/devtools/session.d.ts +82 -0
  36. package/types/e2e/src/devtools/tree.d.ts +23 -0
  37. package/types/e2e/src/devtools/tree.script.d.ts +33 -0
  38. package/types/e2e/src/driver/alerts.d.ts +29 -0
  39. package/types/e2e/src/driver/android/adb-session.d.ts +52 -0
  40. package/types/e2e/src/driver/android/android-alert.d.ts +4 -0
  41. package/types/e2e/src/driver/android/android-animations.d.ts +29 -0
  42. package/types/e2e/src/driver/android/android-app.d.ts +32 -0
  43. package/types/e2e/src/driver/android/android-argv.d.ts +122 -0
  44. package/types/e2e/src/driver/android/android-device.d.ts +44 -0
  45. package/types/e2e/src/driver/android/android-driver.d.ts +60 -0
  46. package/types/e2e/src/driver/android/android-dump.d.ts +46 -0
  47. package/types/e2e/src/driver/android/android-state.d.ts +69 -0
  48. package/types/e2e/src/driver/android/uiad-backend.d.ts +63 -0
  49. package/types/e2e/src/driver/android/uiad-client.d.ts +60 -0
  50. package/types/e2e/src/driver/android/uiad-device.d.ts +70 -0
  51. package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +8 -0
  52. package/types/e2e/src/driver/android/uiad-ops.d.ts +62 -0
  53. package/types/e2e/src/driver/android/uiad-protocol.d.ts +209 -0
  54. package/types/e2e/src/driver/android/uiautomator.d.ts +33 -0
  55. package/types/e2e/src/driver/driver.types.d.ts +231 -0
  56. package/types/e2e/src/driver/errors.d.ts +13 -0
  57. package/types/e2e/src/driver/gesture-geometry.d.ts +48 -0
  58. package/types/e2e/src/driver/ios/hid-recovery.d.ts +36 -0
  59. package/types/e2e/src/driver/ios/ios-driver.d.ts +63 -0
  60. package/types/e2e/src/driver/ios/ios-session.d.ts +30 -0
  61. package/types/e2e/src/driver/ios/locate-helper.d.ts +38 -0
  62. package/types/e2e/src/driver/ios/normalize.d.ts +11 -0
  63. package/types/e2e/src/driver/ios/simctl.d.ts +102 -0
  64. package/types/e2e/src/driver/ios/simd-client.d.ts +68 -0
  65. package/types/e2e/src/driver/ios/simd-protocol.d.ts +433 -0
  66. package/types/e2e/src/driver/ios/simd-transport.d.ts +43 -0
  67. package/types/e2e/src/driver/ios/type-budget.d.ts +6 -0
  68. package/types/e2e/src/driver/observation-cache.d.ts +66 -0
  69. package/types/e2e/src/driver/open.d.ts +29 -0
  70. package/types/e2e/src/driver/settle.d.ts +43 -0
  71. package/types/e2e/src/driver/span-detail.d.ts +12 -0
  72. package/types/e2e/src/driver/traced-driver.d.ts +14 -0
  73. package/types/e2e/src/driver/typing.d.ts +48 -0
  74. package/types/e2e/src/driver/unwrap.d.ts +13 -0
  75. package/types/e2e/src/env.d.ts +78 -0
  76. package/types/e2e/src/errors.d.ts +26 -0
  77. package/types/e2e/src/expect/expect.d.ts +27 -0
  78. package/types/e2e/src/expect/matchers.d.ts +72 -0
  79. package/types/e2e/src/fixture-session.d.ts +38 -0
  80. package/types/e2e/src/fixtures.d.ts +14 -0
  81. package/types/e2e/src/index.d.ts +11 -0
  82. package/types/e2e/src/perf/budgets.d.ts +33 -0
  83. package/types/e2e/src/perf/device-perf.d.ts +45 -0
  84. package/types/e2e/src/perf/perf.types.d.ts +59 -0
  85. package/types/e2e/src/preload.d.ts +29 -0
  86. package/types/e2e/src/runner/artifacts.d.ts +13 -0
  87. package/types/e2e/src/runner/attempt.d.ts +63 -0
  88. package/types/e2e/src/runner/callsite.d.ts +2 -0
  89. package/types/e2e/src/runner/failure.d.ts +40 -0
  90. package/types/e2e/src/runner/fixtures.types.d.ts +60 -0
  91. package/types/e2e/src/runner/results.d.ts +48 -0
  92. package/types/e2e/src/runner/scope.d.ts +64 -0
  93. package/types/e2e/src/runner/span.d.ts +40 -0
  94. package/types/e2e/src/runner/step.d.ts +7 -0
  95. package/types/e2e/src/runner/test.d.ts +59 -0
  96. package/types/e2e/src/runner/trace.d.ts +87 -0
  97. package/types/e2e/src/screen/actions.d.ts +93 -0
  98. package/types/e2e/src/screen/context.d.ts +134 -0
  99. package/types/e2e/src/screen/field.d.ts +121 -0
  100. package/types/e2e/src/screen/locator.d.ts +233 -0
  101. package/types/e2e/src/screen/nodes.d.ts +53 -0
  102. package/types/e2e/src/screen/poll.d.ts +57 -0
  103. package/types/e2e/src/screen/queries.d.ts +34 -0
  104. package/types/e2e/src/screen/query.d.ts +60 -0
  105. package/types/e2e/src/screen/react-locator.d.ts +30 -0
  106. package/types/e2e/src/screen/render.d.ts +42 -0
  107. package/types/e2e/src/screen/roles.d.ts +24 -0
  108. package/types/e2e/src/screen/screen.d.ts +61 -0
  109. package/types/e2e/src/screen/selector.d.ts +38 -0
  110. package/types/e2e/src/screen/text.d.ts +13 -0
  111. package/types/e2e/src/session.d.ts +124 -0
  112. package/types/e2e/src/traced-device.d.ts +9 -0
  113. package/types/e2e/src/version.d.ts +7 -0
  114. package/types/types/src/result.d.ts +12 -0
  115. package/README.md +0 -3
@@ -0,0 +1,126 @@
1
+ export type Platform = "ios" | "android";
2
+ export type DeviceResource = {
3
+ kind: "device";
4
+ platform: Platform;
5
+ id: string;
6
+ name: string;
7
+ };
8
+ export type PortResource = {
9
+ kind: "port";
10
+ port: number;
11
+ };
12
+ export type BuildResource = {
13
+ kind: "build";
14
+ key: string;
15
+ };
16
+ export type Resource = DeviceResource | PortResource | BuildResource;
17
+ export type AgentOwner = {
18
+ kind: "agent";
19
+ sessionId: string;
20
+ cwd: string;
21
+ repo?: string;
22
+ worktree?: string;
23
+ };
24
+ export type UserOwner = {
25
+ kind: "user";
26
+ pid: number;
27
+ tty?: string;
28
+ cwd: string;
29
+ repo?: string;
30
+ worktree?: string;
31
+ };
32
+ export type CiOwner = {
33
+ kind: "ci";
34
+ runId: string;
35
+ };
36
+ export type Owner = AgentOwner | UserOwner | CiOwner;
37
+ export type Lease = {
38
+ id: string;
39
+ resource: Resource;
40
+ owner: Owner;
41
+ label?: string;
42
+ acquiredAt: number;
43
+ heartbeatAt: number;
44
+ ttlMs: number;
45
+ pid?: number;
46
+ /** the owner booted this device (it was off when leased) — so it's theirs to shut down on release */
47
+ bootedByOwner?: boolean;
48
+ /**
49
+ * Heartbeat period of the process that holds this lease (`run` / `dev` / `batch` / `e2e`, which also
50
+ * record their own `pid`). With a pid, a dead pid makes the lease stale after `LOST_HEARTBEATS` periods.
51
+ */
52
+ heartbeatMs?: number;
53
+ /**
54
+ * The holding process (`warden dev`) lets its owner's other processes drive the device beside it: `warden ui`
55
+ * attaches to it instead of refusing with `device_busy`, and never releases it. Other owners still see it as foreign.
56
+ */
57
+ shared?: boolean;
58
+ };
59
+ export type DeviceState = "booted" | "shutdown" | "booting";
60
+ /** A device as seen by a provider, enriched with warden's own bookkeeping. */
61
+ export type InventoryDevice = {
62
+ platform: Platform;
63
+ id: string;
64
+ name: string;
65
+ state: DeviceState;
66
+ /** true when warden created this device (sim) or launched it (emulator) */
67
+ wardenCreated: boolean;
68
+ /** warden profile slug, e.g. `iphone-17` */
69
+ profile?: string;
70
+ /** runtime identifier or version, e.g. `iOS-26-5` */
71
+ runtime?: string;
72
+ /** a golden image (`warden-golden-*`): clone source only — never allocated, adopted, booted by gc or counted in a pool */
73
+ golden?: boolean;
74
+ /** reported bad: never allocated until `warden sim unquarantine` */
75
+ quarantined?: boolean;
76
+ };
77
+ export type DeviceRequest = {
78
+ platform: Platform;
79
+ profile: string;
80
+ runtime?: string;
81
+ count: number;
82
+ /** max warden-created devices for this platform+profile */
83
+ max: number;
84
+ /** allow allocating foreign (non-warden) devices that match the profile */
85
+ adopt?: boolean;
86
+ };
87
+ export type AllocationStep = {
88
+ action: "reuse";
89
+ device: InventoryDevice;
90
+ } | {
91
+ action: "boot";
92
+ device: InventoryDevice;
93
+ } | {
94
+ action: "create";
95
+ name: string;
96
+ };
97
+ export type AllocationPlan = {
98
+ kind: "assign";
99
+ steps: AllocationStep[];
100
+ stale: string[];
101
+ } | {
102
+ kind: "wait";
103
+ available: number;
104
+ needed: number;
105
+ stale: string[];
106
+ };
107
+ export declare function resourceKey(resource: Resource): string;
108
+ export declare function describeOwner(owner: Owner): string;
109
+ /** Owner location for messages: `repo/worktree` when known. */
110
+ export declare function ownerLocation(owner: Owner): string | undefined;
111
+ export declare function sameOwner(a: Owner, b: Owner): boolean;
112
+ /** A claim waiting for host capacity to boot devices (`boot_queue` row). */
113
+ export type QueueEntry = {
114
+ id: string;
115
+ platform: Platform;
116
+ profile: string;
117
+ /** capacity units the boots need */
118
+ units: number;
119
+ /** numeric base priority (see `PRIORITY` in queue.ts); aging adds to it */
120
+ priority: number;
121
+ enqueuedAt: number;
122
+ heartbeatAt: number;
123
+ owner: Owner;
124
+ label?: string;
125
+ pid?: number;
126
+ };
@@ -0,0 +1,41 @@
1
+ import type { Element, Point } from "./driver/driver.types";
2
+ /**
3
+ * Button names `device.alert("accept")` / `("dismiss")` look for, in order. The Android permission prompt's own
4
+ * labels ("While using the app", "Only this time", "Don't allow") are here too; on Android `accept` and `dismiss` go
5
+ * by the permission prompt's button ids first (`ANDROID_PERMISSION_ALLOW_IDS`, `ANDROID_PERMISSION_DENY_IDS`), which also
6
+ * holds in other locales, then by these names, then by `AlertDialog`'s `button1` / `button2`. "Don't allow" is matched by
7
+ * "Don't Allow" (names compare ignoring case).
8
+ */
9
+ export declare const ACCEPT_BUTTONS: string[];
10
+ export declare const DISMISS_BUTTONS: string[];
11
+ export declare function nameOf(button: Element): string | undefined;
12
+ /** The button called `wanted`, ignoring case and curly apostrophes. */
13
+ export declare function findButton(buttons: readonly Element[], wanted: string): Element | undefined;
14
+ export declare function firstOf(buttons: readonly Element[], names: readonly string[]): Element | undefined;
15
+ /** Where to tap an element: its own tap point, else the middle of its frame. */
16
+ export declare function centre(element: Element): Point;
17
+ /**
18
+ * The permission prompt's allow buttons, in the order `device.alert("accept")` tries them, the most lasting grant
19
+ * first (API 30+ camera, mic and location prompts show "While using the app", "Only this time" and "Don't allow",
20
+ * deny last, so the last button is the wrong guess). Going by id holds in other locales.
21
+ */
22
+ export declare const ANDROID_PERMISSION_ALLOW_IDS: readonly string[];
23
+ /** The permission prompt's deny buttons: the plain one before "don't ask again", which a later prompt could not undo. */
24
+ export declare const ANDROID_PERMISSION_DENY_IDS: readonly string[];
25
+ /**
26
+ * `AlertDialog`'s positive (`button1`) and negative (`button2`) ids. Only a fallback after the names: React Native's
27
+ * `Alert.alert` fills them by position, not by style (positive = last button, negative = second to last), so
28
+ * `[OK, Cancel]` puts OK on `button2` and `[Cancel, Delete, Keep]` puts Delete there.
29
+ */
30
+ export declare const ANDROID_POSITIVE_ID = "button1";
31
+ export declare const ANDROID_NEGATIVE_ID = "button2";
32
+ /**
33
+ * Every id `device.alert("accept")` goes by on Android: the permission allow ids (before the names), then
34
+ * `AlertDialog`'s positive `button1` (after the names).
35
+ */
36
+ export declare const ANDROID_ALLOW_IDS: readonly string[];
37
+ /**
38
+ * Every id `device.alert("dismiss")` goes by on Android: the permission deny ids (before the names), then
39
+ * `AlertDialog`'s negative `button2` (after the names).
40
+ */
41
+ export declare const ANDROID_DENY_IDS: readonly string[];
@@ -0,0 +1,25 @@
1
+ import { type AlertBackend } from "./driver/alerts";
2
+ import type { AlertInfo, Driver } from "./driver/driver.types";
3
+ import type { Clock } from "./fixture-session";
4
+ /** A modal is up but its buttons are not readable yet: look again this many times, this far apart. */
5
+ export declare const ALERT_MODAL_LOOKS = 3;
6
+ export type AlertReaderDeps = {
7
+ driver: Driver;
8
+ clock: Clock;
9
+ /** the running attempt's deadline, read per call */
10
+ signal: () => AbortSignal | undefined;
11
+ };
12
+ /** `readAlert(backend?, modalLooks?)`: see {@link createAlertReader}. */
13
+ export type AlertReader = (backend?: AlertBackend, modalLooks?: number) => Promise<AlertInfo | null>;
14
+ /**
15
+ * Reads the system alert on top for `device.alert` and `device.openLink`.
16
+ *
17
+ * With `backend`, and a driver that can read one reader (`driver/alerts.ts`), that reader is read ONCE: a screen without
18
+ * an alert costs one tree read where `driver.alert()` reads both readers, and nothing sleeps. A modal that is up but
19
+ * shows no buttons yet (`buttons: []`) is looked at again `modalLooks` times, `ALERT_MODAL_RETRY_MS` apart, then reported
20
+ * as it is. If the bridge read fails (it answers nothing for ~0.2 s after a launch, while its quiescence stream is not
21
+ * bound to the new process), the host reader is read once: what `driver.alert()` would answer when one of its two readers
22
+ * fails, for one read instead of two. If that fails too, or there is no `backend`, the two-reader `driver.alert()`
23
+ * answers instead, retrying a transient failure after a short sleep.
24
+ */
25
+ export declare function createAlertReader({ driver, clock, signal }: AlertReaderDeps): AlertReader;
@@ -0,0 +1,62 @@
1
+ import type { Driver } from "./driver/driver.types";
2
+ import type { E2eEnv } from "./env";
3
+ import { type Clock, type FixtureSession } from "./fixture-session";
4
+ import { type Locator } from "./screen/locator";
5
+ /** `app.open({ wait: { until } })`: the first screen, named by a locator. */
6
+ export type OpenWaitUntil = {
7
+ /** a locator made by `screen` that is visible once the app's first screen is up */
8
+ until: Locator;
9
+ /** how long `until` gets to show; default the assertion timeout (`WARDEN_E2E_ASSERTION_TIMEOUT`) */
10
+ timeout?: number;
11
+ };
12
+ export type OpenOptions = {
13
+ /** opened with `driver.app.openUrl` once the app is launched */
14
+ url?: string;
15
+ /** appended after `WARDEN_LAUNCH_ARGS` (on Android, which cannot take launch args, these still fail the launch) */
16
+ args?: readonly string[];
17
+ env?: Record<string, string>;
18
+ /**
19
+ * `"none"` (default): return when the launch call returns, which is before the app is on screen.
20
+ *
21
+ * `"quiet"`: then wait for the first screen and for it to stop moving. A launch screen (a storyboard, an Android
22
+ * splash) is idle while the app loads, so "quiet" alone would end on it: the screen is read after 100, 200, then
23
+ * every 400 ms until it shows content (a visible control, or any element with a testID), for at most 3 s, then
24
+ * settled (what `device.settle()` waits for: 250 ms of quiet, at most 3 s). That is a best-effort wait, never an
25
+ * error: a first screen with no control and no testID waits out the 3 s, and an app that is not quiet in time is
26
+ * returned as it is, so assert on what the test needs.
27
+ *
28
+ * `{ until }`: wait for that locator to be visible (an error after `timeout`, like `toBeVisible()`), then settle
29
+ * once, so the next tap lands on a mounted screen. Use it in place of `open()` followed by `toBeVisible()`.
30
+ */
31
+ wait?: "quiet" | "none" | OpenWaitUntil;
32
+ };
33
+ /** The app under test (`WARDEN_BUNDLE_ID`). */
34
+ export type App = {
35
+ /**
36
+ * Launches the app (terminating a running instance first) with `WARDEN_LAUNCH_ARGS` + `args`, then
37
+ * opens `url` if given, then waits as `wait` says (`"quiet"`: first content then still; `{ until }`). The runner calls this (with no options) for the per-test
38
+ * `launch: "fresh"` default.
39
+ */
40
+ open(options?: OpenOptions): Promise<void>;
41
+ close(): Promise<void>;
42
+ /** terminate, then launch again with `WARDEN_LAUNCH_ARGS` (the launch itself terminates a running instance, so there is one stop, not two) */
43
+ restart(): Promise<void>;
44
+ /** uninstall (when installed) and install again from `WARDEN_APP_PATH` */
45
+ reinstall(): Promise<void>;
46
+ /**
47
+ * Wipes app data but keeps the build. Always asks for the keychain to be reset as well: the iOS
48
+ * driver maps this to core `resetAppData` with `resetKeychain`, Android to `pm clear`.
49
+ */
50
+ clearData(): Promise<void>;
51
+ isRunning(): Promise<boolean>;
52
+ };
53
+ export type AppDeps = {
54
+ session: FixtureSession;
55
+ driver: Driver;
56
+ env: E2eEnv;
57
+ /** where the one "dropped WARDEN_LAUNCH_ARGS on Android" line goes; default stderr */
58
+ log?: (line: string) => void;
59
+ /** time for the `wait: "quiet"` content poll; default the system clock */
60
+ clock?: Clock;
61
+ };
62
+ export declare function createApp({ session, driver, env, log, clock }: AppDeps): App;
@@ -0,0 +1,26 @@
1
+ import type { ConsoleLine, DevtoolsFacet, DevtoolsResult } from "./devtools/devtools.types";
2
+ /** Where the facet comes from: the session attaches it on the first `devtools()` and keeps it. */
3
+ export type DevtoolsSource = {
4
+ devtools(): Promise<DevtoolsResult<DevtoolsFacet>>;
5
+ peekDevtools(): DevtoolsFacet | undefined;
6
+ };
7
+ /**
8
+ * `device.devtools`: the app's JS runtime over Metro. Every call attaches on first use (the session shares one
9
+ * connection), so the members that are synchronous on a bare facet (`console`) are async here. `connect()` hands out
10
+ * the whole facet (`target`, `on`, `attachArtifacts`).
11
+ */
12
+ export type DeviceDevtools = {
13
+ evaluate: DevtoolsFacet["evaluate"];
14
+ reloadJs: DevtoolsFacet["reloadJs"];
15
+ /** the app's React components with rects, `devtools_unavailable` when the app has no DevTools hook */
16
+ tree: DevtoolsFacet["tree"];
17
+ network: DevtoolsFacet["network"];
18
+ console: {
19
+ lines(): Promise<ConsoleLine[]>;
20
+ clear(): Promise<void>;
21
+ mark(label: string): Promise<void>;
22
+ };
23
+ /** the attached facet; throws `SESSION` when the app cannot be reached */
24
+ connect(): Promise<DevtoolsFacet>;
25
+ };
26
+ export declare function createDeviceDevtools(source: DevtoolsSource): DeviceDevtools;
@@ -0,0 +1,43 @@
1
+ import type { AlertBackend } from "./driver/alerts";
2
+ import type { AlertInfo, Element } from "./driver/driver.types";
3
+ import type { Clock } from "./fixture-session";
4
+ import type { Locator } from "./screen/locator";
5
+ /** A loaded machine can show the sheet several seconds after `openUrl` returns. */
6
+ export declare const OPEN_LINK_WAIT_MS = 8000;
7
+ export declare function sheetlessFor(session: object): Set<string>;
8
+ /**
9
+ * Whether `openLink` is waiting on a link in `session`. Reads that answer system alerts (`test.use({ systemAlerts })`)
10
+ * leave alerts alone meanwhile: a read that cancelled (or pressed) the sheet first would lose the link, or hide the
11
+ * sheet from `openLink`, which then remembers the scheme as having none.
12
+ */
13
+ export declare function linkInFlight(session: object): boolean;
14
+ /** Runs `fn` with a link in flight in `session` (see {@link linkInFlight}). */
15
+ export declare function whileLinkInFlight<T>(session: object, fn: () => Promise<T>): Promise<T>;
16
+ /** iOS's "Open in “App”?" sheet, which only a link the test opened raises. */
17
+ export declare function isOpenInSheet(info: Pick<AlertInfo, "title">): boolean;
18
+ /** `myapp` for `myapp://x/y`; `https://example.com` for web links, which are handled per host. */
19
+ export declare function linkKey(url: string): string;
20
+ export type LinkDeps = {
21
+ clock: Clock;
22
+ /** `readAlert`: with a backend, one reader; without, both */
23
+ readAlert(backend?: AlertBackend, modalLooks?: number): Promise<AlertInfo | null>;
24
+ press(button: Element): Promise<void>;
25
+ /** is the app under test in front; undefined = cannot tell */
26
+ appInFront(): Promise<boolean | undefined>;
27
+ isVisible(locator: Locator): Promise<boolean>;
28
+ /** `sheetlessFor(session)` */
29
+ sheetless: Set<string>;
30
+ };
31
+ export type LinkPlan = {
32
+ key: string;
33
+ /** iOS, `accept` not false: there may be an "Open in …?" sheet to answer */
34
+ answersSheet: boolean;
35
+ until?: Locator;
36
+ waitMs: number;
37
+ };
38
+ /**
39
+ * Waits for a link `openUrl` just opened to be handled (see `Device.openLink`): the sheet answered, or the app in front
40
+ * with no sheet for 1.2 s, or `until` visible, or `waitMs` over. Reads alerts every 100 ms for the first second, every
41
+ * 250 ms after. While another app (SpringBoard, Safari) is in front no sheet-free verdict is reached, so it waits on.
42
+ */
43
+ export declare function waitForLink(deps: LinkDeps, plan: LinkPlan): Promise<void>;
@@ -0,0 +1,90 @@
1
+ import { type DeviceDevtools } from "./device-devtools";
2
+ import type { Driver, Orientation } from "./driver/driver.types";
3
+ import { type SettleDetail } from "./driver/settle";
4
+ import type { E2eEnv } from "./env";
5
+ import { type Clock, type FixtureSession } from "./fixture-session";
6
+ import { type DevicePerf, type PerfExec } from "./perf/device-perf";
7
+ import type { Locator } from "./screen/locator";
8
+ export type AlertAction = "accept" | "dismiss" | {
9
+ button: string;
10
+ };
11
+ export type OpenLinkOptions = {
12
+ /** answer iOS's "Open in …?" sheet with "Open". Default true. */
13
+ accept?: boolean;
14
+ /**
15
+ * Return as soon as this locator is visible and no sheet is up, instead of when the link is known to be handled.
16
+ * Use it when the link leads to a screen the app is not already showing; the wait still ends at `waitMs`
17
+ * (without an error: the next assertion reports the missing screen).
18
+ */
19
+ until?: Locator;
20
+ /** The longest `openLink` waits, in ms. Default 8000. */
21
+ waitMs?: number;
22
+ };
23
+ export type SnapshotTextOptions = {
24
+ interactiveOnly?: boolean;
25
+ };
26
+ export type SettleOptions = {
27
+ /** how long the UI must stay unchanged. Default 250 ms. */
28
+ quietMs?: number;
29
+ /** give up after this long. Default 3000 ms. */
30
+ timeout?: number;
31
+ };
32
+ export type Device = {
33
+ /**
34
+ * Answers the system alert / sheet on top. `accept` presses the first present of OK, Allow, Open,
35
+ * Continue, Yes, Done, Allow While Using App, Allow Once (else the last button); `dismiss` the first
36
+ * 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.
38
+ */
39
+ alert(action: AlertAction): Promise<boolean>;
40
+ /**
41
+ * Opens a URL from outside the app. On iOS it accepts the "Open in …?" sheet when one shows (pressing again if it
42
+ * stays up) and returns once the link is handled: the sheet was answered, or none showed for 1.2 s while the app is
43
+ * in front (so a link without a sheet costs about 1.3 s, and later links with the same scheme in this session return
44
+ * as soon as the app is in front). While another app (SpringBoard, Safari) is in front it keeps looking for the sheet
45
+ * for up to `waitMs` (8 s). Pass `until` to return when the target screen is visible.
46
+ */
47
+ openLink(url: string, options?: OpenLinkOptions): Promise<void>;
48
+ home(): Promise<void>;
49
+ /** Android back key; `INVALID_ARGUMENT` on iOS (there is none). */
50
+ back(): Promise<void>;
51
+ orientation(set?: Orientation): Promise<Orientation>;
52
+ appearance(set?: "light" | "dark"): Promise<"light" | "dark">;
53
+ clipboard(): Promise<string>;
54
+ setClipboard(text: string): Promise<void>;
55
+ /**
56
+ * Dismisses the soft keyboard when the snapshot shows one, else does nothing. iOS presses Enter
57
+ * (which may submit the focused field); Android presses back.
58
+ */
59
+ hideKeyboard(): Promise<void>;
60
+ /** Saves `<attemptDir>/screenshots/NNN-<name>.png` and returns the path. */
61
+ screenshot(name: string): Promise<string>;
62
+ /** The accessibility tree as text (`warden ui snapshot` format). */
63
+ snapshot(options?: SnapshotTextOptions): Promise<string>;
64
+ /**
65
+ * Waits for a quiet UI: the driver's own settle, else snapshot-hash polling. Resolves `{ quiet, waitedMs }` and,
66
+ * when known, how it decided: `via` (`stream`: the app's run loop went idle; `snapshot`: the helper fell back to
67
+ * comparing screens; `hash`: warden polled snapshot hashes, always on Android), a `reason` for a not-quiet end
68
+ * (`frontmost_mismatch`: the launched app never came to the front) and the `frontmostPid` the helper saw.
69
+ */
70
+ settle(options?: SettleOptions): Promise<SettleDetail>;
71
+ /**
72
+ * The app's JS runtime over Metro (evaluate, React tree, console, network, reload); `undefined` without
73
+ * `WARDEN_METRO_URL` (a release build). The first access starts attaching, so console capture begins there.
74
+ */
75
+ readonly devtools: DeviceDevtools | undefined;
76
+ /**
77
+ * Perf profiling of the app (`warden perf` over the CLI that started the run): `start`, `mark`, then `stop` for the
78
+ * typed report. The report is copied under the attempt's `perf/`; budgets from `warden.config.ts` fail `stop`.
79
+ */
80
+ readonly perf: DevicePerf;
81
+ };
82
+ export type DeviceDeps = {
83
+ session: FixtureSession;
84
+ driver: Driver;
85
+ env: E2eEnv;
86
+ clock?: Clock;
87
+ /** runs `warden …` for `device.perf`; default spawns `WARDEN_BIN` */
88
+ perfExec?: PerfExec;
89
+ };
90
+ export declare function createDevice({ session, driver, env, clock, perfExec }: DeviceDeps): Device;
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ import { type DevtoolsResult, type ExceptionDetails } from "./devtools.types";
3
+ export type CdpOptions = {
4
+ /** injectable for tests; defaults to the global one */
5
+ WebSocket?: typeof WebSocket;
6
+ /** connect timeout and the default per-request timeout */
7
+ timeoutMs?: number;
8
+ };
9
+ export type CdpClient = {
10
+ /** `Runtime.evaluate`-style JSON-RPC call; resolves with the reply's `result`. */
11
+ send: (method: string, params?: Record<string, unknown>, options?: {
12
+ timeoutMs?: number;
13
+ }) => Promise<DevtoolsResult<unknown>>;
14
+ /** Subscribe to a CDP event by method name; returns the unsubscribe function. */
15
+ on: (event: string, handler: (params: unknown) => void) => () => void;
16
+ /** Called when the socket closes on its own (not after `close()`). */
17
+ onClose: (handler: () => void) => () => void;
18
+ /** Every event of every method, in arrival order (a wildcard `on`). */
19
+ onAny: (handler: (method: string, params: unknown) => void) => () => void;
20
+ /**
21
+ * `Runtime.addBinding`, then probe `typeof <name>`: legacy Hermes (RN <= 0.72) acknowledges the call but never
22
+ * installs the function, so `available` is false and `evaluateWithBinding` is refused.
23
+ */
24
+ addBinding: (name: string) => Promise<DevtoolsResult<{
25
+ available: boolean;
26
+ }>>;
27
+ /**
28
+ * Run `script` (which must call the binding with JSON carrying `requestId`) and resolve with the parsed payload of
29
+ * the matching `Runtime.bindingCalled`.
30
+ */
31
+ evaluateWithBinding: (script: string, requestId: string, timeoutMs?: number) => Promise<DevtoolsResult<unknown>>;
32
+ /** `undefined` until `addBinding` ran */
33
+ readonly bindingAvailable: boolean | undefined;
34
+ close: () => void;
35
+ };
36
+ export declare const exceptionDetailsSchema: z.ZodObject<{
37
+ text: z.ZodOptional<z.ZodString>;
38
+ lineNumber: z.ZodOptional<z.ZodNumber>;
39
+ columnNumber: z.ZodOptional<z.ZodNumber>;
40
+ url: z.ZodOptional<z.ZodString>;
41
+ exception: z.ZodOptional<z.ZodObject<{
42
+ type: z.ZodOptional<z.ZodString>;
43
+ subtype: z.ZodOptional<z.ZodString>;
44
+ className: z.ZodOptional<z.ZodString>;
45
+ description: z.ZodOptional<z.ZodString>;
46
+ value: z.ZodOptional<z.ZodUnknown>;
47
+ }, z.core.$loose>>;
48
+ }, z.core.$loose>;
49
+ /** Parse the `exceptionDetails` of a `Runtime.evaluate` reply, if it has one. */
50
+ export declare function readEvaluateReply(reply: unknown): {
51
+ value: unknown;
52
+ type: string | undefined;
53
+ exceptionDetails: ExceptionDetails | undefined;
54
+ };
55
+ /** One line for a JS exception: the error's `description` (`Name: message` + stack), else the CDP text. */
56
+ export declare function describeException(details: ExceptionDetails): string;
57
+ /**
58
+ * The `Origin` Metro's inspector proxy requires: it rejects a debugger upgrade with no Origin (the 401 a bare
59
+ * `new WebSocket(url)` gets) and accepts its own base URL or a local hostname. Always the 127.0.0.1 origin of the
60
+ * socket's port: verified live against Expo's Metro, where `http://localhost:<port>` is accepted by the handshake
61
+ * but the socket is then dropped (1006), while `http://127.0.0.1:<port>` works whatever host the socket dials.
62
+ */
63
+ export declare function inspectorOrigin(wsUrl: string): string | undefined;
64
+ /** Open a CDP JSON-RPC session over a WebSocket (Metro's `/inspector/debug`). */
65
+ export declare function openCdp(wsUrl: string, options?: CdpOptions): Promise<DevtoolsResult<CdpClient>>;
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import type { CdpTap, ConsoleLine } from "./devtools.types";
3
+ import type { DevtoolsSession } from "./session";
4
+ declare const remoteObjectSchema: z.ZodObject<{
5
+ type: z.ZodString;
6
+ subtype: z.ZodOptional<z.ZodString>;
7
+ className: z.ZodOptional<z.ZodString>;
8
+ value: z.ZodOptional<z.ZodUnknown>;
9
+ unserializableValue: z.ZodOptional<z.ZodString>;
10
+ description: z.ZodOptional<z.ZodString>;
11
+ preview: z.ZodOptional<z.ZodObject<{
12
+ subtype: z.ZodOptional<z.ZodString>;
13
+ overflow: z.ZodOptional<z.ZodBoolean>;
14
+ properties: z.ZodOptional<z.ZodArray<z.ZodObject<{
15
+ name: z.ZodString;
16
+ type: z.ZodString;
17
+ subtype: z.ZodOptional<z.ZodString>;
18
+ value: z.ZodOptional<z.ZodString>;
19
+ }, z.core.$loose>>>;
20
+ }, z.core.$loose>>;
21
+ }, z.core.$loose>;
22
+ type RemoteObject = z.infer<typeof remoteObjectSchema>;
23
+ /** The text of a console call and each argument rendered separately. */
24
+ export declare function formatConsoleArgs(args: readonly RemoteObject[]): {
25
+ text: string;
26
+ args: string[];
27
+ };
28
+ export type ConsoleOptions = {
29
+ /** lines kept (default 500) */
30
+ ring?: number;
31
+ /** called with every line as it is recorded, including marks */
32
+ sink?: (line: ConsoleLine) => void;
33
+ now?: () => number;
34
+ };
35
+ export type ConsoleCapture = {
36
+ /** feed it CDP events (a session tap) */
37
+ handle: CdpTap;
38
+ lines: () => ConsoleLine[];
39
+ /** empties the buffer; replayed old lines are still recognised afterwards */
40
+ clear: () => void;
41
+ /** inserts a `note` line, to find a point in time in the log */
42
+ mark: (label: string) => void;
43
+ writeJsonl: (path: string) => Promise<void>;
44
+ };
45
+ /**
46
+ * Collect `Runtime.consoleAPICalled` / `Runtime.exceptionThrown` into a ring.
47
+ *
48
+ * Replay: Hermes keeps a buffer of recent console messages and sends it again on every `Runtime.enable`, so
49
+ * each (re)connect re-delivers old lines. Events flagged `replay` (they arrived during the connect handshake)
50
+ * are matched against what was recorded earlier by `(timestamp, text)` with counts, so a burst of identical
51
+ * lines is kept once per real occurrence. Live events are never de-duplicated: a loop may log the same text
52
+ * within one millisecond.
53
+ */
54
+ export declare function createConsoleCapture(options?: ConsoleOptions): ConsoleCapture;
55
+ /** Capture a connected session's console from now on. `detach()` stops it. */
56
+ export declare function attachConsole(session: DevtoolsSession, options?: ConsoleOptions): ConsoleCapture & {
57
+ detach: () => void;
58
+ };
59
+ export {};