@humanjs/playwright 0.3.0 → 0.4.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/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ReadKind, PersonalityConfig, HumanPlugin, Point, Personality } from '@humanjs/core';
2
- export { ActionResult, ActionType, BezierPathOptions, ComputeReadingDwellOptions, DwellProfile, HumanAction, HumanPlugin, HumanizePathOptions, Keystroke, KnownActionType, MouseProfile, Personality, PersonalityConfig, PersonalityExtension, PlanTypingOptions, PluginContext, Point, PresetName, ReadKind, ReadingProfile, Rng, ScrollProfile, ScrollSegment, TypingProfile, applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, computeReadingDwellMs, countWords, createRng, distracted, fast, humanizePath, planScroll, planTypeKeystrokes, precise, resolvePersonality } from '@humanjs/core';
2
+ export { ActionResult, ActionType, BezierPathOptions, ComputeReadingDwellOptions, DwellProfile, HumanAction, HumanPlugin, HumanizePathOptions, Keystroke, KnownActionType, MouseProfile, Personality, PersonalityConfig, PersonalityExtension, PlanTypingOptions, PluginContext, Point, PresetName, ReadKind, ReadingProfile, Rng, ScrollProfile, ScrollSegment, TypingProfile, applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, computeReadingDwellMs, countWords, createRng, distracted, fast, humanizePath, planScroll, planTypeKeystrokes, precise, resolvePersonality, sleep } from '@humanjs/core';
3
3
  import { Locator, BrowserContext, Page } from 'playwright';
4
+ export { Browser, BrowserContext, BrowserContextOptions, ElementHandle, LaunchOptions, Locator, Page, chromium, firefox, webkit } from 'playwright';
4
5
 
