@delacour/warden-e2e 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/chunk-qtkmfjbq.mjs +4585 -0
  3. package/index.d.ts +1 -0
  4. package/index.mjs +998 -0
  5. package/package.json +49 -4
  6. package/preload.d.ts +1 -0
  7. package/preload.mjs +5512 -0
  8. package/types/core/src/allocate.d.ts +23 -0
  9. package/types/core/src/builds/app-option.schema.d.ts +37 -0
  10. package/types/core/src/builds/install.d.ts +95 -0
  11. package/types/core/src/builds/resolve.d.ts +74 -0
  12. package/types/core/src/config.defaults.d.ts +25 -0
  13. package/types/core/src/exec.d.ts +18 -0
  14. package/types/core/src/liveness.d.ts +12 -0
  15. package/types/core/src/providers/android.d.ts +124 -0
  16. package/types/core/src/providers/health.d.ts +45 -0
  17. package/types/core/src/providers/provider.types.d.ts +56 -0
  18. package/types/core/src/store.d.ts +91 -0
  19. package/types/core/src/types.d.ts +106 -0
  20. package/types/e2e/src/alert-buttons.d.ts +41 -0
  21. package/types/e2e/src/alert-reader.d.ts +25 -0
  22. package/types/e2e/src/app.d.ts +62 -0
  23. package/types/e2e/src/device-devtools.d.ts +26 -0
  24. package/types/e2e/src/device-link.d.ts +43 -0
  25. package/types/e2e/src/device.d.ts +82 -0
  26. package/types/e2e/src/devtools/cdp.d.ts +65 -0
  27. package/types/e2e/src/devtools/console.d.ts +59 -0
  28. package/types/e2e/src/devtools/devtools.types.d.ts +169 -0
  29. package/types/e2e/src/devtools/facet.d.ts +28 -0
  30. package/types/e2e/src/devtools/inspector.d.ts +48 -0
  31. package/types/e2e/src/devtools/jsonl.d.ts +4 -0
  32. package/types/e2e/src/devtools/network.d.ts +33 -0
  33. package/types/e2e/src/devtools/network.script.d.ts +24 -0
  34. package/types/e2e/src/devtools/session.d.ts +82 -0
  35. package/types/e2e/src/devtools/tree.d.ts +23 -0
  36. package/types/e2e/src/devtools/tree.script.d.ts +33 -0
  37. package/types/e2e/src/driver/alerts.d.ts +29 -0
  38. package/types/e2e/src/driver/android/adb-session.d.ts +52 -0
  39. package/types/e2e/src/driver/android/android-alert.d.ts +4 -0
  40. package/types/e2e/src/driver/android/android-animations.d.ts +29 -0
  41. package/types/e2e/src/driver/android/android-app.d.ts +32 -0
  42. package/types/e2e/src/driver/android/android-argv.d.ts +122 -0
  43. package/types/e2e/src/driver/android/android-device.d.ts +44 -0
  44. package/types/e2e/src/driver/android/android-driver.d.ts +60 -0
  45. package/types/e2e/src/driver/android/android-dump.d.ts +46 -0
  46. package/types/e2e/src/driver/android/android-state.d.ts +69 -0
  47. package/types/e2e/src/driver/android/uiad-backend.d.ts +63 -0
  48. package/types/e2e/src/driver/android/uiad-client.d.ts +60 -0
  49. package/types/e2e/src/driver/android/uiad-device.d.ts +70 -0
  50. package/types/e2e/src/driver/android/uiad-dex.generated.d.ts +8 -0
  51. package/types/e2e/src/driver/android/uiad-ops.d.ts +62 -0
  52. package/types/e2e/src/driver/android/uiad-protocol.d.ts +209 -0
  53. package/types/e2e/src/driver/android/uiautomator.d.ts +33 -0
  54. package/types/e2e/src/driver/driver.types.d.ts +231 -0
  55. package/types/e2e/src/driver/errors.d.ts +13 -0
  56. package/types/e2e/src/driver/gesture-geometry.d.ts +48 -0
  57. package/types/e2e/src/driver/ios/ios-driver.d.ts +63 -0
  58. package/types/e2e/src/driver/ios/ios-session.d.ts +30 -0
  59. package/types/e2e/src/driver/ios/locate-helper.d.ts +38 -0
  60. package/types/e2e/src/driver/ios/normalize.d.ts +11 -0
  61. package/types/e2e/src/driver/ios/simctl.d.ts +87 -0
  62. package/types/e2e/src/driver/ios/simd-client.d.ts +68 -0
  63. package/types/e2e/src/driver/ios/simd-protocol.d.ts +433 -0
  64. package/types/e2e/src/driver/ios/simd-transport.d.ts +43 -0
  65. package/types/e2e/src/driver/ios/type-budget.d.ts +6 -0
  66. package/types/e2e/src/driver/observation-cache.d.ts +66 -0
  67. package/types/e2e/src/driver/open.d.ts +29 -0
  68. package/types/e2e/src/driver/settle.d.ts +43 -0
  69. package/types/e2e/src/driver/span-detail.d.ts +12 -0
  70. package/types/e2e/src/driver/traced-driver.d.ts +14 -0
  71. package/types/e2e/src/driver/typing.d.ts +48 -0
  72. package/types/e2e/src/driver/unwrap.d.ts +13 -0
  73. package/types/e2e/src/env.d.ts +61 -0
  74. package/types/e2e/src/errors.d.ts +26 -0
  75. package/types/e2e/src/expect/expect.d.ts +27 -0
  76. package/types/e2e/src/expect/matchers.d.ts +72 -0
  77. package/types/e2e/src/fixture-session.d.ts +34 -0
  78. package/types/e2e/src/fixtures.d.ts +14 -0
  79. package/types/e2e/src/index.d.ts +8 -0
  80. package/types/e2e/src/preload.d.ts +29 -0
  81. package/types/e2e/src/runner/artifacts.d.ts +13 -0
  82. package/types/e2e/src/runner/attempt.d.ts +63 -0
  83. package/types/e2e/src/runner/callsite.d.ts +2 -0
  84. package/types/e2e/src/runner/failure.d.ts +28 -0
  85. package/types/e2e/src/runner/fixtures.types.d.ts +60 -0
  86. package/types/e2e/src/runner/results.d.ts +48 -0
  87. package/types/e2e/src/runner/scope.d.ts +62 -0
  88. package/types/e2e/src/runner/span.d.ts +40 -0
  89. package/types/e2e/src/runner/step.d.ts +7 -0
  90. package/types/e2e/src/runner/test.d.ts +52 -0
  91. package/types/e2e/src/runner/trace.d.ts +87 -0
  92. package/types/e2e/src/screen/actions.d.ts +93 -0
  93. package/types/e2e/src/screen/context.d.ts +134 -0
  94. package/types/e2e/src/screen/field.d.ts +121 -0
  95. package/types/e2e/src/screen/locator.d.ts +233 -0
  96. package/types/e2e/src/screen/nodes.d.ts +53 -0
  97. package/types/e2e/src/screen/poll.d.ts +57 -0
  98. package/types/e2e/src/screen/queries.d.ts +34 -0
  99. package/types/e2e/src/screen/query.d.ts +60 -0
  100. package/types/e2e/src/screen/react-locator.d.ts +30 -0
  101. package/types/e2e/src/screen/render.d.ts +42 -0
  102. package/types/e2e/src/screen/roles.d.ts +24 -0
  103. package/types/e2e/src/screen/screen.d.ts +61 -0
  104. package/types/e2e/src/screen/selector.d.ts +38 -0
  105. package/types/e2e/src/screen/text.d.ts +13 -0
  106. package/types/e2e/src/session.d.ts +105 -0
  107. package/types/e2e/src/traced-device.d.ts +9 -0
  108. package/types/e2e/src/version.d.ts +7 -0
  109. package/types/types/src/result.d.ts +12 -0
  110. package/README.md +0 -3
