@delacour/warden-e2e 0.0.0-stage → 0.2.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-7ns30pbp.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,169 @@
1
+ import type { Result } from "../../../types/src/result";
2
+ import type { SelectedTarget } from "./inspector";
3
+ import type { DevtoolsSession } from "./session";
4
+ /** Why a devtools call failed. `devtools_unavailable` = no dev client to talk to (release build, no Metro, no target). */
5
+ export type DevtoolsErrorCode = "devtools_unavailable" | "device_mismatch" | "cdp_closed" | "cdp_timeout" | "cdp_protocol" | "binding_unavailable" | "eval_failed" | "reload_failed" | "io_failed";
6
+ export type DevtoolsError = {
7
+ code: DevtoolsErrorCode;
8
+ message: string;
9
+ };
10
+ export type DevtoolsResult<T> = Result<T, DevtoolsError>;
11
+ /** The `Runtime.exceptionDetails` object of a failed `Runtime.evaluate`. */
12
+ export type ExceptionDetails = {
13
+ text?: string;
14
+ lineNumber?: number;
15
+ columnNumber?: number;
16
+ url?: string;
17
+ exception?: {
18
+ type?: string;
19
+ subtype?: string;
20
+ className?: string;
21
+ description?: string;
22
+ value?: unknown;
23
+ };
24
+ };
25
+ /** Failure of `DevtoolsSession.evaluate`: a thrown JS exception carries `exceptionDetails`. */
26
+ export type EvaluateError = {
27
+ message: string;
28
+ code?: DevtoolsErrorCode;
29
+ exceptionDetails?: ExceptionDetails;
30
+ };
31
+ /** The slice of `Response` the devtools code reads; the global `fetch` satisfies it. */
32
+ export type FetchResponse = {
33
+ ok: boolean;
34
+ status: number;
35
+ text(): Promise<string>;
36
+ };
37
+ /** Injectable `fetch` (the global one is assignable). */
38
+ export type Fetch = (input: string, init?: {
39
+ method?: string;
40
+ signal?: AbortSignal;
41
+ }) => Promise<FetchResponse>;
42
+ export type Sleep = (ms: number) => Promise<void>;
43
+ export declare function devtoolsErr(code: DevtoolsErrorCode, message: string): {
44
+ success: false;
45
+ error: DevtoolsError;
46
+ };
47
+ /** One CDP event as the session saw it. `replay` = it arrived while the connect handshake was still running. */
48
+ export type CdpTapEvent = {
49
+ method: string;
50
+ params: unknown;
51
+ /** 1 for the first socket, +1 for every reconnect */
52
+ connection: number;
53
+ replay: boolean;
54
+ };
55
+ export type CdpTap = (event: CdpTapEvent) => void;
56
+ export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
57
+ /** One captured console message (`console`), uncaught exception (`exception`) or `mark()` (`note`). */
58
+ export type ConsoleLine = {
59
+ /** epoch ms (the runtime's own timestamp when it sent one) */
60
+ ts: number;
61
+ level: ConsoleLevel;
62
+ kind: "console" | "exception" | "note";
63
+ text: string;
64
+ /** top stack frame; `line`/`col` are 1-based */
65
+ source?: {
66
+ url: string;
67
+ line: number;
68
+ col: number;
69
+ };
70
+ /** each console argument rendered on its own (console messages only) */
71
+ args?: string[];
72
+ };
73
+ /** One finished `fetch` of the app (Metro's own traffic excluded). */
74
+ export type NetworkEntry = {
75
+ /** host-assigned, increasing */
76
+ id: number;
77
+ /** request start, epoch ms */
78
+ ts: number;
79
+ method: string;
80
+ url: string;
81
+ /** 0 when the request failed before a response */
82
+ status: number;
83
+ durationMs: number;
84
+ requestHeaders: Record<string, string>;
85
+ responseHeaders: Record<string, string>;
86
+ requestBody?: string;
87
+ responseBody?: string;
88
+ /** the body was cut at 64 KB */
89
+ responseBodyTruncated?: boolean;
90
+ error?: string;
91
+ };
92
+ export type NetworkQuery = {
93
+ last?: number;
94
+ id?: number;
95
+ };
96
+ export type TreeOptions = {
97
+ /** components returned, at most 5000 (default 5000) */
98
+ max?: number;
99
+ /** only this screen's subtree (a react-navigation / expo-router route name), whether or not it is the focused one */
100
+ screen?: string;
101
+ /** keep inactive screens and hidden subtrees, flagged `inactive` */
102
+ all?: boolean;
103
+ /** multiply measured rects by this: Android reports dp, the driver speaks px (iOS points are dp already) */
104
+ pixelRatio?: number;
105
+ /** give up after this long (default 5 s) */
106
+ timeoutMs?: number;
107
+ };
108
+ /** One React component of the app that carries a testID, a label, text or a screen. */
109
+ export type TreeComponent = {
110
+ /** index in `components` (preorder) */
111
+ id: number;
112
+ name: string;
113
+ testID?: string;
114
+ /** accessibilityLabel / aria-label */
115
+ label?: string;
116
+ /** the strings rendered inside it, concatenated */
117
+ text?: string;
118
+ /** `id` of the enclosing component, null for a top-level one */
119
+ parent: number | null;
120
+ /** window coordinates; absent when the view could not be measured */
121
+ rect?: {
122
+ x: number;
123
+ y: number;
124
+ width: number;
125
+ height: number;
126
+ };
127
+ /** inside an inactive screen or hidden subtree (only with `all`) */
128
+ inactive?: true;
129
+ };
130
+ export type ReactTree = {
131
+ /** route name of the focused screen, when the app uses react-navigation or expo-router */
132
+ screen?: string;
133
+ components: TreeComponent[];
134
+ /** more components existed than `max` */
135
+ truncated: boolean;
136
+ };
137
+ /** Everything `device.devtools` (T6) and the failure capture (T3) use. */
138
+ export type DevtoolsFacet = {
139
+ evaluate: (expression: string, options?: {
140
+ await?: boolean;
141
+ timeoutMs?: number;
142
+ }) => Promise<Result<unknown, EvaluateError>>;
143
+ reloadJs: () => Promise<DevtoolsResult<{
144
+ method: "expo" | "devsettings" | "http";
145
+ }>>;
146
+ console: {
147
+ lines: () => ConsoleLine[];
148
+ clear: () => void;
149
+ mark: (label: string) => void;
150
+ };
151
+ network: {
152
+ /** drains the page first, so it is current */
153
+ entries: (query?: NetworkQuery) => Promise<NetworkEntry[]>;
154
+ /** also discards what the page still holds, so it is a clean cut */
155
+ clear: () => Promise<void>;
156
+ };
157
+ /** The app's React components with rects (see `tree.script.ts`); `devtools_unavailable` when the app has no DevTools hook. */
158
+ tree: (options?: TreeOptions) => Promise<DevtoolsResult<ReactTree>>;
159
+ /** the page currently attached (changes after a reconnect) */
160
+ readonly target: SelectedTarget;
161
+ /** `reconnected` (new target attached) and `lost` (gave up reconnecting) */
162
+ on: DevtoolsSession["on"];
163
+ /** Writes `console.jsonl` and `network.jsonl` into `dir` (created); resolves with the two paths. */
164
+ attachArtifacts: (dir: string) => Promise<DevtoolsResult<{
165
+ console: string;
166
+ network: string;
167
+ }>>;
168
+ close: () => void;
169
+ };
@@ -0,0 +1,28 @@
1
+ import { type ConsoleLine, type DevtoolsFacet, type DevtoolsResult, type Fetch, type NetworkEntry, type Sleep } from "./devtools.types";
2
+ import { type DevtoolsDevice } from "./session";
3
+ export type OpenDevtoolsFacetOptions = {
4
+ /** the spec env: `metroUrl` is `WARDEN_METRO_URL`; absent = a release build, so `devtools_unavailable` */
5
+ env: {
6
+ metroUrl?: string;
7
+ };
8
+ device: DevtoolsDevice;
9
+ fetch: Fetch;
10
+ WebSocket?: typeof WebSocket;
11
+ log?: (line: string) => void;
12
+ sleep?: Sleep;
13
+ timeoutMs?: number;
14
+ /** keep request/response text in network entries (default false) */
15
+ includeBodies?: boolean;
16
+ pollMs?: number;
17
+ consoleRing?: number;
18
+ networkRing?: number;
19
+ /** called with each console line / network entry as it is recorded (the collector's files) */
20
+ consoleSink?: (line: ConsoleLine) => void;
21
+ networkSink?: (entry: NetworkEntry) => void;
22
+ };
23
+ /**
24
+ * Attach to the running dev client and start capturing its console and `fetch` traffic. This is what
25
+ * `device.devtools` and the failure capture use. Console capture starts before the socket opens so the
26
+ * runtime's replay of buffered messages is seen (and de-duplicated on later reconnects).
27
+ */
28
+ export declare function openDevtoolsFacet(o: OpenDevtoolsFacetOptions): Promise<DevtoolsResult<DevtoolsFacet>>;
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ import { type DevtoolsResult, type Fetch } from "./devtools.types";
3
+ /** One entry of Metro's `/json/list` (`/json`). */
4
+ export declare const inspectorTargetSchema: z.ZodObject<{
5
+ id: z.ZodString;
6
+ title: z.ZodOptional<z.ZodString>;
7
+ description: z.ZodOptional<z.ZodString>;
8
+ vm: z.ZodOptional<z.ZodString>;
9
+ deviceName: z.ZodOptional<z.ZodString>;
10
+ appId: z.ZodOptional<z.ZodString>;
11
+ webSocketDebuggerUrl: z.ZodString;
12
+ reactNative: z.ZodOptional<z.ZodObject<{
13
+ logicalDeviceId: z.ZodOptional<z.ZodString>;
14
+ capabilities: z.ZodOptional<z.ZodObject<{
15
+ prefersFuseboxFrontend: z.ZodOptional<z.ZodBoolean>;
16
+ }, z.core.$loose>>;
17
+ }, z.core.$loose>>;
18
+ }, z.core.$loose>;
19
+ export type InspectorTarget = z.infer<typeof inspectorTargetSchema>;
20
+ /** Which target a caller wants; every field narrows, none is required. */
21
+ export type TargetQuery = {
22
+ deviceName?: string;
23
+ logicalDeviceId?: string;
24
+ appId?: string;
25
+ };
26
+ export type SelectedTarget = {
27
+ /** Metro's inspector socket, host and port rewritten to the Metro URL */
28
+ wsUrl: string;
29
+ deviceName: string;
30
+ appId: string | undefined;
31
+ /** persist this to re-pin the same device after an app restart */
32
+ logicalDeviceId: string | undefined;
33
+ /** the C++ (Fusebox) connection, as opposed to the legacy bridge */
34
+ fusebox: boolean;
35
+ id: string;
36
+ title: string | undefined;
37
+ };
38
+ /**
39
+ * Metro's debuggable pages. `/status` must say `packager-status:running` (something else on the port is
40
+ * not Metro), then `/json/list` (`/json` on older Metro). Decoy `vm` entries are dropped.
41
+ */
42
+ export declare function listTargets(metroUrl: string, fetch: Fetch): Promise<DevtoolsResult<InspectorTarget[]>>;
43
+ /**
44
+ * Pick the page to debug: the pinned `logicalDeviceId`, else `deviceName`; then `appId`; then Fusebox over the
45
+ * legacy `React Native Bridge` page over anything, newest page id first. A pin or name that matches nothing is
46
+ * `device_mismatch` when Metro serves several devices (never attach to the wrong one), and ignored when it serves one.
47
+ */
48
+ export declare function selectTarget(targets: readonly InspectorTarget[], query: TargetQuery, metroUrl: string): DevtoolsResult<SelectedTarget>;
@@ -0,0 +1,4 @@
1
+ /** Replace `path` with one JSON object per line (parent directories created). */
2
+ export declare function writeJsonl(path: string, rows: readonly unknown[]): Promise<void>;
3
+ /** Append rows to `path`, creating it (and its directories) when missing. */
4
+ export declare function appendJsonl(path: string, rows: readonly unknown[]): Promise<void>;
@@ -0,0 +1,50 @@
1
+ import type { Exec } from "../../../core/src/exec";
2
+ export declare const NATIVE_LOG = "native.log";
3
+ /** The app a failing attempt ran: `native.log` and crash reports are read for it only. */
4
+ export type NativeTarget = {
5
+ platform: "ios";
6
+ udid: string;
7
+ bundleId: string;
8
+ } | {
9
+ platform: "android";
10
+ serial: string;
11
+ pkg: string;
12
+ };
13
+ export type CrashReport = {
14
+ name: string;
15
+ mtimeMs: number;
16
+ };
17
+ /** Filesystem seam: where crash reports are read from and `native.log` goes. */
18
+ export type NativeFs = {
19
+ listReports(dir: string): CrashReport[];
20
+ copy(from: string, to: string): void;
21
+ write(path: string, text: string): void;
22
+ };
23
+ export type NativeLogDeps = {
24
+ exec: Exec;
25
+ /** path of the `adb` binary */
26
+ adb: string;
27
+ fs: NativeFs;
28
+ /** `~/Library/Logs/DiagnosticReports` */
29
+ reportsDir: string;
30
+ };
31
+ export type NativeLogWindow = {
32
+ /** the attempt's directory; files are written into it */
33
+ dir: string;
34
+ /** when the attempt started (ms since epoch) */
35
+ sinceMs: number;
36
+ nowMs: number;
37
+ timeoutMs?: number;
38
+ };
39
+ export type NativeCapture = {
40
+ files: string[];
41
+ problems: string[];
42
+ };
43
+ export declare function realNativeFs(): NativeFs;
44
+ export declare function realNativeLogDeps(exec: Exec, adb: string): NativeLogDeps;
45
+ /**
46
+ * `native.log` (and iOS `.ips` crash reports) for a failed attempt, read after the fact: iOS
47
+ * `simctl spawn log show` for the app's process over the attempt's window, Android `logcat -d` for its pid
48
+ * plus the crash buffer. Reading afterwards needs no stream to start, stop or leak. Never throws.
49
+ */
50
+ export declare function captureNativeLog(target: NativeTarget, deps: NativeLogDeps, win: NativeLogWindow): Promise<NativeCapture>;
@@ -0,0 +1,33 @@
1
+ import type { DevtoolsResult, NetworkEntry, NetworkQuery, Sleep } from "./devtools.types";
2
+ import type { DevtoolsSession } from "./session";
3
+ export type NetworkOptions = {
4
+ /** entries kept, in the page and on the host (default 2000) */
5
+ ring?: number;
6
+ /** Metro's URL: requests to it (and its loopback aliases on the same port) are not recorded */
7
+ metroUrl?: string;
8
+ /** record request text and response text up to 64 KB (default false) */
9
+ includeBodies?: boolean;
10
+ /** how often the page is drained (default 500 ms) */
11
+ pollMs?: number;
12
+ /** injectable for tests; defaults to a timer */
13
+ sleep?: Sleep;
14
+ /** called with each new entry once */
15
+ sink?: (entry: NetworkEntry) => void;
16
+ };
17
+ export type NetworkCapture = {
18
+ /** Drains the page, then answers from the buffer: all, the `last` n, or the one with `id`. */
19
+ entries: (query?: NetworkQuery) => Promise<NetworkEntry[]>;
20
+ /** Drops the buffer and whatever the page still holds. Ids keep counting. */
21
+ clear: () => Promise<void>;
22
+ writeJsonl: (path: string) => Promise<void>;
23
+ /** stops polling (the page wrapper stays; it is harmless) */
24
+ stop: () => void;
25
+ };
26
+ /** Every URL prefix that is the same Metro: its origin plus the loopback aliases of a simulator and an emulator. */
27
+ export declare function metroSkipPrefixes(metroUrl: string): string[];
28
+ /**
29
+ * Record the app's `fetch` calls: patch `fetch` inside the page once, then drain its ring on a timer and
30
+ * whenever a caller asks. A new JS context (reload) loses the wrapper, so it is re-installed on reconnect and
31
+ * whenever a drain finds it missing; requests made before that are lost.
32
+ */
33
+ export declare function attachNetwork(session: DevtoolsSession, options?: NetworkOptions): Promise<DevtoolsResult<NetworkCapture>>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The page side of network capture: a `fetch` wrapper that records finished requests into an in-page ring,
3
+ * drained by `__warden_net_drain()`.
4
+ *
5
+ * This is source text evaluated inside Hermes by `Runtime.evaluate`, which does not transpile it, so it stays
6
+ * plain ES2015 on purpose: `function`, `var`, promises, no `async`/`await`, arrows, `?.` or `??`.
7
+ * `XMLHttpRequest` is not patched: RN's `fetch` is built on it, so patching both would count every request twice.
8
+ *
9
+ * Use as `(NETWORK_SCRIPT)(config, globalObject)`; see {@link networkInstallExpression}.
10
+ */
11
+ export declare const NETWORK_SCRIPT = "function (cfg, g) {\n if (g.__warden_net_patched) return \"already\";\n if (typeof g.fetch !== \"function\") return \"no fetch\";\n g.__warden_net_patched = true;\n var original = g.fetch;\n var log = [];\n var maxBody = cfg.maxBody || 65536;\n var textual = /json|text|xml|javascript|x-www-form-urlencoded|graphql/i;\n\n function skipped(url) {\n for (var i = 0; i < cfg.skipPrefixes.length; i++) {\n var prefix = cfg.skipPrefixes[i];\n if (url.indexOf(prefix) === 0 && \"/?#\".indexOf(url.charAt(prefix.length) || \"/\") !== -1) return true;\n }\n return false;\n }\n\n function headersToObject(h) {\n var out = {};\n if (!h) return out;\n if (Array.isArray(h)) {\n for (var i = 0; i < h.length; i++) out[String(h[i][0]).toLowerCase()] = String(h[i][1]);\n } else if (typeof h.forEach === \"function\") {\n h.forEach(function (value, key) { out[String(key).toLowerCase()] = String(value); });\n } else {\n for (var k in h) if (Object.prototype.hasOwnProperty.call(h, k)) out[k.toLowerCase()] = String(h[k]);\n }\n return out;\n }\n\n function requestBodyText(body) {\n if (typeof body === \"string\") return body.length > maxBody ? body.slice(0, maxBody) : body;\n if (body && typeof body.toString === \"function\" && body.constructor && body.constructor.name === \"URLSearchParams\") {\n return body.toString();\n }\n return undefined;\n }\n\n function push(entry) {\n log.push(entry);\n if (log.length > cfg.ring) log.shift();\n }\n\n g.__warden_net_drain = function () {\n var out = log;\n log = [];\n return JSON.stringify(out);\n };\n\n g.fetch = function (input, init) {\n var self = this;\n var args = arguments;\n var url = typeof input === \"string\" ? input : (input && input.url) || String(input);\n if (skipped(url)) return original.apply(self, args);\n\n var started = Date.now();\n var entry = {\n ts: started,\n method: String((init && init.method) || (input && input.method) || \"GET\").toUpperCase(),\n url: url,\n status: 0,\n durationMs: 0,\n requestHeaders: headersToObject((init && init.headers) || (input && input.headers)),\n responseHeaders: {}\n };\n if (cfg.includeBodies && init && init.body !== undefined && init.body !== null) {\n var sent = requestBodyText(init.body);\n if (sent !== undefined) entry.requestBody = sent;\n }\n\n return original.apply(self, args).then(function (res) {\n entry.status = res.status;\n entry.durationMs = Date.now() - started;\n entry.responseHeaders = headersToObject(res.headers);\n var type = entry.responseHeaders[\"content-type\"] || \"\";\n if (!cfg.includeBodies || !textual.test(type)) {\n push(entry);\n return res;\n }\n try {\n res.clone().text().then(function (text) {\n if (text.length > maxBody) {\n entry.responseBody = text.slice(0, maxBody);\n entry.responseBodyTruncated = true;\n } else {\n entry.responseBody = text;\n }\n push(entry);\n }, function () { push(entry); });\n } catch (e) {\n push(entry);\n }\n return res;\n }, function (error) {\n entry.durationMs = Date.now() - started;\n entry.error = error && error.message ? error.message : String(error);\n push(entry);\n throw error;\n });\n };\n return \"patched\";\n}";
12
+ export type NetworkScriptConfig = {
13
+ /** page-side ring size */
14
+ ring: number;
15
+ includeBodies: boolean;
16
+ /** URLs starting with any of these are not recorded (Metro) */
17
+ skipPrefixes: string[];
18
+ /** body text cap in characters (default 64 KB) */
19
+ maxBody?: number;
20
+ };
21
+ /** The expression to `Runtime.evaluate`: installs the wrapper once and answers `patched` / `already` / `no fetch`. */
22
+ export declare function networkInstallExpression(config: NetworkScriptConfig): string;
23
+ /** Reads the page's ring and empties it; `null` when the wrapper is not installed (fresh JS context). */
24
+ export declare const NETWORK_DRAIN_EXPRESSION = "typeof __warden_net_drain === \"function\" ? __warden_net_drain() : null";
@@ -0,0 +1,82 @@
1
+ import { type Result } from "../../../types/src/result";
2
+ import { type CdpTap, type DevtoolsError, type DevtoolsResult, type EvaluateError, type Fetch, type Sleep } from "./devtools.types";
3
+ import { type SelectedTarget } from "./inspector";
4
+ /** The one binding warden installs; scripts call `__warden_cb(JSON.stringify({ requestId, … }))`. */
5
+ export declare const WARDEN_BINDING = "__warden_cb";
6
+ export type DevtoolsDevice = {
7
+ /** the simulator's name as the app reports it */
8
+ deviceName?: string;
9
+ /** pin this device on a Metro shared by several (persist `session.target.logicalDeviceId`) */
10
+ logicalDeviceId?: string;
11
+ appId?: string;
12
+ };
13
+ export type ConnectDevtoolsOptions = {
14
+ /** `undefined` = no Metro: a release build, so `devtools_unavailable` */
15
+ metroUrl: string | undefined;
16
+ device: DevtoolsDevice;
17
+ fetch: Fetch;
18
+ WebSocket?: typeof WebSocket;
19
+ log?: (line: string) => void;
20
+ /** injectable for tests; defaults to a timer */
21
+ sleep?: Sleep;
22
+ /** per-request and connect timeout (default 10 s) */
23
+ timeoutMs?: number;
24
+ /** first pause between reconnect attempts (default 200 ms; grows to `maxReconnectBackoffMs` after 10 attempts) */
25
+ reconnectBackoffMs?: number;
26
+ /** longest pause between reconnect attempts (default 1 s) */
27
+ maxReconnectBackoffMs?: number;
28
+ /** reconnect attempts before giving up (default: no cap, `reconnectBudgetMs` decides) */
29
+ maxReconnectAttempts?: number;
30
+ /** how long one reconnect keeps trying before it emits `lost` (default 30 s) */
31
+ reconnectBudgetMs?: number;
32
+ /** injectable for tests; defaults to `Date.now` (with the time slept, measures `reconnectBudgetMs`) */
33
+ now?: () => number;
34
+ /** Called for every CDP event from the moment each socket opens, handshake included (so replays are seen). */
35
+ taps?: readonly CdpTap[];
36
+ /** how long `reloadJs` waits for the app to start tearing its JS context down (default 5 s) */
37
+ reloadWaitMs?: number;
38
+ };
39
+ export type DevtoolsSession = {
40
+ /** `Runtime.evaluate` by value. A thrown JS error fails with `exceptionDetails`. Waits out a reconnect. */
41
+ evaluate: (expression: string, options?: {
42
+ await?: boolean;
43
+ timeoutMs?: number;
44
+ }) => Promise<Result<unknown, EvaluateError>>;
45
+ /**
46
+ * Reloads this device's JS: `expo.reloadAppAsync()` evaluated in the app, else `DevSettings.reload()`, else (only
47
+ * when the app has neither) `POST <metro>/reload`, which reloads every device on that Metro. Never `Page.reload`:
48
+ * RN handles it inside the inspector dispatch on the main thread and a bridgeless host freezes. Fails with
49
+ * `reload_failed` when the JS context did not restart in time; resolves once the app has reconnected.
50
+ */
51
+ reloadJs: () => Promise<DevtoolsResult<{
52
+ method: ReloadMethod;
53
+ }>>;
54
+ /** the page currently attached (changes after a reconnect) */
55
+ readonly target: SelectedTarget;
56
+ /** false on legacy Hermes: `evaluateWithBinding` is refused */
57
+ readonly bindingAvailable: boolean;
58
+ /** raw CDP call on the current socket (waits out a reconnect) */
59
+ send: (method: string, params?: Record<string, unknown>, options?: {
60
+ timeoutMs?: number;
61
+ }) => Promise<DevtoolsResult<unknown>>;
62
+ /** CDP event subscription that survives reconnects */
63
+ onCdp: (method: string, handler: (params: unknown) => void) => () => void;
64
+ /** Tap on every CDP event, handshake events flagged `replay`; survives reconnects. */
65
+ onEvent: (tap: CdpTap) => () => void;
66
+ evaluateWithBinding: (script: string, requestId: string, timeoutMs?: number) => Promise<DevtoolsResult<unknown>>;
67
+ /** `reconnected` (new target attached) and `lost` (gave up reconnecting) */
68
+ on: <E extends keyof SessionEvents>(event: E, handler: (arg: SessionEvents[E]) => void) => () => void;
69
+ close: () => void;
70
+ };
71
+ export type ReloadMethod = "expo" | "devsettings" | "http";
72
+ export type SessionEvents = {
73
+ reconnected: SelectedTarget;
74
+ lost: DevtoolsError;
75
+ };
76
+ /**
77
+ * Attach to the app's Hermes runtime through Metro's inspector proxy. Connect sequence (same as Chrome's Fusebox
78
+ * frontend): `FuseboxClient.setClientMetadata`, `ReactNativeApplication.enable`, `Runtime.enable`,
79
+ * `Runtime.runIfWaitingForDebugger`, `Runtime.addBinding`. The session re-attaches on socket close and on
80
+ * `Runtime.executionContextsCleared` (JS reload), re-selecting the target because a reload gets a new page id.
81
+ */
82
+ export declare function connectDevtools(o: ConnectDevtoolsOptions): Promise<DevtoolsResult<DevtoolsSession>>;
@@ -0,0 +1,23 @@
1
+ import { type DevtoolsResult, type ReactTree, type Sleep, type TreeOptions } from "./devtools.types";
2
+ import { type DevtoolsSession } from "./session";
3
+ /** What `fetchTree` needs of a session. */
4
+ export type TreeSession = Pick<DevtoolsSession, "evaluate" | "evaluateWithBinding" | "bindingAvailable">;
5
+ export type FetchTreeDeps = {
6
+ /** injectable for tests; defaults to a timer */
7
+ sleep?: Sleep;
8
+ now?: () => number;
9
+ };
10
+ /**
11
+ * The app's React components (testID, label, text, screen) with window rects, read through the DevTools hook.
12
+ * The page script answers through the CDP binding when the runtime has one, else the host polls a page global.
13
+ * `pixelRatio` multiplies rects (Android reports dp, the driver speaks px).
14
+ */
15
+ export declare function fetchTree(session: TreeSession, options?: TreeOptions, deps?: FetchTreeDeps): Promise<DevtoolsResult<ReactTree>>;
16
+ export type FormatTreeOptions = {
17
+ /** include unmeasured and inactive components (inactive ones are marked) */
18
+ all?: boolean;
19
+ /** lines at most; the rest is summarised in a last line */
20
+ max?: number;
21
+ };
22
+ /** One line per component, indented by nesting: `View testID=save "Save changes" (20,700 350×48)`. */
23
+ export declare function formatTree(tree: ReactTree, options?: FormatTreeOptions): string[];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Page-side script that reads the app's React component tree through the React DevTools global hook and reports
3
+ * the components a test can address (testID, accessibility label, text, screens) together with their window rects.
4
+ *
5
+ * Runs inside Hermes untranspiled, so it is plain ES2015: no arrow functions, optional chaining, nullish
6
+ * coalescing or async functions, and no backticks or interpolation (it lives in a template literal here).
7
+ * Used as `(TREE_SCRIPT)(config, globalObject)`; it answers "started" at once and delivers the result later:
8
+ * through the CDP binding `config.binding` when the page has one, else as a JSON string in
9
+ * `globalObject.__warden_tree_results[requestId]` for the host to poll and delete.
10
+ *
11
+ * Inactive-screen heuristic (a subtree is skipped unless `all`, then it is kept and flagged `inactive`):
12
+ * - props `activityState === 0` (react-native-screens), `isPageFocused === false` (pager views), `freeze === true`
13
+ * (react-freeze, Suspense-style freezing), `aria-hidden === true`, `accessibilityElementsHidden === true`,
14
+ * `importantForAccessibility === "no-hide-descendants"`, or a style whose last `display` is `"none"`;
15
+ * - a fiber with both `route` and `navigation` props (a react-navigation / expo-router screen) whose
16
+ * `navigation.isFocused()` is false. `isFocused` accounts for parent navigators, so a focused tab inside an
17
+ * unfocused stack screen is unfocused too. When it throws the screen is treated as unknown, not as unfocused.
18
+ * The reported `screen` is `route.name` of the deepest focused screen. `activityState === 1` (transitioning)
19
+ * is kept. With `config.screen` pruning is off and only the subtree of the screen with that route name is reported.
20
+ */
21
+ export declare const TREE_SCRIPT = "function (cfg, g) {\n var rid = cfg.requestId;\n var MAX = typeof cfg.max === \"number\" && cfg.max > 0 ? Math.min(Math.floor(cfg.max), 5000) : 5000;\n var GUARD = typeof cfg.guardMs === \"number\" && cfg.guardMs > 0 ? cfg.guardMs : 1200;\n var ALL = cfg.all === true;\n var ONLY = typeof cfg.screen === \"string\" && cfg.screen !== \"\" ? cfg.screen : null;\n var BUDGET = 300000;\n var MAX_DEPTH = 1500;\n var MEASURE_CAP = 8000;\n var TEXT_CAP = 200;\n var HOST_NAMES = {\n RCTView: \"View\",\n RCTText: \"Text\",\n RCTVirtualText: \"Text\",\n RCTScrollView: \"ScrollView\",\n RCTImageView: \"Image\",\n RCTSinglelineTextInputView: \"TextInput\",\n RCTMultilineTextInputView: \"TextInput\",\n AndroidTextInput: \"TextInput\"\n };\n var EMPTY = {};\n var visits = 0;\n var budgetHit = false;\n var delivered = false;\n var finished = false;\n var timer = null;\n var bestScreen = null;\n var bestDepth = -1;\n var onlyFound = false;\n var hostMap = new Map();\n var hostList = [];\n var rects = [];\n var forest = [];\n var pending = 0;\n\n function isObj(v) {\n return v !== null && typeof v === \"object\";\n }\n\n function str(v) {\n return typeof v === \"string\" && v !== \"\" ? v : undefined;\n }\n\n function message(e) {\n return String(e && e.message ? e.message : e);\n }\n\n function deliver(result) {\n if (delivered) return;\n delivered = true;\n var cb = g[cfg.binding];\n if (typeof cb === \"function\") {\n cb(JSON.stringify({ requestId: rid, result: result }));\n return;\n }\n var store = g.__warden_tree_results;\n if (!isObj(store)) {\n store = {};\n g.__warden_tree_results = store;\n }\n store[rid] = JSON.stringify(result);\n }\n\n function lastDisplay(style, depth) {\n if (!style || depth > 6) return undefined;\n if (Array.isArray(style)) {\n var found;\n for (var i = 0; i < style.length; i++) {\n var d = lastDisplay(style[i], depth + 1);\n if (d !== undefined) found = d;\n }\n return found;\n }\n return typeof style === \"object\" ? style.display : undefined;\n }\n\n function isHidden(props) {\n return props.activityState === 0 ||\n props.isPageFocused === false ||\n props.freeze === true ||\n props[\"aria-hidden\"] === true ||\n props.accessibilityElementsHidden === true ||\n props.importantForAccessibility === \"no-hide-descendants\" ||\n lastDisplay(props.style, 0) === \"none\";\n }\n\n function typeName(t, depth) {\n if (typeof t === \"function\") return t.displayName || t.name || \"\";\n if (typeof t === \"string\") return t;\n if (isObj(t) && depth < 4) {\n return t.displayName || typeName(t.render, depth + 1) || typeName(t.type, depth + 1) || \"\";\n }\n return \"\";\n }\n\n function nameOf(f) {\n var t = f.type;\n if (f.tag === 5 && typeof t === \"string\") {\n return Object.prototype.hasOwnProperty.call(HOST_NAMES, t) ? HOST_NAMES[t] : t;\n }\n return typeName(t, 0) || \"Component\";\n }\n\n function rectHostOf(f) {\n if (f.tag === 5) return f;\n var cur = f.child;\n var n = 0;\n while (cur && n < 30) {\n if (cur.sibling) return null;\n if (cur.tag === 5) return cur;\n if (cur.tag === 6) return null;\n cur = cur.child;\n n++;\n }\n return null;\n }\n\n function registerHost(h) {\n if (!h) return -1;\n var i = hostMap.get(h);\n if (i === undefined) {\n if (hostList.length >= MEASURE_CAP) return -1;\n i = hostList.length;\n hostList.push(h);\n hostMap.set(h, i);\n }\n return i;\n }\n\n function clip(t) {\n return t.length > TEXT_CAP ? t.slice(0, TEXT_CAP) : t;\n }\n\n function buildChildren(f, ctx, depth) {\n var nodes = [];\n var strings = [];\n if (depth >= MAX_DEPTH) {\n budgetHit = true;\n return { nodes: nodes, strings: strings };\n }\n var c = f.child;\n while (c) {\n var r = buildFiber(c, ctx, depth + 1);\n for (var i = 0; i < r.nodes.length; i++) nodes.push(r.nodes[i]);\n for (var j = 0; j < r.strings.length; j++) strings.push(r.strings[j]);\n c = c.sibling;\n }\n return { nodes: nodes, strings: strings };\n }\n\n function buildFiber(f, ctx, depth) {\n visits++;\n if (visits > BUDGET) {\n budgetHit = true;\n return { nodes: [], strings: [] };\n }\n if (f.tag === 6) {\n var s = f.memoizedProps;\n if (typeof s === \"string\" || typeof s === \"number\") return { nodes: [], strings: [String(s)] };\n return { nodes: [], strings: [] };\n }\n var props = isObj(f.memoizedProps) ? f.memoizedProps : EMPTY;\n var route = props.route;\n var nav = props.navigation;\n var screenRoot = isObj(route) && typeof route.name === \"string\" && isObj(nav);\n var focused;\n if (screenRoot && typeof nav.isFocused === \"function\") {\n try {\n focused = nav.isFocused() === true;\n } catch (e) {\n focused = undefined;\n }\n }\n var hidden = isHidden(props) || focused === false;\n if (ONLY === null && hidden && !ALL) return { nodes: [], strings: [] };\n var inactive = ctx.inactive || (ONLY === null && hidden);\n var inScreen = ctx.inScreen;\n if (ONLY !== null && screenRoot && route.name === ONLY) {\n inScreen = true;\n onlyFound = true;\n }\n if (ONLY === null && screenRoot && focused === true && !inactive && depth > bestDepth) {\n bestDepth = depth;\n bestScreen = route.name;\n }\n var childCtx = ctx;\n if (inactive !== ctx.inactive || inScreen !== ctx.inScreen) childCtx = { inactive: inactive, inScreen: inScreen };\n var kids = buildChildren(f, childCtx, depth);\n\n var type = f.type;\n var isHost = f.tag === 5 && typeof type === \"string\";\n var isText = isHost && (type === \"RCTText\" || type === \"Text\");\n var isVirtual = isHost && type === \"RCTVirtualText\";\n var testID = str(props.testID);\n var label = str(props.accessibilityLabel) || str(props[\"aria-label\"]);\n var strings = kids.strings;\n var text;\n var outStrings = strings;\n if (isText) {\n var joined = clip(strings.join(\"\"));\n text = joined === \"\" ? undefined : joined;\n outStrings = [];\n }\n var emit = (ONLY === null || inScreen) &&\n (testID !== undefined || label !== undefined || screenRoot || (isText && text !== undefined));\n if (!emit) return { nodes: kids.nodes, strings: outStrings };\n if (!isText && strings.length > 0) {\n text = clip(strings.join(isVirtual ? \"\" : \" \"));\n if (!isVirtual) outStrings = [];\n }\n var node = {\n name: nameOf(f),\n testID: testID,\n label: label,\n text: text,\n hostIdx: registerHost(rectHostOf(f)),\n kids: kids.nodes,\n inactive: inactive,\n screenRoot: screenRoot,\n rect: undefined\n };\n return { nodes: [node], strings: outStrings };\n }\n\n function r1(v) {\n return Math.round(v * 10) / 10;\n }\n\n function accept(i, x, y, w, h) {\n if (finished) return;\n if (typeof x !== \"number\" || typeof y !== \"number\" || typeof w !== \"number\" || typeof h !== \"number\") return;\n if (!isFinite(x) || !isFinite(y) || !isFinite(w) || !isFinite(h) || w <= 0 || h <= 0) return;\n rects[i] = { x: r1(x), y: r1(y), w: r1(w), h: r1(h) };\n }\n\n function done() {\n pending--;\n if (pending === 0) finish();\n }\n\n function measure(i) {\n var sn = hostList[i].stateNode;\n var settled = false;\n function viaWindow(x, y, w, h) {\n if (settled) return;\n settled = true;\n accept(i, x, y, w, h);\n done();\n }\n function viaShadow(x, y, w, h, px, py) {\n if (settled) return;\n settled = true;\n accept(i, px, py, w, h);\n done();\n }\n try {\n if (isObj(sn)) {\n var pub = isObj(sn.canonical) ? sn.canonical.publicInstance : null;\n if (isObj(pub) && typeof pub.measureInWindow === \"function\") {\n pub.measureInWindow(viaWindow);\n return;\n }\n if (typeof sn.measureInWindow === \"function\") {\n sn.measureInWindow(viaWindow);\n return;\n }\n var fab = g.nativeFabricUIManager;\n if (sn.node && isObj(fab) && typeof fab.measure === \"function\") {\n fab.measure(sn.node, viaShadow);\n return;\n }\n var tag = typeof sn._nativeTag === \"number\"\n ? sn._nativeTag\n : (isObj(sn.canonical) && typeof sn.canonical.nativeTag === \"number\" ? sn.canonical.nativeTag : undefined);\n var proxy = g.nativeModuleProxy;\n var ui = isObj(proxy) ? proxy.UIManager : null;\n if (tag !== undefined && isObj(ui) && typeof ui.measureInWindow === \"function\") {\n ui.measureInWindow(tag, viaWindow);\n return;\n }\n }\n } catch (e) {\n }\n if (!settled) {\n settled = true;\n done();\n }\n }\n\n function sameRect(a, b) {\n if (a === undefined || b === undefined) return a === b;\n return Math.abs(a.x - b.x) <= 1 && Math.abs(a.y - b.y) <= 1 && Math.abs(a.w - b.w) <= 1 && Math.abs(a.h - b.h) <= 1;\n }\n\n function canAbsorb(p, c) {\n return !p.screenRoot &&\n !!p.inactive === !!c.inactive &&\n sameRect(p.rect, c.rect) &&\n (c.testID === undefined || c.testID === p.testID) &&\n (c.label === undefined || c.label === p.label);\n }\n\n function finalize(nodes) {\n var out = [];\n for (var i = 0; i < nodes.length; i++) {\n var n = nodes[i];\n n.rect = n.hostIdx >= 0 ? rects[n.hostIdx] : undefined;\n n.kids = finalize(n.kids);\n if (n.rect === undefined && n.testID === undefined) {\n for (var k = 0; k < n.kids.length; k++) out.push(n.kids[k]);\n continue;\n }\n while (n.kids.length === 1 && canAbsorb(n, n.kids[0])) {\n var c = n.kids[0];\n if (n.testID === undefined) n.testID = c.testID;\n if (n.label === undefined) n.label = c.label;\n if (n.text === undefined) n.text = c.text;\n n.screenRoot = n.screenRoot || c.screenRoot;\n n.kids = c.kids;\n }\n out.push(n);\n }\n return out;\n }\n\n var comps = [];\n var more = false;\n\n function flatten(nodes, parentId) {\n for (var i = 0; i < nodes.length; i++) {\n if (comps.length >= MAX) {\n more = true;\n return;\n }\n var n = nodes[i];\n var id = comps.length;\n var c = { id: id, name: n.name, parent: parentId };\n if (n.testID !== undefined) c.testID = n.testID;\n if (n.label !== undefined) c.label = n.label;\n if (n.text !== undefined) c.text = n.text;\n if (n.rect !== undefined) c.rect = n.rect;\n if (n.inactive) c.inactive = true;\n comps.push(c);\n flatten(n.kids, id);\n }\n }\n\n function finish() {\n if (finished) return;\n finished = true;\n if (timer !== null && typeof g.clearTimeout === \"function\") g.clearTimeout(timer);\n try {\n flatten(finalize(forest), null);\n var result = { components: comps, truncated: more || budgetHit };\n var screen = ONLY !== null ? (onlyFound ? ONLY : null) : bestScreen;\n if (screen !== null) result.screen = screen;\n deliver(result);\n } catch (e) {\n deliver({ error: message(e) });\n }\n }\n\n function countFibers(start) {\n var n = 0;\n var cur = start;\n while (cur && n < 20000) {\n n++;\n if (cur.child) {\n cur = cur.child;\n continue;\n }\n while (cur && cur !== start && !cur.sibling) cur = cur.return;\n if (!cur || cur === start) break;\n cur = cur.sibling;\n }\n return n;\n }\n\n try {\n var hook = g.__REACT_DEVTOOLS_GLOBAL_HOOK__;\n if (!isObj(hook)) {\n deliver({ error: \"no-hook\" });\n return \"started\";\n }\n var best = null;\n var bestCount = -1;\n var consider = function (set) {\n if (!set || typeof set.forEach !== \"function\") return;\n set.forEach(function (root) {\n var cur = root && root.current;\n if (!cur) return;\n var n = countFibers(cur);\n if (n > bestCount) {\n bestCount = n;\n best = cur;\n }\n });\n };\n if (typeof hook.getFiberRoots === \"function\") {\n if (isObj(hook.renderers) && typeof hook.renderers.forEach === \"function\") {\n hook.renderers.forEach(function (renderer, id) {\n consider(hook.getFiberRoots(id));\n });\n }\n if (best === null) consider(hook.getFiberRoots(1));\n }\n if (best === null) {\n deliver({ error: \"no-roots\" });\n return \"started\";\n }\n forest = buildChildren(best, { inactive: false, inScreen: false }, 0).nodes;\n if (hostList.length === 0) {\n finish();\n return \"started\";\n }\n pending = hostList.length + 1;\n for (var i = 0; i < hostList.length; i++) measure(i);\n done();\n if (!finished) {\n if (typeof g.setTimeout === \"function\") timer = g.setTimeout(finish, GUARD);\n else finish();\n }\n } catch (e) {\n deliver({ error: message(e) });\n }\n return \"started\";\n}";
22
+ export type TreeScriptConfig = {
23
+ requestId: string;
24
+ /** name of the CDP binding to report through (when the page has it) */
25
+ binding: string;
26
+ max?: number;
27
+ screen?: string;
28
+ all?: boolean;
29
+ /** how long to wait for native measurements before reporting what arrived (default 1200 ms) */
30
+ guardMs?: number;
31
+ };
32
+ /** Expression that runs `TREE_SCRIPT` against the page's global object; evaluates to "started". */
33
+ export declare function treeInstallExpression(config: TreeScriptConfig): string;
@@ -0,0 +1,29 @@
1
+ import type { AlertInfo, Driver, DriverResult } from "./driver.types";
2
+ /**
3
+ * Optional capabilities a driver can offer next to the frozen `Driver` interface, for `device.alert` and
4
+ * `device.openLink` (pattern: `RepeatingKeys` in `typing.ts`). A driver without them is still fully usable: the
5
+ * device layer falls back to `Driver.alert()` and to waiting out `openLink`'s window.
6
+ */
7
+ /**
8
+ * The iOS accessibility readers a system alert can be read through. `ax` is the host reader: it sees
9
+ * SpringBoard-hosted sheets ("Open in …?", permission prompts) and never touches the in-simulator bridge, so a
10
+ * stuck stream or a relaunched app cannot slow it. `axbridge` is the in-simulator bridge: it also sees prompts the
11
+ * app owns (the Keychain "Save Password?" sheet, app alerts), which `ax` reports as one empty root.
12
+ */
13
+ export type AlertBackend = "ax" | "axbridge";
14
+ /**
15
+ * `alert()` through ONE reader, so a screen without an alert costs one tree read where `Driver.alert()` reads both.
16
+ * `null` = this reader shows none. `{ buttons: [] }` = a modal is up whose buttons are not readable yet.
17
+ */
18
+ export type ReadsAlertOn = {
19
+ alertOn(backend: AlertBackend): DriverResult<AlertInfo | null>;
20
+ };
21
+ export declare function canReadAlertOn(driver: Driver): driver is Driver & ReadsAlertOn;
22
+ /**
23
+ * Whether the app with `bundleId` owns the foreground right now: a read of the helper's own state, no tree read and no
24
+ * waiting. `false` while SpringBoard (a system sheet), Safari or another app is in front.
25
+ */
26
+ export type ProbesFrontmost = {
27
+ isFrontmost(bundleId: string): DriverResult<boolean>;
28
+ };
29
+ export declare function canProbeFrontmost(driver: Driver): driver is Driver & ProbesFrontmost;
@@ -0,0 +1,52 @@
1
+ import type { DriverResult, Logger } from "../driver.types";
2
+ /**
3
+ * One long-lived `adb -s <serial> shell` per driver. Spawning `adb shell <cmd>` per call costs ~50-150 ms
4
+ * of adb round trip; writing to a resident shell costs the command itself. Text commands only: binary
5
+ * output (screenshots) must NEVER go through this session, the pty-less shell is not binary clean, so
6
+ * screenshots use a separate `adb exec-out screencap -p` process (`android-argv.ts: screencapArgv`).
7
+ *
8
+ * Wire format, one line per command:
9
+ * `(<cmd>) 2>&1 </dev/null; echo "__WDN_<id>_$?__"`
10
+ * stdout is accumulated until the sentinel for that id shows up; the text before it is the output and the
11
+ * number inside it the exit code. `</dev/null` keeps a stdin-reading command from eating the next line.
12
+ *
13
+ * Commands run strictly FIFO, one in flight. A timed-out command's shell state is unknowable, so the
14
+ * process is killed and the next `run` starts a fresh one. A process that dies mid-command fails that
15
+ * command with `E_HELPER_CRASHED` and is respawned on the next `run`; one respawn is allowed in a row
16
+ * (a respawned process that also dies before completing any command makes the session give up, every
17
+ * later `run` fails with `E_HELPER_CRASHED` until a new session is opened). A completed command resets
18
+ * the budget.
19
+ */
20
+ /** The slice of a child process the session needs (real: `spawnAdb`; tests may inject their own). */
21
+ export type AdbProcess = {
22
+ /** write text to the shell's stdin; throws when the pipe is closed */
23
+ write(data: string): void;
24
+ stdout: ReadableStream<Uint8Array>;
25
+ kill(): void;
26
+ /** resolves with the exit code once the process is gone */
27
+ exited: Promise<number>;
28
+ };
29
+ export type SpawnAdb = (argv: string[]) => AdbProcess;
30
+ /** `Bun.spawn` with stdin/stdout piped and stderr dropped (`2>&1` is inside every command line). */
31
+ export declare function spawnAdb(argv: string[], env?: Record<string, string>): AdbProcess;
32
+ export type AdbRunResult = {
33
+ stdout: string;
34
+ exitCode: number;
35
+ };
36
+ export type AdbSessionOptions = {
37
+ /** path of the `adb` binary (`androidTools(env).adb`) */
38
+ adb: string;
39
+ serial: string;
40
+ spawn?: SpawnAdb;
41
+ log?: Logger;
42
+ };
43
+ export type AdbSession = {
44
+ /** run one shell command line; `timeoutMs` defaults to 10 s */
45
+ run(cmd: string, opts?: {
46
+ timeoutMs?: number;
47
+ }): DriverResult<AdbRunResult>;
48
+ /** kill the shell; later `run`s fail. Safe to call twice. */
49
+ close(): Promise<void>;
50
+ };
51
+ export declare const DEFAULT_ADB_TIMEOUT_MS = 10000;
52
+ export declare function openAdbSession(options: AdbSessionOptions): AdbSession;
@@ -0,0 +1,4 @@
1
+ import type { AlertInfo, Element } from "../driver.types";
2
+ export { ANDROID_ALLOW_IDS, ANDROID_DENY_IDS } from "../../alert-buttons";
3
+ /** The alert on screen, or null. Title/message are best effort; at least one known button is required. */
4
+ export declare function detectAlert(flat: readonly Element[]): AlertInfo | null;