5
6
  /**
6
7
  * What to read:
@@ -39,9 +40,10 @@ interface ReadOptions {
39
40
  /**
40
41
  * For selector/Locator targets: trace a humanized cursor path through the
41
42
  * target's bounding box while the dwell elapses, like an eye tracking
42
- * lines of text. Off by defaultopt in when you want the humanization
43
- * signal (or when reading-time mouseover behavior matters, e.g. tooltips
44
- * that activate while a real user's cursor moves over the content).
43
+ * lines of text. **Defaults to `true`**"reading" implies looking, and
44
+ * looking implies motion. Pass `false` to skip motion when you only
45
+ * care about the temporal pattern (typical AI-agent use case where
46
+ * the cursor position is irrelevant).
45
47
  *
46
48
  * The scan path is generated by `planReadingScan` from `@humanjs/core`
47
49
  * (same deterministic-by-seed contract as the rest of the library) and
@@ -63,6 +65,183 @@ interface ReadResult {
63
65
  readonly kind: ReadKind;
64
66
  }
65
67
 
68
+ /**
69
+ * A captured frame in the timer-based capture session. `tMs` is the
70
+ * wall-clock offset (ms) from the capture start.
71
+ */
72
+ interface CapturedFrame {
73
+ readonly path: string;
74
+ readonly tMs: number;
75
+ }
76
+ /** Outcome of a finalized capture session. */
77
+ interface CaptureResult {
78
+ readonly dir: string;
79
+ readonly frames: readonly CapturedFrame[];
80
+ readonly startedAtMs: number;
81
+ readonly stoppedAtMs: number;
82
+ readonly format: 'jpeg' | 'png';
83
+ readonly fps: number;
84
+ /**
85
+ * Removes the temp directory + all frames. Called by `Recording.dispose()`
86
+ * and by the sweep-on-exit handler — not by the exporters, which are
87
+ * repeatable and read the frames without consuming them. Also called
88
+ * directly by the error path in `human.record()` when the callback throws.
89
+ */
90
+ cleanup(): Promise<void>;
91
+ }
92
+
93
+ /**
94
+ * Encoding quality preset. Picks the per-frame capture quality + the
95
+ * ffmpeg encode settings used to assemble them into a video.
96
+ *
97
+ * - `'fast'` — JPEG q=85, CRF 23, preset fast (iteration)
98
+ * - `'standard'` — JPEG q=90, CRF 20, preset fast (balanced)
99
+ * - `'high'` (default) — JPEG q=95, CRF 18, preset slow, tune animation (marketing-grade)
100
+ * - `'lossless'` — PNG capture, CRF 12, preset veryslow (archival; huge temp files)
101
+ */
102
+ type RecordingQuality = 'fast' | 'high' | 'lossless' | 'standard';
103
+ /** ffmpeg `-preset` values, ordered from fastest to slowest. */
104
+ type FfmpegPreset = 'fast' | 'faster' | 'medium' | 'slow' | 'slower' | 'superfast' | 'ultrafast' | 'veryfast' | 'veryslow';
105
+ /** ffmpeg `-tune` values for libx264. */
106
+ type FfmpegTune = 'animation' | 'fastdecode' | 'film' | 'grain' | 'stillimage' | 'zerolatency';
107
+ /** Options for {@link Recording.toVideo}. */
108
+ interface ToVideoOptions {
109
+ /** Encoding quality preset. Defaults to `'high'`. */
110
+ readonly quality?: RecordingQuality;
111
+ /** Override CRF (0–51, lower = better). Defaults to the quality preset's CRF. */
112
+ readonly crf?: number;
113
+ /** Override ffmpeg `-preset`. Defaults to the quality preset's preset. */
114
+ readonly preset?: FfmpegPreset;
115
+ /** Override ffmpeg `-tune`. Defaults to the quality preset's tune (if any). */
116
+ readonly tune?: FfmpegTune;
117
+ }
118
+ /** Options for {@link Recording.toGif}. */
119
+ interface ToGifOptions {
120
+ /**
121
+ * Frames per second of the output GIF. Defaults to `15` — high enough for
122
+ * smooth motion in most renderers, low enough to keep file size sane.
123
+ * Renderers cap GIF playback around 50fps anyway.
124
+ */
125
+ readonly fps?: number;
126
+ /**
127
+ * Scale the GIF to this width (pixels). Height is computed to preserve
128
+ * aspect ratio. Omit to keep the source viewport size — but most embedded
129
+ * GIFs (README, PR, Slack) look fine at 640–960px and weigh a fraction.
130
+ */
131
+ readonly width?: number;
132
+ }
133
+ /** One action captured during a recording, as emitted in {@link Timeline.events}. */
134
+ interface TimelineEvent {
135
+ readonly type: string;
136
+ readonly params: Readonly<Record<string, unknown>>;
137
+ /** Offset (ms) from the recording start when the action began. */
138
+ readonly tMs: number;
139
+ readonly durationMs: number;
140
+ /** Error message, present only if the action threw. */
141
+ readonly error?: string;
142
+ }
143
+ /** Structured action timeline of a recording. */
144
+ interface Timeline {
145
+ readonly version: 1;
146
+ readonly personality: string;
147
+ readonly seed: string | null;
148
+ readonly speed: string;
149
+ readonly durationMs: number;
150
+ readonly events: readonly TimelineEvent[];
151
+ }
152
+ /** Metadata passed from `human.record()` into the Recording constructor. */
153
+ interface RecordingTimelineSource {
154
+ readonly personality: string;
155
+ readonly seed: string | null;
156
+ readonly speed: string;
157
+ readonly events: readonly TimelineEvent[];
158
+ }
159
+ /**
160
+ * A recorded window of a humanized session. Returned by `human.record(cb)`.
161
+ *
162
+ * A Recording can hold a frame capture (`hasVideo === true`) OR be
163
+ * timeline-only (`hasVideo === false`). `toVideo()` / `toGif()` require a
164
+ * capture; `toTimeline()` and `.timeline` work either way.
165
+ *
166
+ * The video exporters are repeatable and interleavable — they read the
167
+ * captured frames without consuming them. Calling `dispose()` is OPTIONAL:
168
+ * the captured-frames temp dir is also swept by a `process.on('exit')`
169
+ * handler installed on first use, so casual scripts can skip it entirely.
170
+ * Call `dispose()` (or use `await using`) when you want to release the
171
+ * frames proactively — long-running services, batch jobs, etc.
172
+ */
173
+ declare class Recording {
174
+ #private;
175
+ constructor(capture: CaptureResult | null, windowStartMs: number, windowEndMs: number, timelineSource: RecordingTimelineSource);
176
+ /** Wall-clock duration of the recorded window. */
177
+ get durationMs(): number;
178
+ /** True if frames were captured during this recording. */
179
+ get hasVideo(): boolean;
180
+ /**
181
+ * The structured action timeline of this recording — same data that
182
+ * `toTimeline()` writes to disk.
183
+ */
184
+ get timeline(): Timeline;
185
+ /**
186
+ * Assembles the captured frames into a video at `outputPath`. The output
187
+ * format is inferred from the extension — `.mp4` (H.264, re-encoded
188
+ * with the configured quality) or `.webm` (VP9).
189
+ *
190
+ * Repeatable and interleavable with `toGif()` — the frame source is read,
191
+ * not consumed. Frames live until you call `dispose()` (or `await using`
192
+ * goes out of scope, or the process exits and the OS reaps `tmpdir`).
193
+ *
194
+ * @returns the resolved output path.
195
+ */
196
+ toVideo(outputPath: string, options?: ToVideoOptions): Promise<string>;
197
+ /**
198
+ * Assembles the captured frames into an animated GIF at `outputPath`.
199
+ * Optimized for embedding in READMEs, PRs, Slack, and docs — uses a
200
+ * per-recording palette (`palettegen` + `paletteuse`) with Bayer dithering
201
+ * so gradients stay smooth without exploding the file size.
202
+ *
203
+ * Repeatable and interleavable with `toVideo()` — call them in any order,
204
+ * any number of times. Frames live until you call `dispose()`.
205
+ *
206
+ * @returns the resolved output path.
207
+ */
208
+ toGif(outputPath: string, options?: ToGifOptions): Promise<string>;
209
+ /**
210
+ * Writes the structured action timeline to `outputPath` as JSON.
211
+ * Independent of `toVideo()` / `toGif()` — call before, after, in between,
212
+ * or instead. Safe to call multiple times. Unaffected by `dispose()`
213
+ * (the timeline lives in memory, not in the captured-frames temp dir).
214
+ *
215
+ * @returns the resolved output path.
216
+ */
217
+ toTimeline(outputPath: string): Promise<string>;
218
+ /**
219
+ * Releases the captured-frames temp directory. After this call, `toVideo()`
220
+ * and `toGif()` throw — but `toTimeline()` and the in-memory `timeline`
221
+ * still work because those don't depend on the frames.
222
+ *
223
+ * **Optional.** A process-exit handler also sweeps any un-disposed frame
224
+ * dirs, so casual scripts can skip this entirely. Call it explicitly when
225
+ * you want to release frames proactively (long-running services, batch
226
+ * jobs, or anywhere you want predictable disk usage).
227
+ *
228
+ * Idempotent. Safe to call on a Recording that never had a capture
229
+ * (timeline-only mode) — no-op there.
230
+ *
231
+ * Also wired to `Symbol.asyncDispose`, so the explicit-resource-management
232
+ * `await using` syntax (TypeScript ≥ 5.2 / Node ≥ 20.4) works:
233
+ *
234
+ * ```ts
235
+ * await using rec = await human.record(fn);
236
+ * await rec.toVideo('demo.mp4');
237
+ * await rec.toGif('demo.gif');
238
+ * // frames cleaned up automatically when `rec` goes out of scope
239
+ * ```
240
+ */
241
+ dispose(): Promise<void>;
242
+ [Symbol.asyncDispose](): Promise<void>;
243
+ }
244
+
66
245
  /**
67
246
  * What / where to scroll. The chosen axis (`'y'` by default, `'x'` via
68
247
  * `options.axis`) decides whether targets resolve along the vertical or
@@ -165,31 +344,6 @@ interface InstallMouseHelperOptions {
165
344
  /** Halo opacity behind the cursor. Defaults to `0.18`. */
