@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.
- package/LICENSE +21 -0
- package/chunk-e6pn5ygy.mjs +5832 -0
- package/index.d.ts +1 -0
- package/index.mjs +1047 -0
- package/package.json +49 -4
- package/preload.d.ts +1 -0
- package/preload.mjs +4788 -0
- package/types/core/src/allocate.d.ts +23 -0
- package/types/core/src/builds/app-option.schema.d.ts +37 -0
- package/types/core/src/builds/install.d.ts +95 -0
- package/types/core/src/builds/resolve.d.ts +74 -0
- package/types/core/src/config.defaults.d.ts +25 -0
- package/types/core/src/exec.d.ts +18 -0
- package/types/core/src/liveness.d.ts +12 -0
- package/types/core/src/providers/android.d.ts +132 -0
- package/types/core/src/providers/health.d.ts +45 -0
- package/types/core/src/providers/provider.types.d.ts +56 -0
- package/types/core/src/store.d.ts +105 -0
- package/types/core/src/types.d.ts +126 -0
- package/types/e2e/src/alert-buttons.d.ts +41 -0
- package/types/e2e/src/alert-reader.d.ts +25 -0
- package/types/e2e/src/app.d.ts +62 -0
- package/types/e2e/src/device-devtools.d.ts +26 -0
- package/types/e2e/src/device-link.d.ts +43 -0
- package/types/e2e/src/device.d.ts +90 -0
- package/types/e2e/src/devtools/cdp.d.ts +65 -0
- package/types/e2e/src/devtools/console.d.ts +59 -0
- package/types/e2e/src/devtools/devtools.types.d.ts +169 -0
- package/types/e2e/src/devtools/facet.d.ts +28 -0
- package/types/e2e/src/devtools/inspector.d.ts +48 -0
- package/types/e2e/src/devtools/jsonl.d.ts +4 -0
- package/types/e2e/src/devtools/native-log.d.ts +50 -0
- package/types/e2e/src/devtools/network.d.ts +33 -0
- package/types/e2e/src/devtools/network.script.d.ts +24 -0
- package/types/e2e/src/devtools/session.d.ts +82 -0
- package/types/e2e/src/devtools/tree.d.ts +23 -0
- package/types/e2e/src/devtools/tree.script.d.ts +33 -0
- package/types/e2e/src/driver/alerts.d.ts +29 -0
- package/types/e2e/src/driver/android/adb-session.d.ts +52 -0
- package/types/e2e/src/driver/android/android-alert.d.ts +4 -0
- package/types/e2e/src/driver/android/android-animations.d.ts +29 -0
- package/types/e2e/src/driver/android/android-app.d.ts +32 -0
- package/types/e2e/src/driver/android/android-argv.d.ts +122 -0
- package/types/e2e/src/driver/android/android-device.d.ts +44 -0
- package/types/e2e/src/driver/android/android-driver.d.ts +60 -0
- package/types/e2e/src/driver/android/android-dump.d.ts +46 -0
- package/types/e2e/src/driver/android/android-state.d.ts +69 -0
- package/types/e2e/src/driver/android/uiad-backend.d.ts +63 -0
- package/types/e2e/src/driver/android/uiad-client.d.ts +60 -0
- package/types/e2e/src/driver/android/uiad-device.d.ts +70 -0
- package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +8 -0
- package/types/e2e/src/driver/android/uiad-ops.d.ts +62 -0
- package/types/e2e/src/driver/android/uiad-protocol.d.ts +209 -0
- package/types/e2e/src/driver/android/uiautomator.d.ts +33 -0
- package/types/e2e/src/driver/driver.types.d.ts +231 -0
- package/types/e2e/src/driver/errors.d.ts +13 -0
- package/types/e2e/src/driver/gesture-geometry.d.ts +48 -0
- package/types/e2e/src/driver/ios/hid-recovery.d.ts +36 -0
- package/types/e2e/src/driver/ios/ios-driver.d.ts +63 -0
- package/types/e2e/src/driver/ios/ios-session.d.ts +30 -0
- package/types/e2e/src/driver/ios/locate-helper.d.ts +38 -0
- package/types/e2e/src/driver/ios/normalize.d.ts +11 -0
- package/types/e2e/src/driver/ios/simctl.d.ts +102 -0
- package/types/e2e/src/driver/ios/simd-client.d.ts +68 -0
- package/types/e2e/src/driver/ios/simd-protocol.d.ts +433 -0
- package/types/e2e/src/driver/ios/simd-transport.d.ts +43 -0
- package/types/e2e/src/driver/ios/type-budget.d.ts +6 -0
- package/types/e2e/src/driver/observation-cache.d.ts +66 -0
- package/types/e2e/src/driver/open.d.ts +29 -0
- package/types/e2e/src/driver/settle.d.ts +43 -0
- package/types/e2e/src/driver/span-detail.d.ts +12 -0
- package/types/e2e/src/driver/traced-driver.d.ts +14 -0
- package/types/e2e/src/driver/typing.d.ts +48 -0
- package/types/e2e/src/driver/unwrap.d.ts +13 -0
- package/types/e2e/src/env.d.ts +78 -0
- package/types/e2e/src/errors.d.ts +26 -0
- package/types/e2e/src/expect/expect.d.ts +27 -0
- package/types/e2e/src/expect/matchers.d.ts +72 -0
- package/types/e2e/src/fixture-session.d.ts +38 -0
- package/types/e2e/src/fixtures.d.ts +14 -0
- package/types/e2e/src/index.d.ts +11 -0
- package/types/e2e/src/perf/budgets.d.ts +33 -0
- package/types/e2e/src/perf/device-perf.d.ts +45 -0
- package/types/e2e/src/perf/perf.types.d.ts +59 -0
- package/types/e2e/src/preload.d.ts +29 -0
- package/types/e2e/src/runner/artifacts.d.ts +13 -0
- package/types/e2e/src/runner/attempt.d.ts +63 -0
- package/types/e2e/src/runner/callsite.d.ts +2 -0
- package/types/e2e/src/runner/failure.d.ts +40 -0
- package/types/e2e/src/runner/fixtures.types.d.ts +60 -0
- package/types/e2e/src/runner/results.d.ts +48 -0
- package/types/e2e/src/runner/scope.d.ts +64 -0
- package/types/e2e/src/runner/span.d.ts +40 -0
- package/types/e2e/src/runner/step.d.ts +7 -0
- package/types/e2e/src/runner/test.d.ts +59 -0
- package/types/e2e/src/runner/trace.d.ts +87 -0
- package/types/e2e/src/screen/actions.d.ts +93 -0
- package/types/e2e/src/screen/context.d.ts +134 -0
- package/types/e2e/src/screen/field.d.ts +121 -0
- package/types/e2e/src/screen/locator.d.ts +233 -0
- package/types/e2e/src/screen/nodes.d.ts +53 -0
- package/types/e2e/src/screen/poll.d.ts +57 -0
- package/types/e2e/src/screen/queries.d.ts +34 -0
- package/types/e2e/src/screen/query.d.ts +60 -0
- package/types/e2e/src/screen/react-locator.d.ts +30 -0
- package/types/e2e/src/screen/render.d.ts +42 -0
- package/types/e2e/src/screen/roles.d.ts +24 -0
- package/types/e2e/src/screen/screen.d.ts +61 -0
- package/types/e2e/src/screen/selector.d.ts +38 -0
- package/types/e2e/src/screen/text.d.ts +13 -0
- package/types/e2e/src/session.d.ts +124 -0
- package/types/e2e/src/traced-device.d.ts +9 -0
- package/types/e2e/src/version.d.ts +7 -0
- package/types/types/src/result.d.ts +12 -0
- 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 {};
|