@delacour/warden-e2e 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{chunk-8em9kgsk.mjs → chunk-j9b17ade.mjs} +200 -37
- package/index.mjs +1 -1
- package/package.json +2 -2
- package/preload.mjs +61 -10
- package/types/core/src/affected/affected.schema.d.ts +185 -0
- package/types/core/src/batch/preset.schema.d.ts +59 -0
- package/types/core/src/builds/app-option.schema.d.ts +9 -1
- package/types/core/src/builds/builds.defaults.d.ts +62 -0
- package/types/core/src/builds/config.d.ts +122 -0
- package/types/core/src/builds/config.schema.d.ts +392 -0
- package/types/core/src/builds/define-config.d.ts +719 -0
- package/types/core/src/builds/kinds.d.ts +56 -0
- package/types/core/src/builds/native-ids.d.ts +14 -0
- package/types/core/src/builds/resolve.d.ts +26 -0
- package/types/core/src/duration.d.ts +5 -0
- package/types/core/src/exec.d.ts +2 -0
- package/types/core/src/ports.d.ts +76 -0
- package/types/core/src/providers/provider.types.d.ts +7 -0
- package/types/core/src/sims/slim.profile.d.ts +54 -0
- package/types/e2e/src/devtools/cdp.d.ts +7 -2
- package/types/e2e/src/devtools/devtools.types.d.ts +13 -0
- package/types/e2e/src/devtools/facet.d.ts +2 -0
- package/types/e2e/src/devtools/logbox.d.ts +27 -0
- package/types/e2e/src/devtools/logbox.script.d.ts +17 -0
- package/types/e2e/src/devtools/session.d.ts +8 -1
- package/types/e2e/src/driver/ios/ios-driver.d.ts +7 -6
- package/types/e2e/src/driver/ios/simd-protocol.d.ts +23 -0
- package/types/e2e/src/driver/ios/type-budget.d.ts +8 -4
- package/types/e2e/src/driver/observation-cache.d.ts +3 -1
- package/types/e2e/src/driver/typing.d.ts +10 -0
- package/types/e2e/src/preload.d.ts +5 -0
- package/types/e2e/src/screen/field.d.ts +11 -1
- package/types/e2e/src/screen/locator.d.ts +24 -5
- package/types/e2e/src/session.d.ts +4 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Platform } from "../types";
|
|
2
|
+
import { type BuildConfiguration } from "./builds.defaults";
|
|
3
|
+
import type { Project } from "./config";
|
|
4
|
+
import type { AppFramework } from "./native-ids";
|
|
5
|
+
/**
|
|
6
|
+
* What a cached build is for:
|
|
7
|
+
* - `dev`: Debug, JS from Metro at runtime.
|
|
8
|
+
* - `simulator`: Release for a simulator / emulator with no JS bundle embedded; a repack supplies the JS.
|
|
9
|
+
* - `release`: Release with the JS bundle embedded at build time (the key also covers the JS sources).
|
|
10
|
+
*/
|
|
11
|
+
export type BuildKind = "dev" | "simulator" | "release";
|
|
12
|
+
export declare const BUILD_KINDS: readonly ["dev", "simulator", "release"];
|
|
13
|
+
/** The kind `warden e2e` installs when the suite's `app` names none. */
|
|
14
|
+
export declare const DEFAULT_E2E_KIND: BuildKind;
|
|
15
|
+
/** The kind `warden ui open` / `warden dev` use when no `--kind` is given. */
|
|
16
|
+
export declare const DEFAULT_UI_KIND: BuildKind;
|
|
17
|
+
/** `projects[].kinds.<kind>`: overrides of one kind's built-in defaults. */
|
|
18
|
+
export type KindConfig = {
|
|
19
|
+
/** replaces the kind's built-in build command, per platform */
|
|
20
|
+
build?: {
|
|
21
|
+
ios?: string | undefined;
|
|
22
|
+
android?: string | undefined;
|
|
23
|
+
} | undefined;
|
|
24
|
+
/** env of the build and of the JS bundle step (`EXPO_PUBLIC_*`, `NODE_ENV`…); part of the JS key */
|
|
25
|
+
env?: Record<string, string> | undefined;
|
|
26
|
+
/** globs (relative to the project root, `!` excludes) of the JS sources the key covers; `simulator` / `release` only */
|
|
27
|
+
jsInputs?: string[] | undefined;
|
|
28
|
+
};
|
|
29
|
+
export type ProjectKinds = Partial<Record<BuildKind, KindConfig | undefined>>;
|
|
30
|
+
/** A kind's effective build settings: the built-in defaults with `projects[].kinds.<kind>` applied. */
|
|
31
|
+
export type KindSettings = {
|
|
32
|
+
kind: BuildKind;
|
|
33
|
+
build: Record<Platform, string>;
|
|
34
|
+
buildConfiguration: BuildConfiguration;
|
|
35
|
+
/** the app binary carries its JS (`release`); false = Metro or a repack supplies it */
|
|
36
|
+
embedsJs: boolean;
|
|
37
|
+
/** `native`: the key is the native fingerprint; `native+js`: also the `jsInputs` content */
|
|
38
|
+
fingerprint: "native" | "native+js";
|
|
39
|
+
env: Record<string, string>;
|
|
40
|
+
/** the JS sources the JS key (`simulator`, `release`) covers; undefined for `dev` */
|
|
41
|
+
jsInputs?: string[];
|
|
42
|
+
};
|
|
43
|
+
/** The built-in build command of `kind` (the default `projects[].kinds.<kind>.build` replaces). */
|
|
44
|
+
export declare function kindBuildCommand(kind: BuildKind, platform: Platform, framework: AppFramework): string;
|
|
45
|
+
/**
|
|
46
|
+
* `kind`'s settings for `project`. Precedence: `kinds.<kind>.build` > the built-in default. The project's top-level
|
|
47
|
+
* `build` / `fingerprint.include` / `fingerprint.jsInputs` belong to the kind-less flow and are not read here
|
|
48
|
+
* (`fingerprint.command`, `build.cache` and `build.prebuiltReactNative` still apply: they are not about the kind).
|
|
49
|
+
* JS inputs: `kinds.<kind>.jsInputs`, else `DEFAULT_JS_INPUTS` (all tracked files but `ios/`, `android/`, `node_modules`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveKind(project: Project, kind: BuildKind): KindSettings;
|
|
52
|
+
/**
|
|
53
|
+
* `project` as `kind` builds it: the build commands, configuration and env of `resolveKind`, and `jsInputs` set only
|
|
54
|
+
* when the kind's cache key covers the JS (`release`), so `runLocalBuild` / `computeCacheKey` run unchanged.
|
|
55
|
+
*/
|
|
56
|
+
export declare function projectForKind(project: Project, kind: BuildKind): Project;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Platform } from "../types";
|
|
2
|
+
/** How an app is run: Expo (`expo start`, a dev client) or bare React Native (`react-native start`, the app itself). */
|
|
3
|
+
export type AppFramework = "expo" | "react-native";
|
|
4
|
+
/**
|
|
5
|
+
* The app's framework from its `package.json`: `expo` when it depends on `expo`, `react-native` when
|
|
6
|
+
* only on `react-native`; undefined without either (or without a readable `package.json`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function appFramework(root: string): AppFramework | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* A bare React Native app's ids, read from its native projects: iOS `PRODUCT_BUNDLE_IDENTIFIER`
|
|
11
|
+
* (the template's `$(PRODUCT_NAME:rfc1034identifier)` filled in with `appName`), Android
|
|
12
|
+
* `applicationId`. A platform whose id can't be read (no native dir, a build variable) is left out.
|
|
13
|
+
*/
|
|
14
|
+
export declare function nativeBundleIds(root: string, appName: string): Partial<Record<Platform, string>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AsyncResult } from "../../../types/src/result";
|
|
2
|
+
import type { Platform } from "../types";
|
|
2
3
|
import type { InstallMode } from "./app-option.schema";
|
|
3
4
|
/** Where `ensure` found the app. */
|
|
4
5
|
export type ResolveSource = "installed" | "cache" | "eas" | "build";
|
|
@@ -10,6 +11,29 @@ export type EnsureResult = {
|
|
|
10
11
|
/** the app is on the device at `hash` (false when no device was given) */
|
|
11
12
|
installed: boolean;
|
|
12
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* What a resolve is doing right now, for a progress display: `fetching` the EAS build, `waiting` on another device's
|
|
16
|
+
* (or process's) fetch / build of the same hash, `building` locally, `repacking` JS into the simulator base,
|
|
17
|
+
* `installing` on a device (only while an install really runs), `ready` when the resolve is done.
|
|
18
|
+
*/
|
|
19
|
+
export type AppPhase = {
|
|
20
|
+
phase: "fetching";
|
|
21
|
+
source: "eas";
|
|
22
|
+
} | {
|
|
23
|
+
phase: "waiting";
|
|
24
|
+
} | {
|
|
25
|
+
phase: "building";
|
|
26
|
+
platform: Platform;
|
|
27
|
+
hash: string;
|
|
28
|
+
} | {
|
|
29
|
+
phase: "repacking";
|
|
30
|
+
platform: Platform;
|
|
31
|
+
jsKey: string;
|
|
32
|
+
} | {
|
|
33
|
+
phase: "installing";
|
|
34
|
+
} | {
|
|
35
|
+
phase: "ready";
|
|
36
|
+
};
|
|
13
37
|
export type EasStepOutcome = {
|
|
14
38
|
kind: "hit";
|
|
15
39
|
path: string;
|
|
@@ -63,6 +87,8 @@ export type ResolveSteps = {
|
|
|
63
87
|
/** local build into the cache; path */
|
|
64
88
|
build?: () => AsyncResult<string>;
|
|
65
89
|
log: (line: string) => void;
|
|
90
|
+
/** progress: where this resolve is (see `AppPhase`) */
|
|
91
|
+
onPhase?: (phase: AppPhase) => void;
|
|
66
92
|
};
|
|
67
93
|
/**
|
|
68
94
|
* The 5-step resolver, first hit wins: (1) already installed at this hash → nothing to do;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Result } from "../../types/src/result";
|
|
2
|
+
/** `10m` / `30s` / `2h` / `500ms` / bare seconds → milliseconds. */
|
|
3
|
+
export declare function parseDuration(input: string): Result<number>;
|
|
4
|
+
/** 125000 → `2m5s`. */
|
|
5
|
+
export declare function formatDuration(ms: number): string;
|
package/types/core/src/exec.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ExecOptions = {
|
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
/** aborting kills the command (SIGTERM), like a timeout */
|
|
11
11
|
signal?: AbortSignal;
|
|
12
|
+
/** each line of stdout and stderr as it arrives (no trailing newline); the result still carries the whole output */
|
|
13
|
+
onLine?: (line: string) => void;
|
|
12
14
|
};
|
|
13
15
|
/** Injectable process runner — providers take this so tests can fake `simctl`/`adb`. */
|
|
14
16
|
export type Exec = (cmd: readonly string[], opts?: ExecOptions) => Promise<ExecResult>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AsyncResult, Result } from "../../types/src/result";
|
|
2
|
+
import { type PidAlive } from "./liveness";
|
|
3
|
+
import type { Store } from "./store";
|
|
4
|
+
import type { Lease, Owner } from "./types";
|
|
5
|
+
/** Span used when a spec has no `:span` (mirrors salient's `pickPort` 20-port scan). */
|
|
6
|
+
export declare const DEFAULT_PORT_SPAN = 20;
|
|
7
|
+
export type PortRange = {
|
|
8
|
+
from: number;
|
|
9
|
+
span: number;
|
|
10
|
+
};
|
|
11
|
+
/** `8091:20` → `{ from: 8091, span: 20 }`; bare `8091` → span `DEFAULT_PORT_SPAN`. */
|
|
12
|
+
export declare function parsePortSpec(spec: string): Result<PortRange>;
|
|
13
|
+
export type ProbeOutcome = "free" | "busy" | "unsupported";
|
|
14
|
+
/**
|
|
15
|
+
* A failed probe bind → outcome. In use / forbidden is busy anywhere; a missing address or family is
|
|
16
|
+
* unsupported; any other error on the IPv6 wildcard is unsupported too (Bun reports a code-less
|
|
17
|
+
* "Failed to listen at ::" on hosts without IPv6), while on an IPv4 host it stays busy.
|
|
18
|
+
*/
|
|
19
|
+
export declare function classifyBindError(error: {
|
|
20
|
+
code?: string;
|
|
21
|
+
}, host: string): Exclude<ProbeOutcome, "free">;
|
|
22
|
+
/**
|
|
23
|
+
* Bind probe: listen on `port` and close immediately. With `host`, probes only that address;
|
|
24
|
+
* otherwise the port must bind on 127.0.0.1, 0.0.0.0 and `::` (hosts without IPv6 are skipped).
|
|
25
|
+
*/
|
|
26
|
+
export declare function isPortFree(port: number, host?: string): Promise<boolean>;
|
|
27
|
+
export type ClaimPortsRequest = {
|
|
28
|
+
owner: Owner;
|
|
29
|
+
from: number;
|
|
30
|
+
span: number;
|
|
31
|
+
/** ports wanted (default 1) */
|
|
32
|
+
count?: number;
|
|
33
|
+
/** candidate stride inside the range (default 1; 2 for emulator console ports) */
|
|
34
|
+
step?: number;
|
|
35
|
+
ttlMs: number;
|
|
36
|
+
label?: string;
|
|
37
|
+
pid?: number;
|
|
38
|
+
};
|
|
39
|
+
export type ClaimPortsDeps = {
|
|
40
|
+
now: () => number;
|
|
41
|
+
pidAlive: PidAlive;
|
|
42
|
+
isPortFree: (port: number) => Promise<boolean>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Lease `count` ports from `[from, from + span)` (stride `step`). Bind probes run outside the
|
|
46
|
+
* transaction (async); then, under `BEGIN IMMEDIATE`: reclaim stale leases, pick the first probed-free
|
|
47
|
+
* ports with no live lease, insert them. Retries when another process wins the race.
|
|
48
|
+
*/
|
|
49
|
+
export declare function claimPorts(store: Store, req: ClaimPortsRequest, deps: ClaimPortsDeps): AsyncResult<Lease[]>;
|
|
50
|
+
/** Which port leases to release. */
|
|
51
|
+
export type PortReleaseSelector = {
|
|
52
|
+
by: "port";
|
|
53
|
+
ports: readonly number[];
|
|
54
|
+
} | {
|
|
55
|
+
by: "lease";
|
|
56
|
+
ids: readonly string[];
|
|
57
|
+
} | {
|
|
58
|
+
by: "owner";
|
|
59
|
+
owner: Owner;
|
|
60
|
+
};
|
|
61
|
+
export type PortReleaseResult = {
|
|
62
|
+
released: Lease[];
|
|
63
|
+
/** matched but held by someone other than `onlyOwner` — left in place */
|
|
64
|
+
denied: Lease[];
|
|
65
|
+
/** ports / lease ids with no matching port lease */
|
|
66
|
+
missing: string[];
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Release port leases by port number, lease id, or owner. With `onlyOwner`, leases held by anyone
|
|
70
|
+
* else are reported in `denied` and kept. Non-port lease ids count as `missing` (never deleted here).
|
|
71
|
+
*/
|
|
72
|
+
export declare function releasePorts(store: Store, selector: PortReleaseSelector, opts?: {
|
|
73
|
+
onlyOwner?: Owner;
|
|
74
|
+
}): PortReleaseResult;
|
|
75
|
+
/** All port leases (live or stale), ordered by port. */
|
|
76
|
+
export declare function listPortLeases(store: Store): Lease[];
|
|
@@ -31,6 +31,13 @@ export interface DeviceProvider {
|
|
|
31
31
|
bootReady?(id: string, timeoutMs: number): Promise<BootReady>;
|
|
32
32
|
/** Probe for a device `bootReady` just booted and saw ready. Absent = `probe`. */
|
|
33
33
|
probeBooted?(id: string): AsyncResult<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Best effort, called for a device this claim is about to boot from shutdown (after a create, or a
|
|
36
|
+
* shutdown reuse), never for an already-booted one. A failure is logged by the provider, never fatal.
|
|
37
|
+
*/
|
|
38
|
+
prepareBoot?(id: string): Promise<void>;
|
|
39
|
+
/** Called once the device is ready, before the health probe. Best effort. */
|
|
40
|
+
afterReady?(id: string): Promise<void>;
|
|
34
41
|
}
|
|
35
42
|
/** `bootReady`: a boot failure is an error; a device that booted but never got ready is unhealthy. */
|
|
36
43
|
export type BootReady = {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Result } from "../../../types/src/result";
|
|
2
|
+
/**
|
|
3
|
+
* What `warden sim slim` may switch off inside an iOS simulator: 15 categories of `launchd_sim`
|
|
4
|
+
* jobs, each a coherent feature area (widgets, Siri, iCloud…) so a suite can keep one with
|
|
5
|
+
* `slim: { except: ["store"] }`. Labels the e2e stack depends on live in `slim.ts` `NEVER_DISABLE`
|
|
6
|
+
* and are left out here; labels that break boot are in `FORBIDDEN_LABELS` and never touched.
|
|
7
|
+
*/
|
|
8
|
+
/** A label a category must leave enabled because something common breaks without it. */
|
|
9
|
+
export type AlwaysEnabled = {
|
|
10
|
+
label: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export type SlimCategory = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
labels: readonly string[];
|
|
17
|
+
alwaysEnabled?: readonly AlwaysEnabled[];
|
|
18
|
+
};
|
|
19
|
+
export declare const SLIM_CATEGORIES: readonly SlimCategory[];
|
|
20
|
+
/** Jobs whose absence breaks boot or the home screen (observed): never disabled, whatever a profile says. */
|
|
21
|
+
export declare const FORBIDDEN_LABELS: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* Which categories stay on (`except`, by id) and which single labels stay on (`keep`, full launchd
|
|
24
|
+
* labels). A label listed by any excepted category stays on even when another category lists it too.
|
|
25
|
+
*/
|
|
26
|
+
export type SlimProfile = {
|
|
27
|
+
except: readonly string[];
|
|
28
|
+
keep: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
export declare const DEFAULT_SLIM_PROFILE: SlimProfile;
|
|
31
|
+
/** Every label some category may switch off. */
|
|
32
|
+
export declare function slimmableSet(): Set<string>;
|
|
33
|
+
/** Slimmable labels plus the `alwaysEnabled` ones: the labels warden is allowed to write an override for. */
|
|
34
|
+
export declare function managedSet(): Set<string>;
|
|
35
|
+
/** The labels to have disabled under `profile`, in category order. */
|
|
36
|
+
export declare function desiredLabels(profile: SlimProfile): Set<string>;
|
|
37
|
+
/**
|
|
38
|
+
* What to change to reach `desired`: disable the wanted labels not yet disabled, enable the managed
|
|
39
|
+
* labels that are disabled but not wanted. A disabled label outside `managed` is not ours, so it is
|
|
40
|
+
* never re-enabled. `current` is `print-disabled` (label → disabled) or just the disabled labels.
|
|
41
|
+
*/
|
|
42
|
+
export declare function delta(current: ReadonlyMap<string, boolean> | ReadonlySet<string>, desired: ReadonlySet<string>, managed: ReadonlySet<string>): {
|
|
43
|
+
toDisable: string[];
|
|
44
|
+
toEnable: string[];
|
|
45
|
+
};
|
|
46
|
+
/** Reject category ids that do not exist and `keep` labels warden does not manage; messages name the culprit. */
|
|
47
|
+
export declare function validateProfile(profile: SlimProfile): Result<SlimProfile>;
|
|
48
|
+
/** The categories for `warden sim profiles`. */
|
|
49
|
+
export declare function describeCategories(): {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
labelCount: number;
|
|
53
|
+
labels: readonly string[];
|
|
54
|
+
}[];
|
|
@@ -6,6 +6,11 @@ export type CdpOptions = {
|
|
|
6
6
|
/** connect timeout and the default per-request timeout */
|
|
7
7
|
timeoutMs?: number;
|
|
8
8
|
};
|
|
9
|
+
/** How a socket closed: the WebSocket close code and reason (Metro's proxy puts a `[TAG]` in the reason). */
|
|
10
|
+
export type CloseInfo = {
|
|
11
|
+
code: number;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
9
14
|
export type CdpClient = {
|
|
10
15
|
/** `Runtime.evaluate`-style JSON-RPC call; resolves with the reply's `result`. */
|
|
11
16
|
send: (method: string, params?: Record<string, unknown>, options?: {
|
|
@@ -13,8 +18,8 @@ export type CdpClient = {
|
|
|
13
18
|
}) => Promise<DevtoolsResult<unknown>>;
|
|
14
19
|
/** Subscribe to a CDP event by method name; returns the unsubscribe function. */
|
|
15
20
|
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;
|
|
21
|
+
/** Called when the socket closes on its own (not after `close()`), with the close code and reason Metro sent. */
|
|
22
|
+
onClose: (handler: (info: CloseInfo) => void) => () => void;
|
|
18
23
|
/** Every event of every method, in arrival order (a wildcard `on`). */
|
|
19
24
|
onAny: (handler: (method: string, params: unknown) => void) => () => void;
|
|
20
25
|
/**
|
|
@@ -54,6 +54,15 @@ export type CdpTapEvent = {
|
|
|
54
54
|
};
|
|
55
55
|
export type CdpTap = (event: CdpTapEvent) => void;
|
|
56
56
|
export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
|
|
57
|
+
/** One stack frame; `line`/`col` are 1-based. Raw frames carry the bundle URL, symbolicated ones the source file. */
|
|
58
|
+
export type StackFrame = {
|
|
59
|
+
file: string;
|
|
60
|
+
line: number;
|
|
61
|
+
col: number;
|
|
62
|
+
fn?: string;
|
|
63
|
+
/** library / runtime internals Metro marks collapsible */
|
|
64
|
+
collapsed?: boolean;
|
|
65
|
+
};
|
|
57
66
|
/** One captured console message (`console`), uncaught exception (`exception`) or `mark()` (`note`). */
|
|
58
67
|
export type ConsoleLine = {
|
|
59
68
|
/** epoch ms (the runtime's own timestamp when it sent one) */
|
|
@@ -67,6 +76,10 @@ export type ConsoleLine = {
|
|
|
67
76
|
line: number;
|
|
68
77
|
col: number;
|
|
69
78
|
};
|
|
79
|
+
/** the runtime's stack when it sent one; the broker rewrites it to original files (symbolication) */
|
|
80
|
+
stack?: StackFrame[];
|
|
81
|
+
/** Metro's code frame around the first app frame (symbolication) */
|
|
82
|
+
codeFrame?: string;
|
|
70
83
|
/** each console argument rendered on its own (console messages only) */
|
|
71
84
|
args?: string[];
|
|
72
85
|
};
|
|
@@ -19,6 +19,8 @@ export type OpenDevtoolsFacetOptions = {
|
|
|
19
19
|
/** called with each console line / network entry as it is recorded (the collector's files) */
|
|
20
20
|
consoleSink?: (line: ConsoleLine) => void;
|
|
21
21
|
networkSink?: (entry: NetworkEntry) => void;
|
|
22
|
+
/** keep React Native's LogBox (toasts and the error inspector) off in the app, across reloads (default false) */
|
|
23
|
+
muteLogBox?: boolean;
|
|
22
24
|
};
|
|
23
25
|
/**
|
|
24
26
|
* Attach to the running dev client and start capturing its console and `fetch` traffic. This is what
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Sleep } from "./devtools.types";
|
|
2
|
+
import type { DevtoolsSession } from "./session";
|
|
3
|
+
/** What {@link muteLogBox} needs of a devtools session (or facet). */
|
|
4
|
+
export type LogBoxSession = {
|
|
5
|
+
evaluate: DevtoolsSession["evaluate"];
|
|
6
|
+
on: (event: "reconnected", handler: () => void) => () => void;
|
|
7
|
+
};
|
|
8
|
+
export type MuteLogBoxOptions = {
|
|
9
|
+
/** injectable for tests; defaults to a timer */
|
|
10
|
+
sleep?: Sleep;
|
|
11
|
+
/** pause between tries while the bundle is still loading (default 250 ms) */
|
|
12
|
+
intervalMs?: number;
|
|
13
|
+
/** tries per JS context before LogBox is left on (default 80: 20 s at the default interval) */
|
|
14
|
+
attempts?: number;
|
|
15
|
+
log?: (line: string) => void;
|
|
16
|
+
};
|
|
17
|
+
export type LogBoxMute = {
|
|
18
|
+
/** resolves when the current JS context's tries end (LogBox off, given up, or stopped) */
|
|
19
|
+
readonly settled: Promise<void>;
|
|
20
|
+
stop(): void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Keeps React Native's LogBox off in the attached app (see `logbox.script.ts`): now, and again in every new JS context
|
|
24
|
+
* (`reconnected` after a relaunch or JS reload). A context whose bundle has not run yet is tried again every
|
|
25
|
+
* `intervalMs`; a newer context or `stop()` ends the older context's tries.
|
|
26
|
+
*/
|
|
27
|
+
export declare function muteLogBox(session: LogBoxSession, options?: MuteLogBoxOptions): LogBoxMute;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-side expression that turns React Native's LogBox off for good in this JS context: no warning/error toasts and
|
|
3
|
+
* no full-screen inspector, even for a fatal error. Every log still reaches the console (and so `console.jsonl` and
|
|
4
|
+
* Metro's terminal); only the overlay that covers the app and steals taps is gone.
|
|
5
|
+
*
|
|
6
|
+
* LogBox is not on a global: the script finds `react-native/Libraries/LogBox/Data/LogBoxData.js` in Metro's dev
|
|
7
|
+
* module registry (`__r.getModules()`, a `Map` of id → module with a `verboseName` path) and, once it is initialized,
|
|
8
|
+
* adds an ignore pattern that matches every message (`appendNewLog` drops ignored logs before they are stored, which
|
|
9
|
+
* is what keeps a fatal's inspector shut too, and the logs already shown are filtered out), disables notifications
|
|
10
|
+
* (`LogBox.ignoreAllLogs()`) and closes an open inspector. Re-running it is harmless (the pattern is de-duplicated).
|
|
11
|
+
*
|
|
12
|
+
* Synchronous: `"off"`, `"no-require"` (no dev Metro require yet: the bundle has not run, or a release build) or
|
|
13
|
+
* `"not-ready"` (LogBoxData not loaded yet). Plain ES5 (Hermes), no backticks or interpolation.
|
|
14
|
+
*/
|
|
15
|
+
export declare const LOGBOX_SCRIPT = "function (g) {\n var req = g.__r;\n if (typeof req !== \"function\" || typeof req.getModules !== \"function\") return \"no-require\";\n var modules = req.getModules();\n if (!modules || typeof modules.forEach !== \"function\") return \"no-require\";\n var data = null;\n modules.forEach(function (mod) {\n if (data || !mod || typeof mod.verboseName !== \"string\" || !mod.isInitialized) return;\n var name = mod.verboseName.replace(/\\\\/g, \"/\");\n if (!/(^|\\/)react-native\\/Libraries\\/LogBox\\/Data\\/LogBoxData\\.js$/.test(name)) return;\n var exports = mod.publicModule && mod.publicModule.exports;\n if (exports && typeof exports.addIgnorePatterns === \"function\") data = exports;\n });\n if (!data) return \"not-ready\";\n data.addIgnorePatterns([/[\\s\\S]*/]);\n if (typeof data.setDisabled === \"function\") data.setDisabled(true);\n if (typeof data.setSelectedLog === \"function\") data.setSelectedLog(-1);\n return \"off\";\n}";
|
|
16
|
+
/** Expression that runs `LOGBOX_SCRIPT` against the page's global object. */
|
|
17
|
+
export declare const LOGBOX_EXPRESSION = "(function (g) {\n var req = g.__r;\n if (typeof req !== \"function\" || typeof req.getModules !== \"function\") return \"no-require\";\n var modules = req.getModules();\n if (!modules || typeof modules.forEach !== \"function\") return \"no-require\";\n var data = null;\n modules.forEach(function (mod) {\n if (data || !mod || typeof mod.verboseName !== \"string\" || !mod.isInitialized) return;\n var name = mod.verboseName.replace(/\\\\/g, \"/\");\n if (!/(^|\\/)react-native\\/Libraries\\/LogBox\\/Data\\/LogBoxData\\.js$/.test(name)) return;\n var exports = mod.publicModule && mod.publicModule.exports;\n if (exports && typeof exports.addIgnorePatterns === \"function\") data = exports;\n });\n if (!data) return \"not-ready\";\n data.addIgnorePatterns([/[\\s\\S]*/]);\n if (typeof data.setDisabled === \"function\") data.setDisabled(true);\n if (typeof data.setSelectedLog === \"function\") data.setSelectedLog(-1);\n return \"off\";\n})(typeof globalThis !== \"undefined\" ? globalThis : (typeof global !== \"undefined\" ? global : this))";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Result } from "../../../types/src/result";
|
|
2
|
+
import { type CloseInfo } from "./cdp";
|
|
2
3
|
import { type CdpTap, type DevtoolsError, type DevtoolsResult, type EvaluateError, type Fetch, type Sleep } from "./devtools.types";
|
|
3
4
|
import { type SelectedTarget } from "./inspector";
|
|
4
5
|
/** The one binding warden installs; scripts call `__warden_cb(JSON.stringify({ requestId, … }))`. */
|
|
@@ -35,6 +36,11 @@ export type ConnectDevtoolsOptions = {
|
|
|
35
36
|
taps?: readonly CdpTap[];
|
|
36
37
|
/** how long `reloadJs` waits for the app to start tearing its JS context down (default 5 s) */
|
|
37
38
|
reloadWaitMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Asked when the socket closes on its own. `true` = another debugger took the page: do not reconnect (that would
|
|
41
|
+
* kick it in turn), close the session and emit `yielded`. Default: always reconnect.
|
|
42
|
+
*/
|
|
43
|
+
yieldOnClose?: (info: CloseInfo) => boolean;
|
|
38
44
|
};
|
|
39
45
|
export type DevtoolsSession = {
|
|
40
46
|
/** `Runtime.evaluate` by value. A thrown JS error fails with `exceptionDetails`. Waits out a reconnect. */
|
|
@@ -64,7 +70,7 @@ export type DevtoolsSession = {
|
|
|
64
70
|
/** Tap on every CDP event, handshake events flagged `replay`; survives reconnects. */
|
|
65
71
|
onEvent: (tap: CdpTap) => () => void;
|
|
66
72
|
evaluateWithBinding: (script: string, requestId: string, timeoutMs?: number) => Promise<DevtoolsResult<unknown>>;
|
|
67
|
-
/** `reconnected` (new target attached)
|
|
73
|
+
/** `reconnected` (new target attached), `lost` (gave up reconnecting), `yielded` (`yieldOnClose` said stop) */
|
|
68
74
|
on: <E extends keyof SessionEvents>(event: E, handler: (arg: SessionEvents[E]) => void) => () => void;
|
|
69
75
|
close: () => void;
|
|
70
76
|
};
|
|
@@ -72,6 +78,7 @@ export type ReloadMethod = "expo" | "devsettings" | "http";
|
|
|
72
78
|
export type SessionEvents = {
|
|
73
79
|
reconnected: SelectedTarget;
|
|
74
80
|
lost: DevtoolsError;
|
|
81
|
+
yielded: CloseInfo;
|
|
75
82
|
};
|
|
76
83
|
/**
|
|
77
84
|
* Attach to the app's Hermes runtime through Metro's inspector proxy. Connect sequence (same as Chrome's Fusebox
|
|
@@ -2,20 +2,21 @@ import type { Exec } from "../../../../core/src/exec";
|
|
|
2
2
|
import type { E2eEnv } from "../../env";
|
|
3
3
|
import type { ProbesFrontmost, ReadsAlertOn } from "../alerts";
|
|
4
4
|
import type { Driver, DriverResult, KeyName, Logger } from "../driver.types";
|
|
5
|
-
import type { SelectsAll } from "../typing";
|
|
5
|
+
import type { PastesText, SelectsAll } from "../typing";
|
|
6
6
|
import { type SwappableClient } from "./ios-session";
|
|
7
7
|
import { type Simctl } from "./simctl";
|
|
8
8
|
import { type SimdClient } from "./simd-client";
|
|
9
9
|
import type { SimdSpawn } from "./simd-transport";
|
|
10
10
|
/**
|
|
11
|
-
* The iOS `Driver`: input and
|
|
12
|
-
* helper spawned on stdio)
|
|
13
|
-
* `typeText` pastes what the HID keyboard map
|
|
14
|
-
* helper has no native quiescence, `screenshot` uses
|
|
11
|
+
* The iOS `Driver`: input, AX and app launch / terminate / openUrl through `warden-simd` (socket when
|
|
12
|
+
* `WARDEN_SIMD_SOCKET` is set, else a helper spawned on stdio); install / uninstall / isRunning / clearData,
|
|
13
|
+
* clipboard and appearance through `xcrun simctl`. Fallbacks: `typeText` pastes what the HID keyboard map
|
|
14
|
+
* cannot type, `settle` polls snapshot hashes when the helper has no native quiescence, `screenshot` uses
|
|
15
|
+
* `simctl io` when the helper cannot, and a helper built before the app ops leaves app lifecycle to `simctl`.
|
|
15
16
|
*/
|
|
16
17
|
export type IosDriverEnv = Pick<E2eEnv, "simd" | "simdSocket" | "helperDir">;
|
|
17
18
|
/** The iOS driver is a `Driver` plus the optional capabilities of `driver/alerts.ts`. */
|
|
18
|
-
export type IosDriver = Driver & SelectsAll & ReadsAlertOn & ProbesFrontmost;
|
|
19
|
+
export type IosDriver = Driver & SelectsAll & PastesText & ReadsAlertOn & ProbesFrontmost;
|
|
19
20
|
export type OpenIosDriverOptions = {
|
|
20
21
|
udid: string;
|
|
21
22
|
env?: IosDriverEnv;
|
|
@@ -275,6 +275,21 @@ export declare const frontmostDataSchema: z.ZodObject<{
|
|
|
275
275
|
frontmostPid: z.ZodOptional<z.ZodNumber>;
|
|
276
276
|
}, z.core.$loose>;
|
|
277
277
|
export type FrontmostData = z.infer<typeof frontmostDataSchema>;
|
|
278
|
+
/**
|
|
279
|
+
* `launch{bundleId, args?, env?, terminateRunning?}`: the helper starts the app in the process that already holds
|
|
280
|
+
* CoreSimulator (no `simctl` process per launch). `args` follow the executable, `env` reaches the app as given, and
|
|
281
|
+
* `terminateRunning` (default true) ends a running instance first; false brings a running app to the front instead.
|
|
282
|
+
* `pid` is the process that is running when the op answers.
|
|
283
|
+
*/
|
|
284
|
+
export declare const launchDataSchema: z.ZodObject<{
|
|
285
|
+
pid: z.ZodNumber;
|
|
286
|
+
}, z.core.$loose>;
|
|
287
|
+
export type LaunchData = z.infer<typeof launchDataSchema>;
|
|
288
|
+
/** `terminate{bundleId}`: `terminated: false` says nothing was running, which is the wanted state, not an error. */
|
|
289
|
+
export declare const terminateDataSchema: z.ZodObject<{
|
|
290
|
+
terminated: z.ZodBoolean;
|
|
291
|
+
}, z.core.$loose>;
|
|
292
|
+
export type TerminateData = z.infer<typeof terminateDataSchema>;
|
|
278
293
|
/**
|
|
279
294
|
* `orientation`: a real read may also say `faceUp` / `faceDown` / `unknown` (flat device); `source` tells
|
|
280
295
|
* whether the value was read from the device, is the last one this helper wrote, or is an assumed portrait.
|
|
@@ -401,6 +416,12 @@ export declare const opSchemas: {
|
|
|
401
416
|
assumed: "assumed";
|
|
402
417
|
}>>;
|
|
403
418
|
}, z.core.$strip>;
|
|
419
|
+
readonly launch: z.ZodObject<{
|
|
420
|
+
pid: z.ZodNumber;
|
|
421
|
+
}, z.core.$loose>;
|
|
422
|
+
readonly terminate: z.ZodObject<{
|
|
423
|
+
terminated: z.ZodBoolean;
|
|
424
|
+
}, z.core.$loose>;
|
|
404
425
|
};
|
|
405
426
|
export type OpDataMap = {
|
|
406
427
|
health: HealthData;
|
|
@@ -412,6 +433,8 @@ export type OpDataMap = {
|
|
|
412
433
|
alertButtons: AlertButtonsData;
|
|
413
434
|
frontmost: FrontmostData;
|
|
414
435
|
orientation: OrientationData;
|
|
436
|
+
launch: LaunchData;
|
|
437
|
+
terminate: TerminateData;
|
|
415
438
|
};
|
|
416
439
|
export type OpData<Op extends string> = Op extends keyof OpDataMap ? OpDataMap[Op] : unknown;
|
|
417
440
|
export declare function hasOpSchema(op: string): op is keyof typeof opSchemas;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Per-request budget for the helper's `typeText`: it holds, paces and (for a repeated key) waits per character.
|
|
3
|
+
* The helper's defaults (pace 5, hold 20, repeat gap 50) cost ~38 ms per character on a quiet local simulator; the
|
|
4
|
+
* budget allows ~100 ms (the EAS VirtualMac ran ~3.7x the nominal sleeps) and 200 ms per repeated key.
|
|
5
|
+
*/
|
|
2
6
|
export declare const TYPE_BASE_MS = 10000;
|
|
3
|
-
export declare const TYPE_PER_CHAR_MS =
|
|
4
|
-
export declare const TYPE_PER_REPEAT_MS =
|
|
5
|
-
/** Base + ~
|
|
7
|
+
export declare const TYPE_PER_CHAR_MS = 100;
|
|
8
|
+
export declare const TYPE_PER_REPEAT_MS = 200;
|
|
9
|
+
/** Base + ~100 ms per character + 200 ms per character equal to the one before it. */
|
|
6
10
|
export declare function typeTextBudgetMs(text: string): number;
|
|
@@ -8,7 +8,9 @@ import type { AlertInfo, Driver, DriverResult, Platform, Snapshot } from "./driv
|
|
|
8
8
|
* the one driver: whatever calls it, an input moves the *generation* on and the kept snapshot dies with the old one.
|
|
9
9
|
* A snapshot is also dropped once it is older than the platform's max age, input or not (the app can change by itself).
|
|
10
10
|
* Which reads may reuse one is the caller's call (`observe({ reuse })`): only the first look of a wait does, so a
|
|
11
|
-
* poll never spins on a tree that stopped being true.
|
|
11
|
+
* poll never spins on a tree that stopped being true. A `settle` is a wait, not an input: the kept snapshot predates
|
|
12
|
+
* it and is dropped, but nothing was pressed, so `previous().sinceInputMs` still counts from the last real input; one
|
|
13
|
+
* that reports `quiet` is the app's word that the input has landed, and then no input is recent at all.
|
|
12
14
|
*/
|
|
13
15
|
/** How old a kept snapshot may be and still answer a read. iOS reads cost ~100-300 ms; an Android dump takes ~1.9 s. */
|
|
14
16
|
export declare const OBSERVATION_MAX_AGE_MS: Record<Platform, number>;
|
|
@@ -42,6 +42,16 @@ export type SetsText = {
|
|
|
42
42
|
setText(text: string): DriverResult<void>;
|
|
43
43
|
};
|
|
44
44
|
export declare function canSetText(driver: Driver): driver is Driver & SetsText;
|
|
45
|
+
/**
|
|
46
|
+
* Optional fast path next to the frozen `Driver`: put `text` on the device pasteboard and paste it into the focused
|
|
47
|
+
* field with one Cmd+V (iOS `simctl.pbcopy` then `warden-simd` `paste`), replacing the selection. Cost is flat (~400 ms)
|
|
48
|
+
* where typing is 70-260 ms per character. A pasteboard write that cannot be confirmed is an error (nothing was
|
|
49
|
+
* pasted) and the caller types the old way.
|
|
50
|
+
*/
|
|
51
|
+
export type PastesText = {
|
|
52
|
+
pasteText(text: string): DriverResult<void>;
|
|
53
|
+
};
|
|
54
|
+
export declare function canPasteText(driver: Driver): driver is Driver & PastesText;
|
|
45
55
|
/** Types `text` at the caret. Android types the whole string in one call. */
|
|
46
56
|
export declare function typeText(driver: Driver, sleep: Sleep, text: string): DriverResult<void>;
|
|
47
57
|
/** Presses `key` `times` times, paced like typed characters. */
|
|
@@ -27,3 +27,8 @@ export declare function isCi(env: Readonly<Record<string, string | undefined>>):
|
|
|
27
27
|
* session eagerly and, in `afterAll`, closes it and (under `CI`) fails the run if a `.only` was collected.
|
|
28
28
|
*/
|
|
29
29
|
export declare function installPreload(deps: PreloadDeps): void;
|
|
30
|
+
/**
|
|
31
|
+
* `warden e2e` may run this process as a compiled warden with `BUN_BE_BUN=1`; drop it so a `warden` the spec spawns
|
|
32
|
+
* (`WARDEN_BIN`, `device.perf`) runs as warden, not bun.
|
|
33
|
+
*/
|
|
34
|
+
export declare function scrubRunnerEnv(env: Record<string, string | undefined>): void;
|
|
@@ -101,6 +101,12 @@ export type ClearMethod = "auto" | "counted" | "setText";
|
|
|
101
101
|
export declare const SETTEXT_ENV = "WARDEN_ANDROID_SETTEXT";
|
|
102
102
|
/** Whether `env` opts `auto` fills into `setText` (`SETTEXT_ENV`). */
|
|
103
103
|
export declare function setTextOptedIn(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* How `focusClearType` puts text in. `paste` writes it to the pasteboard and sends one Cmd+V on a driver that offers
|
|
106
|
+
* `pasteText` (iOS: ~400 ms flat against 70-260 ms per typed character), and types when it does not or when the paste
|
|
107
|
+
* fails; `type` always types.
|
|
108
|
+
*/
|
|
109
|
+
export type FillVia = "paste" | "type";
|
|
104
110
|
/**
|
|
105
111
|
* Taps `point` and empties what `entry` shows: select-all and one Backspace when the driver offers it, else one
|
|
106
112
|
* Backspace per character (see `focusAndSelect`). The clear routine behind the spec `clear` and `warden ui clear`.
|
|
@@ -117,5 +123,9 @@ export declare function focusAndClear(driver: Driver, sleep: Sleep, point: Point
|
|
|
117
123
|
* select-all then typing; the read-back after a `fill` checks it took, and the counted retry types. An empty field gains
|
|
118
124
|
* nothing from it, and a secure one cannot be read back, so both are typed as before. A `setText` that fails falls back
|
|
119
125
|
* to select-all and typing, without a second tap.
|
|
126
|
+
*
|
|
127
|
+
* With `via: "paste"` (the default) on a driver with `pasteText` (iOS), the text replaces the selection with one paste,
|
|
128
|
+
* secure fields included (they keep their Backspace-per-dot clearing). A paste that fails (pasteboard write not
|
|
129
|
+
* confirmed) is typed in the same attempt; nothing was pasted, so the selection is still there to be replaced.
|
|
120
130
|
*/
|
|
121
|
-
export declare function focusClearType(driver: Driver, sleep: Sleep, point: Point, entry: NodeEntry | undefined, text: string, method?: ClearMethod, env?: Readonly<Record<string, string | undefined
|
|
131
|
+
export declare function focusClearType(driver: Driver, sleep: Sleep, point: Point, entry: NodeEntry | undefined, text: string, method?: ClearMethod, env?: Readonly<Record<string, string | undefined>>, via?: FillVia): DriverResult<void>;
|