166
345
  readonly haloOpacity?: number;
167
346
  }
168
- /**
169
- * Installs a visual cursor overlay that follows every `mousemove` on each
170
- * page in the target. Real synthetic motion from Playwright (e.g.
171
- * `human.click()`, `human.read(..., { withMotion: true })`) is *already*
172
- * happening — the page just doesn't render a system cursor for it. This
173
- * helper injects a HumanJS-styled SVG cursor that listens to mousemove
174
- * events and follows them, making the motion visible in headed demos and
175
- * screen recordings.
176
- *
177
- * Re-runs on every navigation via `addInitScript`, so the overlay survives
178
- * page reloads. Idempotent — guard flag on `window` prevents double-install.
179
- *
180
- * Accepts either a `Page` (overlay applies to that page) or a
181
- * `BrowserContext` (overlay applies to every page in the context, including
182
- * pages opened later).
183
- *
184
- * @example
185
- * ```ts
186
- * const browser = await chromium.launch({ headless: false });
187
- * const context = await browser.newContext();
188
- * await installMouseHelper(context);
189
- * const page = await context.newPage();
190
- * // human-driven actions are now visible in the page
191
- * ```
192
- */
193
347
  declare function installMouseHelper(target: BrowserContext | Page, options?: InstallMouseHelperOptions): Promise<void>;
