@delacour/warden-e2e 0.4.0 → 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.
@@ -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;
@@ -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[];
@@ -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
+ }[];
@@ -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))";
@@ -18,7 +18,10 @@ export declare function devtoolsTarget(env: E2eEnv): {
18
18
  appId: string;
19
19
  deviceName?: string;
20
20
  };
21
- /** Metro's inspector for this env's app (`WARDEN_METRO_URL`), pinned by {@link devtoolsTarget}. */
21
+ /**
22
+ * Metro's inspector for this env's app (`WARDEN_METRO_URL`), pinned by {@link devtoolsTarget}. LogBox is kept off:
23
+ * its toasts and inspector cover the app and take taps, while the logs still reach the console capture.
24
+ */
22
25
  export declare const openDevtools: DevtoolsOpener;
23
26
  export type Session = {
24
27
  readonly env: E2eEnv;