@@ -0,0 +1,48 @@
1
+ import type { Driver, DriverResult, KeyName } from "./driver.types";
2
+ /**
3
+ * The one place that decides how text and keys are sent to a device (specs' `fill`/`type`/`clear` and `warden ui
4
+ * fill`/`type` all go through it).
5
+ *
6
+ * iOS: `warden-simd` holds, paces and spaces repeated keys itself (`WARDEN_SIMD_PACE_MS`, `WARDEN_SIMD_HOLD_MS`,
7
+ * `WARDEN_SIMD_REPEAT_GAP_MS`), so the whole string goes in one request and the client adds no gap. Before that
8
+ * the client sent one grapheme per request, 25 ms apart (set `KEY_PACE_MS` 25 and `SPLIT_TEXT` true to go back).
9
+ */
10
+ /** Gap after each key on iOS. */
11
+ export declare const KEY_PACE_MS = 0;
12
+ /** iOS types one grapheme per request. */
13
+ export declare const SPLIT_TEXT = false;
14
+ export type Sleep = (ms: number) => Promise<void>;
15
+ /**
16
+ * Optional fast path a driver can offer next to the frozen `Driver` interface: `times` presses of `key` in one
17
+ * device round trip. Android needs it because every `input keyevent` is a process start (clearing a 20 character
18
+ * field one key at a time takes seconds); iOS paces keys itself.
19
+ */
20
+ export type RepeatingKeys = {
21
+ pressKeyRepeat(key: KeyName, times: number): DriverResult<void>;
22
+ };
23
+ export declare function canRepeatKeys(driver: Driver): driver is Driver & RepeatingKeys;
24
+ /**
25
+ * Optional fast path next to the frozen `Driver`: select everything in the focused text field (iOS Cmd+A through
26
+ * `warden-simd`, Android Ctrl+A through `input keycombination` on API 33+), so one Backspace empties a field of any
27
+ * length instead of one per character. A field that refuses the shortcut is caught by the read-back after the fill.
28
+ */
29
+ export type SelectsAll = {
30
+ selectAll(): DriverResult<void>;
31
+ };
32
+ export declare function canSelectAll(driver: Driver): driver is Driver & SelectsAll;
33
+ /**
34
+ * Optional fast path next to the frozen `Driver`: replace the whole text of the focused field in one call (Android
35
+ * `warden-uiad` `setText`, `ACTION_SET_TEXT`: the same action its `typeText` inserts with). `screen/field.ts` uses it,
36
+ * opt-in only (`ClearMethod` `setText`, or `WARDEN_ANDROID_SETTEXT=1`), for a `fill` over a field that holds text,
37
+ * instead of select-all then typing; the read-back checks it took. A refusal (no server, no focused input) is an error,
38
+ * and the caller types the old way. Opt-in because the server writes to the node its cached focus names, which may
39
+ * still be the field focused before the tap.
40
+ */
41
+ export type SetsText = {
42
+ setText(text: string): DriverResult<void>;
43
+ };
44
+ export declare function canSetText(driver: Driver): driver is Driver & SetsText;
45
+ /** Types `text` at the caret. Android types the whole string in one call. */
46
+ export declare function typeText(driver: Driver, sleep: Sleep, text: string): DriverResult<void>;
47
+ /** Presses `key` `times` times, paced like typed characters. */
48
+ export declare function pressKeys(driver: Driver, sleep: Sleep, key: KeyName, times: number): DriverResult<void>;
@@ -0,0 +1,13 @@
1
+ import type { Result } from "../../../types/src/result";
2
+ import { E2eError } from "../errors";
3
+ import type { DriverError, DriverResult } from "./driver.types";
4
+ /** `TIMEOUT` for time limits, `DRIVER` when the helper / device is broken, else `ACTION_FAILED`. */
5
+ export declare function toE2eError(error: DriverError, action: string): E2eError;
6
+ /**
7
+ * Runs one driver call, racing it against `signal`: an already-aborted signal never starts the call,
8
+ * an abort while it runs rejects with `E2eError{TIMEOUT}`. Returns the raw `Result` (use {@link unwrap}
9
+ * when a failure should throw).
10
+ */
11
+ export declare function settled<T>(call: () => DriverResult<T>, action: string, signal?: AbortSignal): Promise<Result<T, DriverError>>;
12
+ /** {@link settled}, throwing the mapped `E2eError` (see {@link toE2eError}) on a driver failure. */
13
+ export declare function unwrap<T>(call: () => DriverResult<T>, action: string, signal?: AbortSignal): Promise<T>;
@@ -0,0 +1,61 @@
1
+ import type { Result } from "../../types/src/result";
2
+ /** The env names `warden e2e` writes and the spec process reads (`apps/cli` imports these). */
3
+ export declare const ENV_NAMES: {
4
+ readonly udid: "WARDEN_UDID";
5
+ readonly deviceName: "WARDEN_DEVICE_NAME";
6
+ readonly platform: "WARDEN_PLATFORM";
7
+ readonly bundleId: "WARDEN_BUNDLE_ID";
8
+ readonly appPath: "WARDEN_APP_PATH";
9
+ readonly metroUrl: "WARDEN_METRO_URL";
10
+ readonly launchArgs: "WARDEN_LAUNCH_ARGS";
11
+ readonly out: "WARDEN_E2E_OUT";
12
+ readonly timeout: "WARDEN_E2E_TIMEOUT";
13
+ readonly actionTimeout: "WARDEN_E2E_ACTION_TIMEOUT";
14
+ readonly assertionTimeout: "WARDEN_E2E_ASSERTION_TIMEOUT";
15
+ readonly flowsDir: "WARDEN_E2E_FLOWS_DIR";
16
+ readonly helperDir: "WARDEN_HELPER_DIR";
17
+ readonly simd: "WARDEN_SIMD";
18
+ readonly simdSocket: "WARDEN_SIMD_SOCKET";
19
+ readonly headless: "WARDEN_HEADLESS";
20
+ readonly logTimestamps: "WARDEN_LOG_TIMESTAMPS";
21
+ readonly worker: "WARDEN_WORKER";
22
+ /** the `warden` that started this process: the preload refuses to run when it is not this package's version */
23
+ readonly version: "WARDEN_VERSION";
24
+ };
25
+ export declare const DEFAULT_TIMEOUT_MS = 120000;
26
+ export declare const DEFAULT_ACTION_TIMEOUT_MS = 30000;
27
+ export declare const DEFAULT_ASSERTION_TIMEOUT_MS = 5000;
28
+ export type E2eEnv = {
29
+ udid: string;
30
+ /** the leased device's name: pins devtools to its own Metro inspector page when several devices share one Metro */
31
+ deviceName?: string;
32
+ platform: "ios" | "android";
33
+ bundleId: string;
34
+ appPath?: string;
35
+ metroUrl?: string;
36
+ launchArgs: string[];
37
+ /** where `results.jsonl` and per-attempt artifacts go (may be relative to cwd) */
38
+ out: string;
39
+ timeoutMs: number;
40
+ actionTimeoutMs: number;
41
+ assertionTimeoutMs: number;
42
+ flowsDir?: string;
43
+ helperDir?: string;
44
+ simd?: string;
45
+ simdSocket?: string;
46
+ /** nothing in this package reads it: kept for specs and helpers that honour a headless run (`guides/headless`) */
47
+ headless: boolean;
48
+ /** warden already stamps the log lines: the spec reporter prints no clock of its own */
49
+ logTimestamps: boolean;
50
+ /** nothing in this package reads it: the 0-based worker index, for specs that shard or name per-worker data (`guides/e2e`) */
51
+ worker?: string;
52
+ /** `WARDEN_VERSION`: the warden that started this run */
53
+ wardenVersion?: string;
54
+ };
55
+ /**
56
+ * Reads the spec env contract. `undefined` data = not running under warden (no `WARDEN_UDID`); an
57
+ * error lists every invalid variable by its env name. Empty values count as unset.
58
+ */
59
+ export declare function readEnv(env: Readonly<Record<string, string | undefined>>, opts?: {
60
+ now?: () => Date;
61
+ }): Result<E2eEnv | undefined>;
@@ -0,0 +1,26 @@
1
+ import type { ErrorResult } from "../../types/src/result";
2
+ export type E2eErrorCode = "LOCATOR_NOT_FOUND" | "LOCATOR_AMBIGUOUS" | "INVALID_LOCATOR" | "ACTION_FAILED" | "TIMEOUT" | "NOT_ACTIONABLE" | "INVALID_ARGUMENT" | "SESSION" | "DRIVER" | "ASSERTION";
3
+ /** Plain shape of a spec error: what `e2eErr()` accepts and what a `Result` carries. */
4
+ export type E2eErrorShape = {
5
+ code: E2eErrorCode;
6
+ message: string;
7
+ /** text render of the accessibility tree at the moment of failure, when the caller had one */
8
+ screen?: string;
9
+ /** number of candidates for `LOCATOR_AMBIGUOUS` */
10
+ matches?: number;
11
+ };
12
+ /**
13
+ * Every failure a spec can see (`screen`, `expect`, `test`, session), discriminated by `code`.
14
+ * A thrown class so specs can `instanceof` it; `e2eErr()` wraps one in a failed `Result`.
15
+ */
16
+ export declare class E2eError extends Error implements E2eErrorShape {
17
+ readonly code: E2eErrorCode;
18
+ readonly screen?: string;
19
+ readonly matches?: number;
20
+ constructor(code: E2eErrorCode, message: string, screen?: string, matches?: number);
21
+ static from(shape: E2eErrorShape): E2eError;
22
+ }
23
+ /** `err()` for results carrying an `E2eError`. */
24
+ export declare function e2eErr(error: E2eErrorShape): ErrorResult<E2eError>;
25
+ /** One line for logs and thrown messages: `CODE: message`. */
26
+ export declare function describeE2eError(error: E2eErrorShape): string;
@@ -0,0 +1,27 @@
1
+ import { type Expect as BunExpect, type Matchers } from "bun:test";
2
+ import { E2eError } from "../errors";
3
+ import { type Locator } from "../screen/locator";
4
+ import { type Session } from "../session";
5
+ import { LocatorAssertions } from "./matchers";
6
+ /** Receives a failure `expect.soft` kept instead of throwing. */
7
+ export type SoftSink = (error: E2eError) => void;
8
+ /** `expect.soft`: `expect` whose failures are kept on the running attempt, which fails once its body has settled. */
9
+ export type SoftExpect = {
10
+ (actual: Locator): LocatorAssertions;
11
+ <T = unknown>(actual: T, message?: string): Matchers<T>;
12
+ };
13
+ /**
14
+ * The spec `expect`: `expect(locator)` gives the polling locator matchers, anything else is bun's own `expect`
15
+ * (all of its statics — `any`, `arrayContaining`, `extend`, `not`, … — are on it too).
16
+ */
17
+ export type E2eExpect = ((actual: Locator) => LocatorAssertions) & BunExpect & {
18
+ readonly soft: SoftExpect;
19
+ };
20
+ export type ExpectDeps = {
21
+ /** where `expect.soft` keeps failures right now; undefined (outside an attempt) means throw as usual */
22
+ softSink: () => SoftSink | undefined;
23
+ };
24
+ /** The running attempt's collector (`AttemptContext.softFailures`), if a test is running. */
25
+ export declare function softSinkFor(session: Pick<Session, "attempt">): SoftSink | undefined;
26
+ export declare function createExpect(deps: ExpectDeps): E2eExpect;
27
+ export declare const expect: E2eExpect;
@@ -0,0 +1,72 @@
1
+ import { E2eError } from "../errors";
2
+ import { type Locator } from "../screen/locator";
3
+ import { type TextMatch } from "../screen/text";
4
+ /**
5
+ * A negated matcher passes only after its condition held this long without a break (or the whole timeout, if shorter).
6
+ *
7
+ * It can end sooner: once the condition held on one poll, a driver with native settle (iOS `axbridge`, Android
8
+ * `warden-uiad`: both watch the app's own activity stream, ~250-300 ms) is asked once whether the screen is quiet
9
+ * (`NEGATION_QUIET_MS`, bounded by what is left of the hold). If it is, one fresh poll that still holds passes. A screen
10
+ * that is quiet but about to change by itself (a toast after a timer) can therefore pass before that change, where the
11
+ * full hold would have seen it. Without native settle the hold stays plain: there, knowing the screen is quiet means
12
+ * polled snapshots or a `uiautomator dump` (~1.9 s), longer than the window it would save.
13
+ *
14
+ * The settle is asked only when no input came within the last `NEGATION_GRACE_MS` (the observation cache's
15
+ * `sinceInputMs`; no input yet counts as none). A quiet app is not a settled one right after a tap: a submit waiting on
16
+ * the network is idle until the reply comes, so `submit.tap()` then `expect(error).not.toBeVisible()` keeps the full
17
+ * hold, which sees an error that comes within it. Without the cache the time of the last input is unknown: plain hold.
18
+ * Input is every driver call that is not a read, the matcher's own native settle included (so a second `.not` right
19
+ * after one that ended early holds the full time). Devtools calls (`device.devtools.evaluate`, `reloadJs`) go to Metro,
20
+ * not the driver, so they do not count: a `.not` right after one may end early. Tap, `wait` or assert something
21
+ * positive first.
22
+ */
23
+ export declare const NEGATION_GRACE_MS = 1000;
24
+ /** The quiet window a negated matcher asks the driver for before it ends its hold early. */
25
+ export declare const NEGATION_QUIET_MS = 250;
26
+ export type AssertionOptions = {
27
+ timeout?: number;
28
+ };
29
+ export type TextAssertionOptions = AssertionOptions & {
30
+ exact?: boolean;
31
+ };
32
+ export type ContainAssertionOptions = AssertionOptions & {
33
+ ignoreCase?: boolean;
34
+ };
35
+ export type CheckedOptions = AssertionOptions & {
36
+ checked?: boolean;
37
+ };
38
+ export type AssertionContext = {
39
+ negate: boolean;
40
+ /** collects a failure instead of throwing it (`expect.soft`) */
41
+ soft?: (error: E2eError) => void;
42
+ };
43
+ /**
44
+ * The locator matchers of `expect(locator)`. Each one polls (100 ms apart for the first second, then 250 ms) until its
45
+ * condition holds or the assertion timeout passes, then fails with `ASSERTION`. `.not` inverts the condition (and must
46
+ * see it hold for a grace window; the polls land exactly on its end).
47
+ * Except `toHaveCount`, a matcher needs exactly one match: none keeps polling, several fail at once.
48
+ */
49
+ export declare class LocatorAssertions {
50
+ private readonly locator;
51
+ private readonly context;
52
+ constructor(locator: Locator, context: AssertionContext);
53
+ get not(): LocatorAssertions;
54
+ toBeVisible(options?: AssertionOptions): Promise<void>;
55
+ toBeHidden(options?: AssertionOptions): Promise<void>;
56
+ /** Whole-text after whitespace normalisation (`exact: false`: a case-insensitive substring); a RegExp as written. */
57
+ toHaveText(expected: TextMatch, options?: TextAssertionOptions): Promise<void>;
58
+ /** The (normalised) text contains the string, case-sensitively unless `ignoreCase`; a RegExp is tested as written. */
59
+ toContainText(expected: TextMatch, options?: ContainAssertionOptions): Promise<void>;
60
+ /** The field value as it is (no whitespace normalisation). Secure fields cannot be judged. */
61
+ toHaveValue(expected: TextMatch, options?: AssertionOptions): Promise<void>;
62
+ /** Checked by default; `{ checked: false }` expects unchecked. A node that is not checkable counts as unchecked. */
63
+ toBeChecked(options?: CheckedOptions): Promise<void>;
64
+ toBeEnabled(options?: AssertionOptions): Promise<void>;
65
+ toBeDisabled(options?: AssertionOptions): Promise<void>;
66
+ /** Exactly `expected` current matches (zero included). */
67
+ toHaveCount(expected: number, options?: AssertionOptions): Promise<void>;
68
+ private assert;
69
+ private poll;
70
+ /** The screen is rendered here, from the last snapshot read, because only a failure needs the text. */
71
+ private failure;
72
+ }
@@ -0,0 +1,34 @@
1
+ import type { DevtoolsFacet, DevtoolsResult } from "./devtools/devtools.types";
2
+ import type { SpanRunner } from "./runner/span";
3
+ import type { TraceEvent } from "./runner/trace";
4
+ /**
5
+ * What the `app` / `device` fixtures need from the session. `Session` satisfies it structurally;
6
+ * the runner (T3) mutates `signal`, `attemptDir` and `trace` per attempt, so fixtures read them at
7
+ * call time, never at construction.
8
+ */
9
+ export type FixtureSession = {
10
+ /** absolute `WARDEN_E2E_OUT` (screenshot fallback when no attempt is running) */
11
+ readonly out: string;
12
+ /** the running attempt's deadline / cancel signal; every driver call honours it */
13
+ readonly signal?: AbortSignal;
14
+ /** `<out>/<flowId>/<title-slug>-<hash6>` of the running attempt */
15
+ readonly attemptDir?: string;
16
+ /** the running attempt's `trace.jsonl` */
17
+ readonly trace?: {
18
+ write(event: TraceEvent): void;
19
+ };
20
+ /** one reported step (`▸`/`▪` lines, `trace.jsonl`) that gives up when the attempt deadline aborts; absent = just run it */
21
+ step?<T>(kind: string, label: string, fn: () => Promise<T>): Promise<T>;
22
+ /** times one call as a trace-only `span` (`device.*`, `read.*`); absent = untraced */
23
+ span?: SpanRunner;
24
+ /** attaches the app's devtools on first call (the session shares one connection); absent = no devtools */
25
+ devtools?(): Promise<DevtoolsResult<DevtoolsFacet>>;
26
+ /** the facet if already attached; never attaches */
27
+ peekDevtools?(): DevtoolsFacet | undefined;
28
+ };
29
+ /** Injected time: tests drive a fake clock. */
30
+ export type Clock = {
31
+ now(): number;
32
+ sleep(ms: number): Promise<void>;
33
+ };
34
+ export declare const systemClock: Clock;
@@ -0,0 +1,14 @@
1
+ import { type Device } from "./device";
2
+ import type { BaseFixtures, FixtureContext } from "./runner/fixtures.types";
3
+ import type { ScreenHost } from "./screen/context";
4
+ /**
5
+ * What `screen` needs from one attempt: the live driver, its timeouts and deadline, and reported steps. With
6
+ * `systemAlerts` set (`test.use`) and a `device` to press with, reads and waits answer the alerts they see.
7
+ */
8
+ export declare function screenHostFor(ctx: FixtureContext, device?: Pick<Device, "alert">): ScreenHost;
9
+ /**
10
+ * The `app` / `device` / `screen` / `platform` every test gets, over the session's driver. `device` calls and
11
+ * locator reads are trace-only spans (`device.*`, `read.*`); the driver's own calls are `driver.*` spans (the session
12
+ * traces its driver).
13
+ */
14
+ export declare function defaultFixtureFactory(ctx: FixtureContext): Promise<BaseFixtures>;
@@ -0,0 +1,8 @@
1
+ export type { OpenOptions, OpenWaitUntil } from "./app";
2
+ export { E2eError } from "./errors";
3
+ export { expect } from "./expect/expect";
4
+ export type { App, Device, Screen, SystemAlerts, TestFixtures } from "./runner/fixtures.types";
5
+ export { afterAll, afterEach, beforeAll, beforeEach, describe, test } from "./runner/test";
6
+ export type { Locator, LocatorReading, PinchOptions, RotateOptions, TapOptions, TapUntil } from "./screen/locator";
7
+ export type { ScreenGestures, ScreenPathOptions, ScreenPinchOptions, ScreenRotateOptions, } from "./screen/screen";
8
+ export { WARDEN_E2E_VERSION } from "./version";
@@ -0,0 +1,29 @@
1
+ import { BUN_TIMEOUT_MARGIN_MS } from "./runner/scope";
2
+ import { getSession } from "./session";
3
+ export { BUN_TIMEOUT_MARGIN_MS };
4
+ export type PreloadDeps = {
5
+ env: Readonly<Record<string, string | undefined>>;
6
+ setDefaultTimeout: (ms: number) => void;
7
+ afterAll: (fn: () => Promise<void>) => void;
8
+ createSession: typeof getSession;
9
+ /** this package's version (`WARDEN_E2E_VERSION`) */
10
+ version: string;
11
+ /** whether a `test.only` was collected */
12
+ onlySeen: () => boolean;
13
+ stderr: (text: string) => void;
14
+ exit: (code: number) => void;
15
+ };
16
+ /**
17
+ * The protocol handshake: `warden e2e` and this package version in lock-step. A mismatch is a warning, not an
18
+ * error (most pairs still work), but the install fix is spelled out. Nothing to compare without `WARDEN_VERSION`
19
+ * (a by-hand run) or from the workspace sources (not a published build).
20
+ */
21
+ export declare function versionMismatch(wardenVersion: string | undefined, e2eVersion: string): string | undefined;
22
+ /** `CI=1`, `CI=true`, … — anything but empty, `0` and `false`. */
23
+ export declare function isCi(env: Readonly<Record<string, string | undefined>>): boolean;
24
+ /**
25
+ * `bun test --preload`: a no-op without `WARDEN_UDID` (safe in `bunfig.toml`); otherwise validates
26
+ * the env (exit 2 when invalid), raises bun's default timeout above the test timeout, opens the
27
+ * session eagerly and, in `afterAll`, closes it and (under `CI`) fails the run if a `.only` was collected.
28
+ */
29
+ export declare function installPreload(deps: PreloadDeps): void;
@@ -0,0 +1,13 @@
1
+ /** Lowercase, runs of non-alphanumerics become `-`, trimmed, capped at 60 chars; `fallback` when empty. */
2
+ export declare function slug(text: string, fallback?: string): string;
3
+ /** First 6 hex chars of a sha1 over the parts: keeps equal slugs from colliding. */
4
+ export declare function hash6(...parts: string[]): string;
5
+ /** `<out>/<flowId>/<title-slug>-<hash6>/` for one test attempt. */
6
+ export declare function attemptDir(out: string, flowId: string, title: readonly string[], hash: string): string;
7
+ /** `<dir>/screenshots/NNN-name.png` (3-digit, 1-based order). */
8
+ export declare function screenshotPath(dir: string, n: number, name: string): string;
9
+ /**
10
+ * Stable per-file id: the path under `flowsDir` without extension (`auth/sign-in.e2e.ts` →
11
+ * `auth/sign-in`), segments slugged. A file outside `flowsDir` (or no `flowsDir`) uses its basename.
12
+ */
13
+ export declare function flowIdOf(file: string, flowsDir?: string): string;
@@ -0,0 +1,63 @@
1
+ import { E2eError } from "../errors";
2
+ import { type Session } from "../session";
3
+ import type { BaseFixtures } from "./fixtures.types";
4
+ import { type ResolvedUse, type Scope } from "./scope";
5
+ /** Longest one hook or fixture teardown may take once the test itself is over. */
6
+ export declare const CLEANUP_TIMEOUT_MS = 10000;
7
+ /** Part of bun's margin the runner never spends after a deadline: the `■` line and the results line still need it. */
8
+ export declare const POST_DEADLINE_RESERVE_MS = 5000;
9
+ /** The runner's own time limits around a test body (see {@link setRunnerTiming}). */
10
+ export type RunnerTiming = {
11
+ /** longest one `afterEach` hook or fixture teardown may take ({@link CLEANUP_TIMEOUT_MS}) */
12
+ cleanupTimeoutMs: number;
13
+ /** longest one failure capture call may take (`CAPTURE_TIMEOUT_MS`) */
14
+ captureTimeoutMs: number;
15
+ /** what bun's own per-test timeout adds over warden's deadline (`BUN_TIMEOUT_MARGIN_MS`) */
16
+ bunMarginMs: number;
17
+ /** part of that margin left unspent ({@link POST_DEADLINE_RESERVE_MS}) */
18
+ reserveMs: number;
19
+ };
20
+ export declare function runnerTiming(): Readonly<RunnerTiming>;
21
+ /**
22
+ * Test seam: a runner fixture spec shrinks the limits (before it registers its tests) so a slow-cleanup case runs
23
+ * in milliseconds. `undefined` restores the defaults.
24
+ */
25
+ export declare function setRunnerTiming(next: Partial<RunnerTiming> | undefined): void;
26
+ /** `test.extend` fixture: set up, `use(value)`, and everything after `use` is the teardown. */
27
+ export type FixtureSetup = (base: Record<string, unknown>, use: (value: unknown) => Promise<void>) => Promise<void>;
28
+ export type FixtureEntry = {
29
+ name: string;
30
+ setup: FixtureSetup;
31
+ };
32
+ export type AttemptSpec<F extends object> = {
33
+ /** absolute path of the spec file */
34
+ file: string;
35
+ /** describe titles, then the test title */
36
+ title: readonly string[];
37
+ scope: Scope;
38
+ fixtures: readonly FixtureEntry[];
39
+ body: (fixtures: F) => void | Promise<void>;
40
+ /** per-test `timeout` option (beats `test.use`) */
41
+ timeout?: number;
42
+ only: boolean;
43
+ /** no retries left: only a final (or passing) attempt appends to `results.jsonl` */
44
+ final: boolean;
45
+ /** bun's own timeout for this test, fixed when it was registered (default: the resolved timeout + bun's margin) */
46
+ bunTimeoutMs?: number;
47
+ };
48
+ /** `app`/`device`/`screen`/`platform` from the session's factory (throwing stand-ins when none is registered). */
49
+ export declare function buildBaseFixtures(session: Session, signal: AbortSignal, use: ResolvedUse): Promise<BaseFixtures>;
50
+ /**
51
+ * When the runner must be done with an attempt: bun's own timeout for the test (or the resolved timeout plus bun's
52
+ * margin, if that is sooner) minus the reserve. Failure capture, `afterEach` hooks and fixture teardowns all spend
53
+ * from this one budget, so bun never gives up on a test (and starts the next one) while its cleanup still runs.
54
+ */
55
+ export declare function cleanupDeadline(startedAt: number, timeoutMs: number, bunTimeoutMs: number | undefined): number;
56
+ /**
57
+ * One attempt of one test: deadline (`AbortSignal`), fixtures, `beforeEach`, body, then — whatever
58
+ * happened — failure capture, `afterEach`, fixture teardown (reverse order), the `■` line and the
59
+ * `results.jsonl` line. Throws the test's own error so bun marks the test failed.
60
+ */
61
+ export declare function runAttempt<F extends object>(session: Session, spec: AttemptSpec<F>): Promise<void>;
62
+ /** One error for every `expect.soft` failure of the attempt. */
63
+ export declare function softFailureError(failures: readonly E2eError[]): E2eError;
@@ -0,0 +1,2 @@
1
+ /** The absolute path of the first stack frame outside the e2e runner: the spec file calling `test()`. */
2
+ export declare function callerFile(stack?: string | undefined): string;
@@ -0,0 +1,28 @@
1
+ import type { DevtoolsFacet } from "../devtools/devtools.types";
2
+ import type { Driver } from "../driver/driver.types";
3
+ export declare const FAILURE_PNG = "failure.png";
4
+ export declare const SCREEN_AT_FAILURE = "screen-at-failure.txt";
5
+ export declare const CONSOLE_JSONL = "console.jsonl";
6
+ export declare const NETWORK_JSONL = "network.jsonl";
7
+ /** Longest a single capture call may take: a wedged device must not hold the test run. */
8
+ export declare const CAPTURE_TIMEOUT_MS = 10000;
9
+ /** What is left of the runner's post-deadline budget, in ms; the default never runs out. */
10
+ export type CaptureBudget = () => number;
11
+ export type FailureCapture = {
12
+ /** files written into `dir`, relative to it */
13
+ files: string[];
14
+ /** what could not be captured, for the trace */
15
+ problems: string[];
16
+ };
17
+ /**
18
+ * Best-effort `failure.png` (`driver.screenshot`) and `screen-at-failure.txt` (`driver.snapshot`,
19
+ * rendered like `warden ui snapshot --all`) into `dir`. Never throws: the test's own error is
20
+ * the one worth reporting. Each call gets `timeoutMs` or what `budget` says is left, whichever is less; a
21
+ * screenshot that timed out skips the snapshot (the device is not answering).
22
+ */
23
+ export declare function captureFailure(driver: Driver, dir: string, timeoutMs?: number, budget?: CaptureBudget): Promise<FailureCapture>;
24
+ /**
25
+ * `console.jsonl` and `network.jsonl` of the app's attached devtools into `dir`. Best effort like
26
+ * {@link captureFailure}: never throws, gives up after `timeoutMs` (the network drain talks to the page).
27
+ */
28
+ export declare function captureDevtoolsArtifacts(facet: DevtoolsFacet, dir: string, timeoutMs?: number, budget?: CaptureBudget): Promise<FailureCapture>;
@@ -0,0 +1,60 @@
1
+ import type { App } from "../app";
2
+ import type { Device } from "../device";
3
+ import type { Platform } from "../driver/driver.types";
4
+ import type { E2eError } from "../errors";
5
+ import type { Screen } from "../screen/screen";
6
+ import type { Session } from "../session";
7
+ import type { TraceWriter } from "./trace";
8
+ export type { App, Device, Screen };
9
+ /** What every test receives from the runner; `test.extend` adds to it. */
10
+ export type TestFixtures = BaseFixtures;
11
+ /** What every test receives; `test.extend` adds to it. */
12
+ export type BaseFixtures = {
13
+ app: App;
14
+ device: Device;
15
+ screen: Screen;
16
+ platform: Platform;
17
+ };
18
+ export type LaunchMode = "fresh" | "none";
19
+ /**
20
+ * `test.use({ systemAlerts })`: what to do when a read or wait sees an alert or sheet over the app (a permission
21
+ * prompt, "Open in …?", the Keychain "Save Password?" sheet, an app alert). `accept` / `dismiss` answer it with the
22
+ * buttons `device.alert("accept")` / `("dismiss")` press; `off` (the default) leaves it for the test.
23
+ */
24
+ export type SystemAlerts = "accept" | "dismiss" | "off";
25
+ export declare const SYSTEM_ALERTS: readonly SystemAlerts[];
26
+ /** What a fixture factory gets for one attempt (or, for `beforeAll`/`afterAll`, the idle session). */
27
+ export type FixtureContext = {
28
+ /**
29
+ * Inside an attempt, a view of the session bound to that attempt (`attemptSession`): its `signal`, `step`, `span`
30
+ * and `trace` stay that attempt's, so a fixture never acts in a later test.
31
+ */
32
+ session: Session;
33
+ /** aborts at the attempt deadline; every driver call and poll must honour it */
34
+ signal: AbortSignal;
35
+ launch: LaunchMode;
36
+ actionTimeoutMs: number;
37
+ assertionTimeoutMs: number;
38
+ /** absent = `off` */
39
+ systemAlerts?: SystemAlerts;
40
+ };
41
+ /**
42
+ * Builds `app`/`device`/`screen`. Registered on `session.fixtures` or via `setDefaultFixtureFactory`.
43
+ * The runner itself calls `app.open()` before the body when `launch` is `"fresh"`.
44
+ */
45
+ export type FixtureFactory = (ctx: FixtureContext) => BaseFixtures | Promise<BaseFixtures>;
46
+ /** The running attempt: where steps, screenshots and failure files go. Read it through the session. */
47
+ export type AttemptContext = {
48
+ readonly signal: AbortSignal;
49
+ /** `<out>/<flowId>/<title-slug>-<hash6>/` */
50
+ readonly dir: string;
51
+ /** spec name tagging step lines */
52
+ readonly short: string;
53
+ readonly trace: TraceWriter;
54
+ /** nesting depth of the step in progress (indents console lines) */
55
+ depth: number;
56
+ /** files written into `dir`, relative to it (reported in `TestResult.artifacts`) */
57
+ readonly files: Set<string>;
58
+ /** failures kept by `expect.soft`; the attempt fails with all of them once the body has settled */
59
+ readonly softFailures: E2eError[];
60
+ };
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ export declare const testStatusSchema: z.ZodEnum<{
3
+ failed: "failed";
4
+ passed: "passed";
5
+ skipped: "skipped";
6
+ "timed-out": "timed-out";
7
+ }>;
8
+ export declare const testResultSchema: z.ZodObject<{
9
+ file: z.ZodString;
10
+ title: z.ZodArray<z.ZodString>;
11
+ status: z.ZodEnum<{
12
+ failed: "failed";
13
+ passed: "passed";
14
+ skipped: "skipped";
15
+ "timed-out": "timed-out";
16
+ }>;
17
+ durationMs: z.ZodNumber;
18
+ startedAt: z.ZodNumber;
19
+ only: z.ZodOptional<z.ZodBoolean>;
20
+ error: z.ZodOptional<z.ZodObject<{
21
+ message: z.ZodString;
22
+ code: z.ZodOptional<z.ZodString>;
23
+ stack: z.ZodOptional<z.ZodString>;
24
+ }, z.core.$strip>>;
25
+ artifacts: z.ZodOptional<z.ZodObject<{
26
+ dir: z.ZodString;
27
+ files: z.ZodArray<z.ZodString>;
28
+ }, z.core.$strip>>;
29
+ device: z.ZodObject<{
30
+ udid: z.ZodString;
31
+ platform: z.ZodEnum<{
32
+ ios: "ios";
33
+ android: "android";
34
+ }>;
35
+ name: z.ZodOptional<z.ZodString>;
36
+ }, z.core.$strip>;
37
+ }, z.core.$strip>;
38
+ export type TestResult = z.infer<typeof testResultSchema>;
39
+ export type TestStatus = z.infer<typeof testStatusSchema>;
40
+ export type ResultsWriter = {
41
+ readonly path: string;
42
+ /** one JSON line; `appendFileSync` so a crash right after never loses an earlier line */
43
+ append(result: TestResult): void;
44
+ };
45
+ /** Append-only `results.jsonl` writer; the directory is created on first use. */
46
+ export declare function openResults(path: string): ResultsWriter;
47
+ /** Valid results in file order. Blank, truncated (killed mid-write) and invalid lines are skipped. */
48
+ export declare function readResults(text: string): TestResult[];
@@ -0,0 +1,62 @@
1
+ import type { BaseFixtures, LaunchMode, SystemAlerts } from "./fixtures.types";
2
+ /** Headroom over the test timeout so warden's `AbortSignal` deadline fires before bun's own. */
3
+ export declare const BUN_TIMEOUT_MARGIN_MS = 30000;
4
+ export type UseOptions = {
5
+ timeout?: number;
6
+ actionTimeout?: number;
7
+ assertionTimeout?: number;
8
+ launch?: LaunchMode;
9
+ /**
10
+ * Answer an alert or sheet over the app whenever a read or wait sees one (it already has the snapshot, so a screen
11
+ * without one costs nothing): `dismiss` presses what `device.alert("dismiss")` would, `accept` what `("accept")`
12
+ * would, `off` (default) leaves it. It also answers the app's own alerts, so a test that taps those buttons
13
+ * itself keeps it `off`.
14
+ */
15
+ systemAlerts?: SystemAlerts;
16
+ };
17
+ export type ResolvedUse = {
18
+ timeoutMs: number;
19
+ actionTimeoutMs: number;
20
+ assertionTimeoutMs: number;
21
+ launch: LaunchMode;
22
+ systemAlerts: SystemAlerts;
23
+ };
24
+ /** `beforeEach`/`afterEach` hooks get the base fixtures (bun's own hooks take no arguments). */
25
+ export type Hook = (fixtures: BaseFixtures) => void | Promise<void>;
26
+ export type Scope = {
27
+ /** describe titles from the root down to this scope */
28
+ readonly title: readonly string[];
29
+ readonly parent: Scope | undefined;
30
+ use: UseOptions;
31
+ readonly beforeEach: Hook[];
32
+ readonly afterEach: Hook[];
33
+ /** accepted for API parity: bun runs a file's tests serially unless `--concurrent` */
34
+ readonly serial: boolean;
35
+ };
36
+ /** The innermost `describe` being collected, else the root scope of the spec file making the call. */
37
+ export declare function currentScope(): Scope;
38
+ /**
39
+ * The scope of a `describe` call. Created when `describe()` is called — its parent is whatever is
40
+ * current then — because bun runs the callback later (nested ones after the parent's callback has
41
+ * returned), when the stack no longer says where it belongs.
42
+ */
43
+ export declare function createScope(title: string, serial: boolean): Scope;
44
+ /** Runs `body` (bun's `describe` callback) with `scope` current. */
45
+ export declare function runInScope(scope: Scope, body: () => void): void;
46
+ /** Root first, `scope` last. */
47
+ export declare function scopeChain(scope: Scope): Scope[];
48
+ /** `beforeEach`: outermost first. `afterEach`: innermost first. */
49
+ export declare function hooksFor(scope: Scope, kind: "beforeEach" | "afterEach"): Hook[];
50
+ /** Session defaults (env contract) under the nearest `test.use`, under the test's own `timeout`. */
51
+ export declare function resolveUse(scope: Scope, defaults: Pick<ResolvedUse, "timeoutMs" | "actionTimeoutMs" | "assertionTimeoutMs">, testTimeout?: number): ResolvedUse;
52
+ /** Timeouts known at collection time: the env when set, else the contract defaults. */
53
+ export declare function registrationDefaults(env?: Readonly<Record<string, string | undefined>>): {
54
+ timeoutMs: number;
55
+ actionTimeoutMs: number;
56
+ assertionTimeoutMs: number;
57
+ platform: "ios" | "android" | undefined;
58
+ };
59
+ /** Records that a `.only` was collected (kept on `globalThis`: the preload and the specs may be separate bundles). */
60
+ export declare function markOnly(): void;
61
+ export declare function onlyWasSeen(): boolean;
62
+ export declare function resetOnly(): void;