194
348
 
195
349
  /**
@@ -310,6 +464,65 @@ interface Human {
310
464
  * Returns a {@link ScrollResult} for assertions in tests.
311
465
  */
312
466
  scroll(target?: ScrollTarget, options?: ScrollOptions): Promise<ScrollResult>;
467
+ /**
468
+ * Pause for `ms` milliseconds. Equivalent to importing `sleep` from
469
+ * `@humanjs/playwright` and calling it — exposed on the Human instance
470
+ * so users who already have `human` in scope don't need an extra import.
471
+ *
472
+ * Not a humanized action: this is a raw `setTimeout` wait, not scaled
473
+ * by personality or speed mode, and no plugin events fire. Use it for
474
+ * generic pacing between humanized actions.
475
+ */
476
+ sleep(ms: number): Promise<void>;
477
+ /**
478
+ * Records `fn`'s actions. Returns a {@link Recording} you can export
479
+ * to mp4/webm video (`toVideo`), JSON timeline (`toTimeline`), or both.
480
+ *
481
+ * By default, frames are captured from the browser via timer-polled
482
+ * `page.screenshot()` — fully controllable quality, not limited by
483
+ * Playwright's recordVideo bitrate ceiling. Pass `{ video: false }` to
484
+ * skip capture entirely (timeline-only mode, zero video overhead).
485
+ *
486
+ * Plugins observe `'record'` in `beforeAction` / `afterAction` so
487
+ * recording shows up alongside the other primitives in observability
488
+ * pipelines.
489
+ *
490
+ * Single-use per session: `Recording.toVideo()` finalizes the captured
491
+ * frames, so calling `human.record()` twice on the same human throws.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * // Default: video + timeline
496
+ * const rec = await human.record(async () => {
497
+ * await human.click('#login');
498
+ * });
499
+ * await rec.toVideo('demo.mp4');
500
+ * await rec.toTimeline('demo.json');
501
+ *
502
+ * // Timeline-only, no video overhead
503
+ * const rec = await human.record({ video: false }, async () => {
504
+ * await human.click('#login');
505
+ * });
506
+ * await rec.toTimeline('demo.json');
507
+ * ```
508
+ */
509
+ record(fn: () => Promise<void>): Promise<Recording>;
510
+ record(options: HumanRecordOptions, fn: () => Promise<void>): Promise<Recording>;
511
+ }
512
+ /** Options for {@link Human.record}. */
513
+ interface HumanRecordOptions {
514
+ /**
515
+ * Whether to capture frames for video output. Defaults to `true`.
516
+ * Set to `false` for timeline-only recordings (no capture loop, no
517
+ * temp files, no encoding overhead — `toTimeline()` still works).
518
+ */
519
+ readonly video?: boolean;
520
+ /**
521
+ * Capture quality preset. Controls per-frame JPEG quality (or PNG for
522
+ * lossless) AND the ffmpeg encode settings used by `toVideo()`.
523
+ * Defaults to `'high'`. Ignored when `video: false`.
524
+ */
525
+ readonly quality?: RecordingQuality;
313
526
  }
314
527
  /**
315
528
  * Creates a humanized session bound to a Playwright `Page`.
@@ -332,4 +545,4 @@ interface Human {
332
545
  */
333
546
  declare function createHuman(page: Page, options?: CreateHumanOptions): Promise<Human>;
334
547
 
335
- export { type CreateHumanOptions, type Human, type InstallMouseHelperOptions, type ReadOptions, type ReadResult, type ReadTarget, type ScrollOptions, type ScrollResult, type ScrollTarget, type Speed, createHuman, installMouseHelper };
548
+ export { type CreateHumanOptions, type FfmpegPreset, type FfmpegTune, type Human, type HumanRecordOptions, type InstallMouseHelperOptions, type ReadOptions, type ReadResult, type ReadTarget, Recording, type RecordingQuality, type ScrollOptions, type ScrollResult, type ScrollTarget, type Speed, type Timeline, type TimelineEvent, type ToGifOptions, type ToVideoOptions, createHuman, installMouseHelper };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ReadKind, PersonalityConfig, HumanPlugin, Point, Personality } from '@humanjs/core';
2
- export { ActionResult, ActionType, BezierPathOptions, ComputeReadingDwellOptions, DwellProfile, HumanAction, HumanPlugin, HumanizePathOptions, Keystroke, KnownActionType, MouseProfile, Personality, PersonalityConfig, PersonalityExtension, PlanTypingOptions, PluginContext, Point, PresetName, ReadKind, ReadingProfile, Rng, ScrollProfile, ScrollSegment, TypingProfile, applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, computeReadingDwellMs, countWords, createRng, distracted, fast, humanizePath, planScroll, planTypeKeystrokes, precise, resolvePersonality } from '@humanjs/core';
2
+ export { ActionResult, ActionType, BezierPathOptions, ComputeReadingDwellOptions, DwellProfile, HumanAction, HumanPlugin, HumanizePathOptions, Keystroke, KnownActionType, MouseProfile, Personality, PersonalityConfig, PersonalityExtension, PlanTypingOptions, PluginContext, Point, PresetName, ReadKind, ReadingProfile, Rng, ScrollProfile, ScrollSegment, TypingProfile, applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, computeReadingDwellMs, countWords, createRng, distracted, fast, humanizePath, planScroll, planTypeKeystrokes, precise, resolvePersonality, sleep } from '@humanjs/core';
3
3
  import { Locator, BrowserContext, Page } from 'playwright';
4
+ export { Browser, BrowserContext, BrowserContextOptions, ElementHandle, LaunchOptions, Locator, Page, chromium, firefox, webkit } from 'playwright';
4
5
 
5
6
  /**
6
7
  * What to read:
@@ -39,9 +40,10 @@ interface ReadOptions {
39
40
  /**
40
41
  * For selector/Locator targets: trace a humanized cursor path through the
41
42
  * target's bounding box while the dwell elapses, like an eye tracking
42
- * lines of text. Off by defaultopt in when you want the humanization
43
- * signal (or when reading-time mouseover behavior matters, e.g. tooltips
44
- * that activate while a real user's cursor moves over the content).
43
+ * lines of text. **Defaults to `true`**"reading" implies looking, and
44
+ * looking implies motion. Pass `false` to skip motion when you only
45
+ * care about the temporal pattern (typical AI-agent use case where
46
+ * the cursor position is irrelevant).
45
47
  *
46
48
  * The scan path is generated by `planReadingScan` from `@humanjs/core`
47
49
  * (same deterministic-by-seed contract as the rest of the library) and
@@ -63,6 +65,183 @@ interface ReadResult {
63
65
  readonly kind: ReadKind;
64
66
  }
65
67
 
68
+ /**
69
+ * A captured frame in the timer-based capture session. `tMs` is the
70
+ * wall-clock offset (ms) from the capture start.
71
+ */
72
+ interface CapturedFrame {
73
+ readonly path: string;
74
+ readonly tMs: number;
75
+ }
76
+ /** Outcome of a finalized capture session. */
77
+ interface CaptureResult {
78
+ readonly dir: string;
79
+ readonly frames: readonly CapturedFrame[];
80
+ readonly startedAtMs: number;
81
+ readonly stoppedAtMs: number;
82
+ readonly format: 'jpeg' | 'png';
83
+ readonly fps: number;
84
+ /**
85
+ * Removes the temp directory + all frames. Called by `Recording.dispose()`
86
+ * and by the sweep-on-exit handler — not by the exporters, which are
87
+ * repeatable and read the frames without consuming them. Also called
88
+ * directly by the error path in `human.record()` when the callback throws.
89
+ */
90
+ cleanup(): Promise<void>;
91
+ }
92
+
93
+ /**
94
+ * Encoding quality preset. Picks the per-frame capture quality + the
95
+ * ffmpeg encode settings used to assemble them into a video.
96
+ *
97
+ * - `'fast'` — JPEG q=85, CRF 23, preset fast (iteration)
98
+ * - `'standard'` — JPEG q=90, CRF 20, preset fast (balanced)
99
+ * - `'high'` (default) — JPEG q=95, CRF 18, preset slow, tune animation (marketing-grade)
100
+ * - `'lossless'` — PNG capture, CRF 12, preset veryslow (archival; huge temp files)
101
+ */
102
+ type RecordingQuality = 'fast' | 'high' | 'lossless' | 'standard';
103
+ /** ffmpeg `-preset` values, ordered from fastest to slowest. */
104
+ type FfmpegPreset = 'fast' | 'faster' | 'medium' | 'slow' | 'slower' | 'superfast' | 'ultrafast' | 'veryfast' | 'veryslow';
105
+ /** ffmpeg `-tune` values for libx264. */
106
+ type FfmpegTune = 'animation' | 'fastdecode' | 'film' | 'grain' | 'stillimage' | 'zerolatency';
107
+ /** Options for {@link Recording.toVideo}. */
108
+ interface ToVideoOptions {
109
+ /** Encoding quality preset. Defaults to `'high'`. */
110
+ readonly quality?: RecordingQuality;
111
+ /** Override CRF (0–51, lower = better). Defaults to the quality preset's CRF. */
112
+ readonly crf?: number;
113
+ /** Override ffmpeg `-preset`. Defaults to the quality preset's preset. */
114
+ readonly preset?: FfmpegPreset;
115
+ /** Override ffmpeg `-tune`. Defaults to the quality preset's tune (if any). */
116
+ readonly tune?: FfmpegTune;
117
+ }
118
+ /** Options for {@link Recording.toGif}. */
119
+ interface ToGifOptions {
120
+ /**
121
+ * Frames per second of the output GIF. Defaults to `15` — high enough for
122
+ * smooth motion in most renderers, low enough to keep file size sane.
123
+ * Renderers cap GIF playback around 50fps anyway.
124
+ */
125
+ readonly fps?: number;
126
+ /**
127
+ * Scale the GIF to this width (pixels). Height is computed to preserve
128
+ * aspect ratio. Omit to keep the source viewport size — but most embedded
129
+ * GIFs (README, PR, Slack) look fine at 640–960px and weigh a fraction.
130
+ */
131
+ readonly width?: number;
132
+ }
133
+ /** One action captured during a recording, as emitted in {@link Timeline.events}. */
134
+ interface TimelineEvent {
135
+ readonly type: string;
136
+ readonly params: Readonly<Record<string, unknown>>;
137
+ /** Offset (ms) from the recording start when the action began. */
138
+ readonly tMs: number;
139
+ readonly durationMs: number;
140
+ /** Error message, present only if the action threw. */
141
+ readonly error?: string;
142
+ }
143
+ /** Structured action timeline of a recording. */
144
+ interface Timeline {
145
+ readonly version: 1;
146
+ readonly personality: string;
147
+ readonly seed: string | null;
148
+ readonly speed: string;
149
+ readonly durationMs: number;
150
+ readonly events: readonly TimelineEvent[];
151
+ }
152
+ /** Metadata passed from `human.record()` into the Recording constructor. */
153
+ interface RecordingTimelineSource {
154
+ readonly personality: string;
155
+ readonly seed: string | null;
156
+ readonly speed: string;
157
+ readonly events: readonly TimelineEvent[];
158
+ }
159
+ /**
160
+ * A recorded window of a humanized session. Returned by `human.record(cb)`.
161
+ *
162
+ * A Recording can hold a frame capture (`hasVideo === true`) OR be
163
+ * timeline-only (`hasVideo === false`). `toVideo()` / `toGif()` require a
164
+ * capture; `toTimeline()` and `.timeline` work either way.
165
+ *
166
+ * The video exporters are repeatable and interleavable — they read the
167
+ * captured frames without consuming them. Calling `dispose()` is OPTIONAL:
168
+ * the captured-frames temp dir is also swept by a `process.on('exit')`
169
+ * handler installed on first use, so casual scripts can skip it entirely.
170
+ * Call `dispose()` (or use `await using`) when you want to release the
171
+ * frames proactively — long-running services, batch jobs, etc.
172
+ */
173
+ declare class Recording {
174
+ #private;
175
+ constructor(capture: CaptureResult | null, windowStartMs: number, windowEndMs: number, timelineSource: RecordingTimelineSource);
176
+ /** Wall-clock duration of the recorded window. */
177
+ get durationMs(): number;
178
+ /** True if frames were captured during this recording. */
179
+ get hasVideo(): boolean;
180
+ /**
181
+ * The structured action timeline of this recording — same data that
182
+ * `toTimeline()` writes to disk.
183
+ */
184
+ get timeline(): Timeline;
185
+ /**
186
+ * Assembles the captured frames into a video at `outputPath`. The output
187
+ * format is inferred from the extension — `.mp4` (H.264, re-encoded
188
+ * with the configured quality) or `.webm` (VP9).
189
+ *
190
+ * Repeatable and interleavable with `toGif()` — the frame source is read,
191
+ * not consumed. Frames live until you call `dispose()` (or `await using`
192
+ * goes out of scope, or the process exits and the OS reaps `tmpdir`).
193
+ *
194
+ * @returns the resolved output path.
195
+ */
196
+ toVideo(outputPath: string, options?: ToVideoOptions): Promise<string>;
197
+ /**
198
+ * Assembles the captured frames into an animated GIF at `outputPath`.
199
+ * Optimized for embedding in READMEs, PRs, Slack, and docs — uses a
200
+ * per-recording palette (`palettegen` + `paletteuse`) with Bayer dithering
201
+ * so gradients stay smooth without exploding the file size.
202
+ *
203
+ * Repeatable and interleavable with `toVideo()` — call them in any order,
204
+ * any number of times. Frames live until you call `dispose()`.
205
+ *
206
+ * @returns the resolved output path.
207
+ */
208
+ toGif(outputPath: string, options?: ToGifOptions): Promise<string>;
209
+ /**
210
+ * Writes the structured action timeline to `outputPath` as JSON.
211
+ * Independent of `toVideo()` / `toGif()` — call before, after, in between,
212
+ * or instead. Safe to call multiple times. Unaffected by `dispose()`
213
+ * (the timeline lives in memory, not in the captured-frames temp dir).
214
+ *
215
+ * @returns the resolved output path.
216
+ */
217
+ toTimeline(outputPath: string): Promise<string>;
218
+ /**
219
+ * Releases the captured-frames temp directory. After this call, `toVideo()`
220
+ * and `toGif()` throw — but `toTimeline()` and the in-memory `timeline`
221
+ * still work because those don't depend on the frames.
222
+ *
223
+ * **Optional.** A process-exit handler also sweeps any un-disposed frame
224
+ * dirs, so casual scripts can skip this entirely. Call it explicitly when
225
+ * you want to release frames proactively (long-running services, batch
226
+ * jobs, or anywhere you want predictable disk usage).
227
+ *
228
+ * Idempotent. Safe to call on a Recording that never had a capture
229
+ * (timeline-only mode) — no-op there.
230
+ *
231
+ * Also wired to `Symbol.asyncDispose`, so the explicit-resource-management
232
+ * `await using` syntax (TypeScript ≥ 5.2 / Node ≥ 20.4) works:
233
+ *
234
+ * ```ts
235
+ * await using rec = await human.record(fn);
236
+ * await rec.toVideo('demo.mp4');
237
+ * await rec.toGif('demo.gif');
238
+ * // frames cleaned up automatically when `rec` goes out of scope
239
+ * ```
240
+ */
241
+ dispose(): Promise<void>;
242
+ [Symbol.asyncDispose](): Promise<void>;
243
+ }
244
+
66
245
  /**
67
246
  * What / where to scroll. The chosen axis (`'y'` by default, `'x'` via
68
247
  * `options.axis`) decides whether targets resolve along the vertical or
@@ -165,31 +344,6 @@ interface InstallMouseHelperOptions {
165
344
  /** Halo opacity behind the cursor. Defaults to `0.18`. */
166
345
  readonly haloOpacity?: number;
167
346
  }
168
- /**
169
- * Installs a visual cursor overlay that follows every `mousemove` on each
170
- * page in the target. Real synthetic motion from Playwright (e.g.
171
- * `human.click()`, `human.read(..., { withMotion: true })`) is *already*
172
- * happening — the page just doesn't render a system cursor for it. This
173
- * helper injects a HumanJS-styled SVG cursor that listens to mousemove
174
- * events and follows them, making the motion visible in headed demos and
175
- * screen recordings.
176
- *
177
- * Re-runs on every navigation via `addInitScript`, so the overlay survives
178
- * page reloads. Idempotent — guard flag on `window` prevents double-install.
179
- *
180
- * Accepts either a `Page` (overlay applies to that page) or a
181
- * `BrowserContext` (overlay applies to every page in the context, including
182
- * pages opened later).
183
- *
184
- * @example
185
- * ```ts
186
- * const browser = await chromium.launch({ headless: false });
187
- * const context = await browser.newContext();
188
- * await installMouseHelper(context);
189
- * const page = await context.newPage();
190
- * // human-driven actions are now visible in the page
191
- * ```
192
- */
193
347
  declare function installMouseHelper(target: BrowserContext | Page, options?: InstallMouseHelperOptions): Promise<void>;
194
348
 
195
349
  /**
@@ -310,6 +464,65 @@ interface Human {
310
464
  * Returns a {@link ScrollResult} for assertions in tests.
311
465
  */
312
466
  scroll(target?: ScrollTarget, options?: ScrollOptions): Promise<ScrollResult>;
467
+ /**
468
+ * Pause for `ms` milliseconds. Equivalent to importing `sleep` from
469
+ * `@humanjs/playwright` and calling it — exposed on the Human instance
470
+ * so users who already have `human` in scope don't need an extra import.
471
+ *
472
+ * Not a humanized action: this is a raw `setTimeout` wait, not scaled
473
+ * by personality or speed mode, and no plugin events fire. Use it for
474
+ * generic pacing between humanized actions.
475
+ */
476
+ sleep(ms: number): Promise<void>;
477
+ /**
478
+ * Records `fn`'s actions. Returns a {@link Recording} you can export
479
+ * to mp4/webm video (`toVideo`), JSON timeline (`toTimeline`), or both.
480
+ *
481
+ * By default, frames are captured from the browser via timer-polled
482
+ * `page.screenshot()` — fully controllable quality, not limited by
483
+ * Playwright's recordVideo bitrate ceiling. Pass `{ video: false }` to
484
+ * skip capture entirely (timeline-only mode, zero video overhead).
485
+ *
486
+ * Plugins observe `'record'` in `beforeAction` / `afterAction` so
487
+ * recording shows up alongside the other primitives in observability
488
+ * pipelines.
489
+ *
490
+ * Single-use per session: `Recording.toVideo()` finalizes the captured
491
+ * frames, so calling `human.record()` twice on the same human throws.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * // Default: video + timeline
496
+ * const rec = await human.record(async () => {
497
+ * await human.click('#login');
498
+ * });
499
+ * await rec.toVideo('demo.mp4');
500
+ * await rec.toTimeline('demo.json');
501
+ *
502
+ * // Timeline-only, no video overhead
503
+ * const rec = await human.record({ video: false }, async () => {
504
+ * await human.click('#login');
505
+ * });
506
+ * await rec.toTimeline('demo.json');
507
+ * ```
508
+ */
509
+ record(fn: () => Promise<void>): Promise<Recording>;
510
+ record(options: HumanRecordOptions, fn: () => Promise<void>): Promise<Recording>;
511
+ }
512
+ /** Options for {@link Human.record}. */
513
+ interface HumanRecordOptions {
514
+ /**
515
+ * Whether to capture frames for video output. Defaults to `true`.
516
+ * Set to `false` for timeline-only recordings (no capture loop, no
517
+ * temp files, no encoding overhead — `toTimeline()` still works).
518
+ */
519
+ readonly video?: boolean;
520
+ /**
521
+ * Capture quality preset. Controls per-frame JPEG quality (or PNG for
522
+ * lossless) AND the ffmpeg encode settings used by `toVideo()`.
523
+ * Defaults to `'high'`. Ignored when `video: false`.
524
+ */
525
+ readonly quality?: RecordingQuality;
313
526
  }
314
527
  /**
315
528
  * Creates a humanized session bound to a Playwright `Page`.
@@ -332,4 +545,4 @@ interface Human {
332
545
  */
333
546
  declare function createHuman(page: Page, options?: CreateHumanOptions): Promise<Human>;
334
547
 
335
- export { type CreateHumanOptions, type Human, type InstallMouseHelperOptions, type ReadOptions, type ReadResult, type ReadTarget, type ScrollOptions, type ScrollResult, type ScrollTarget, type Speed, createHuman, installMouseHelper };
548
+ export { type CreateHumanOptions, type FfmpegPreset, type FfmpegTune, type Human, type HumanRecordOptions, type InstallMouseHelperOptions, type ReadOptions, type ReadResult, type ReadTarget, Recording, type RecordingQuality, type ScrollOptions, type ScrollResult, type ScrollTarget, type Speed, type Timeline, type TimelineEvent, type ToGifOptions, type ToVideoOptions, createHuman, installMouseHelper };