@humanjs/recorder 0.2.0 → 0.3.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/README.md +3 -0
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ The returned `Recording` has:
|
|
|
58
58
|
| `rec.toVideo(path, options?)` | Write an mp4 or webm. Repeatable. |
|
|
59
59
|
| `rec.toGif(path, options?)` | Write an animated GIF (palette-optimized, defaults to 15fps). Repeatable. |
|
|
60
60
|
| `rec.toTimeline(path)` | Write the structured JSON timeline. Repeatable. |
|
|
61
|
+
| `rec.toHumanJS(path)` | Write a runnable HumanJS script that replays the session. |
|
|
62
|
+
| `rec.toPlaywright(path)` | Write a `@playwright/test` spec (humanized — uses HumanJS). |
|
|
61
63
|
| `rec.timeline` | Read the in-memory `Timeline` object. |
|
|
62
64
|
| `rec.durationMs` | Wall-clock duration of the recorded window. |
|
|
63
65
|
| `rec.hasVideo` | True if frames were captured (i.e. `output` was set). |
|
|
@@ -81,6 +83,7 @@ await record(
|
|
|
81
83
|
{
|
|
82
84
|
output: 'demo.mp4', // .mp4 / .webm / .gif — omit to skip video entirely
|
|
83
85
|
quality: 'high', // 'fast' | 'standard' | 'high' (default) | 'lossless'
|
|
86
|
+
captureInputs: true, // capture typed/pasted text for code export (default; passwords masked)
|
|
84
87
|
url: 'https://example.com', // optional — navigate before the callback
|
|
85
88
|
personality: 'careful', // any PersonalityConfig
|
|
86
89
|
seed: 'session-42', // deterministic when set
|
package/dist/index.cjs
CHANGED
|
@@ -19,7 +19,9 @@ async function record(optionsOrFn, maybeFn) {
|
|
|
19
19
|
const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
|
|
20
20
|
const {
|
|
21
21
|
output,
|
|
22
|
+
name,
|
|
22
23
|
quality,
|
|
24
|
+
captureInputs,
|
|
23
25
|
url,
|
|
24
26
|
viewport,
|
|
25
27
|
headless,
|
|
@@ -49,7 +51,7 @@ async function record(optionsOrFn, maybeFn) {
|
|
|
49
51
|
if (url) await page.goto(url);
|
|
50
52
|
const human = await playwright.createHuman(page, createHumanOptions);
|
|
51
53
|
const recording = await human.record(
|
|
52
|
-
{ video: wantsCapture, quality: resolvedQuality },
|
|
54
|
+
{ name, video: wantsCapture, quality: resolvedQuality, captureInputs },
|
|
53
55
|
() => fn(human, page)
|
|
54
56
|
);
|
|
55
57
|
if (wantsCapture && output) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/record/index.ts"],"names":["createHuman","extname","installMouseHelper","browser","chromium","context","page"],"mappings":"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","file":"index.cjs","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContext,\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /**\n * Viewport dimensions. Overrides the quality preset's viewport. Applies to\n * the default and persistent (`userDataDir`) modes; ignored when attaching\n * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).\n */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Record in a **persistent profile** at this directory so logins and\n * cookies survive across runs — sign in once (in a headed run), and later\n * recordings start authenticated. Uses `launchPersistentContext` under the\n * hood (a single context); `headless`, `launch`, `channel`, and `viewport`\n * still apply. Starts empty the first time.\n */\n readonly userDataDir?: string;\n /**\n * Record by **attaching to a browser you already launched** over CDP (e.g.\n * `\"http://localhost:9222\"`). Reuses that browser's existing context — your\n * real logins, tabs, extensions. Start the browser yourself with\n * `--remote-debugging-port`. HumanJS never closes a browser it attached to;\n * it only borrows it. Takes precedence over `userDataDir`.\n *\n * `launch` / `headless` / `channel` / `viewport` don't apply here — you're\n * borrowing a window HumanJS doesn't own, so the browser's real window size\n * wins. Forcing a `viewport` would only *emulate* one and letterbox the\n * capture. For a fixed recording resolution, launch the browser yourself\n * with `--window-size=1920,1080`, or use the default / persistent modes.\n */\n readonly cdpUrl?: string;\n /**\n * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that\n * installed browser's binary instead of bundled Chromium (applies to the\n * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse\n * your existing profile — pair it with `userDataDir` (persistent) or\n * `cdpUrl` (attach) for real logins.\n */\n readonly channel?: string;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches (or attaches to) a browser, opens a\n * page, creates a humanized session, runs `fn`, and returns a\n * {@link Recording} you can export to video, JSON timeline, or read in-memory.\n *\n * Browser source (default → ephemeral fresh profile):\n * - `userDataDir` — a persistent profile that keeps logins across runs.\n * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);\n * never closed on finish, only released.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Stay signed in across runs (persistent profile)\n * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {\n * await human.goto('https://app.example.com/dashboard');\n * });\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n quality,\n url,\n viewport,\n headless,\n launch,\n context: contextOptions,\n userDataDir,\n cdpUrl,\n channel,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const { page, dispose } = await acquireRecordingContext({\n cdpUrl,\n userDataDir,\n channel,\n headless: headless ?? false,\n viewport: resolvedViewport,\n launch,\n context: contextOptions,\n cursor,\n });\n\n try {\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record({ video: wantsCapture, quality: resolvedQuality }, () =>\n fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await dispose();\n }\n}\n\n/** Internal options for {@link acquireRecordingContext}. */\ninterface AcquireOptions {\n readonly cdpUrl?: string;\n readonly userDataDir?: string;\n readonly channel?: string;\n readonly headless: boolean;\n readonly viewport: { readonly width: number; readonly height: number };\n readonly launch?: LaunchOptions;\n readonly context?: BrowserContextOptions;\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/**\n * Resolves the browser source — CDP attach > persistent profile > ephemeral\n * — and returns the page to drive plus a `dispose` that tears down only what\n * we created. A CDP-attached browser is borrowed: `dispose` leaves it (and\n * its context) untouched so we never close the caller's real browser.\n */\nasync function acquireRecordingContext(\n opts: AcquireOptions,\n): Promise<{ page: Page; dispose: () => Promise<void> }> {\n const cursorOptions = typeof opts.cursor === 'object' ? opts.cursor : undefined;\n const installCursor = async (ctx: BrowserContext): Promise<void> => {\n if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);\n };\n\n // Attach to a browser the caller launched. Reuse its existing context so we\n // record their real session; only make a fresh one if there's none. Never\n // close it on dispose — it's borrowed.\n if (opts.cdpUrl) {\n const browser = await chromium.connectOverCDP(opts.cdpUrl);\n const context = browser.contexts()[0] ?? (await browser.newContext({ ...opts.context }));\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return { page, dispose: async () => undefined };\n }\n\n // Persistent profile — the context owns its browser, so closing it on\n // dispose tears everything down.\n if (opts.userDataDir) {\n const context = await chromium.launchPersistentContext(opts.userDataDir, {\n ...opts.launch,\n ...opts.context,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n viewport: opts.viewport,\n });\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n },\n };\n }\n\n // Ephemeral (default) — a fresh throwaway browser + context.\n const browser = await chromium.launch({\n ...opts.launch,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n });\n const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });\n await installCursor(context);\n const page = await context.newPage();\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/record/index.ts"],"names":["createHuman","extname","installMouseHelper","browser","chromium","context","page"],"mappings":"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","file":"index.cjs","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContext,\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Optional label for the recording — becomes the title of a generated\n * `toPlaywright()` test. See `@humanjs/playwright`'s `HumanRecordOptions.name`.\n */\n readonly name?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /**\n * Capture actual typed/pasted text into the timeline, so `toHumanJS()` /\n * `toPlaywright()` exports include the values. Defaults to `true`; password\n * fields are always masked. Set `false` to record no input values (exports\n * emit empty-string placeholders). See `@humanjs/playwright`'s\n * `HumanRecordOptions.captureInputs`.\n */\n readonly captureInputs?: boolean;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /**\n * Viewport dimensions. Overrides the quality preset's viewport. Applies to\n * the default and persistent (`userDataDir`) modes; ignored when attaching\n * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).\n */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Record in a **persistent profile** at this directory so logins and\n * cookies survive across runs — sign in once (in a headed run), and later\n * recordings start authenticated. Uses `launchPersistentContext` under the\n * hood (a single context); `headless`, `launch`, `channel`, and `viewport`\n * still apply. Starts empty the first time.\n */\n readonly userDataDir?: string;\n /**\n * Record by **attaching to a browser you already launched** over CDP (e.g.\n * `\"http://localhost:9222\"`). Reuses that browser's existing context — your\n * real logins, tabs, extensions. Start the browser yourself with\n * `--remote-debugging-port`. HumanJS never closes a browser it attached to;\n * it only borrows it. Takes precedence over `userDataDir`.\n *\n * `launch` / `headless` / `channel` / `viewport` don't apply here — you're\n * borrowing a window HumanJS doesn't own, so the browser's real window size\n * wins. Forcing a `viewport` would only *emulate* one and letterbox the\n * capture. For a fixed recording resolution, launch the browser yourself\n * with `--window-size=1920,1080`, or use the default / persistent modes.\n */\n readonly cdpUrl?: string;\n /**\n * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that\n * installed browser's binary instead of bundled Chromium (applies to the\n * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse\n * your existing profile — pair it with `userDataDir` (persistent) or\n * `cdpUrl` (attach) for real logins.\n */\n readonly channel?: string;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches (or attaches to) a browser, opens a\n * page, creates a humanized session, runs `fn`, and returns a\n * {@link Recording} you can export to video, JSON timeline, or read in-memory.\n *\n * Browser source (default → ephemeral fresh profile):\n * - `userDataDir` — a persistent profile that keeps logins across runs.\n * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);\n * never closed on finish, only released.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Stay signed in across runs (persistent profile)\n * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {\n * await human.goto('https://app.example.com/dashboard');\n * });\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n name,\n quality,\n captureInputs,\n url,\n viewport,\n headless,\n launch,\n context: contextOptions,\n userDataDir,\n cdpUrl,\n channel,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const { page, dispose } = await acquireRecordingContext({\n cdpUrl,\n userDataDir,\n channel,\n headless: headless ?? false,\n viewport: resolvedViewport,\n launch,\n context: contextOptions,\n cursor,\n });\n\n try {\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record(\n { name, video: wantsCapture, quality: resolvedQuality, captureInputs },\n () => fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await dispose();\n }\n}\n\n/** Internal options for {@link acquireRecordingContext}. */\ninterface AcquireOptions {\n readonly cdpUrl?: string;\n readonly userDataDir?: string;\n readonly channel?: string;\n readonly headless: boolean;\n readonly viewport: { readonly width: number; readonly height: number };\n readonly launch?: LaunchOptions;\n readonly context?: BrowserContextOptions;\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/**\n * Resolves the browser source — CDP attach > persistent profile > ephemeral\n * — and returns the page to drive plus a `dispose` that tears down only what\n * we created. A CDP-attached browser is borrowed: `dispose` leaves it (and\n * its context) untouched so we never close the caller's real browser.\n */\nasync function acquireRecordingContext(\n opts: AcquireOptions,\n): Promise<{ page: Page; dispose: () => Promise<void> }> {\n const cursorOptions = typeof opts.cursor === 'object' ? opts.cursor : undefined;\n const installCursor = async (ctx: BrowserContext): Promise<void> => {\n if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);\n };\n\n // Attach to a browser the caller launched. Reuse its existing context so we\n // record their real session; only make a fresh one if there's none. Never\n // close it on dispose — it's borrowed.\n if (opts.cdpUrl) {\n const browser = await chromium.connectOverCDP(opts.cdpUrl);\n const context = browser.contexts()[0] ?? (await browser.newContext({ ...opts.context }));\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return { page, dispose: async () => undefined };\n }\n\n // Persistent profile — the context owns its browser, so closing it on\n // dispose tears everything down.\n if (opts.userDataDir) {\n const context = await chromium.launchPersistentContext(opts.userDataDir, {\n ...opts.launch,\n ...opts.context,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n viewport: opts.viewport,\n });\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n },\n };\n }\n\n // Ephemeral (default) — a fresh throwaway browser + context.\n const browser = await chromium.launch({\n ...opts.launch,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n });\n const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });\n await installCursor(context);\n const page = await context.newPage();\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n },\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -14,6 +14,11 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
14
14
|
* the structured action timeline via `.toTimeline()` / `.timeline`.
|
|
15
15
|
*/
|
|
16
16
|
readonly output?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional label for the recording — becomes the title of a generated
|
|
19
|
+
* `toPlaywright()` test. See `@humanjs/playwright`'s `HumanRecordOptions.name`.
|
|
20
|
+
*/
|
|
21
|
+
readonly name?: string;
|
|
17
22
|
/**
|
|
18
23
|
* Quality preset. Picks both source viewport and ffmpeg encoding settings.
|
|
19
24
|
* Defaults to `'high'` (visually-lossless 1080p).
|
|
@@ -24,6 +29,14 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
24
29
|
* - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation
|
|
25
30
|
*/
|
|
26
31
|
readonly quality?: RecordingQuality;
|
|
32
|
+
/**
|
|
33
|
+
* Capture actual typed/pasted text into the timeline, so `toHumanJS()` /
|
|
34
|
+
* `toPlaywright()` exports include the values. Defaults to `true`; password
|
|
35
|
+
* fields are always masked. Set `false` to record no input values (exports
|
|
36
|
+
* emit empty-string placeholders). See `@humanjs/playwright`'s
|
|
37
|
+
* `HumanRecordOptions.captureInputs`.
|
|
38
|
+
*/
|
|
39
|
+
readonly captureInputs?: boolean;
|
|
27
40
|
/** Optional URL to navigate to before the callback runs. */
|
|
28
41
|
readonly url?: string;
|
|
29
42
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
14
14
|
* the structured action timeline via `.toTimeline()` / `.timeline`.
|
|
15
15
|
*/
|
|
16
16
|
readonly output?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional label for the recording — becomes the title of a generated
|
|
19
|
+
* `toPlaywright()` test. See `@humanjs/playwright`'s `HumanRecordOptions.name`.
|
|
20
|
+
*/
|
|
21
|
+
readonly name?: string;
|
|
17
22
|
/**
|
|
18
23
|
* Quality preset. Picks both source viewport and ffmpeg encoding settings.
|
|
19
24
|
* Defaults to `'high'` (visually-lossless 1080p).
|
|
@@ -24,6 +29,14 @@ interface RecordOptions extends CreateHumanOptions {
|
|
|
24
29
|
* - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation
|
|
25
30
|
*/
|
|
26
31
|
readonly quality?: RecordingQuality;
|
|
32
|
+
/**
|
|
33
|
+
* Capture actual typed/pasted text into the timeline, so `toHumanJS()` /
|
|
34
|
+
* `toPlaywright()` exports include the values. Defaults to `true`; password
|
|
35
|
+
* fields are always masked. Set `false` to record no input values (exports
|
|
36
|
+
* emit empty-string placeholders). See `@humanjs/playwright`'s
|
|
37
|
+
* `HumanRecordOptions.captureInputs`.
|
|
38
|
+
*/
|
|
39
|
+
readonly captureInputs?: boolean;
|
|
27
40
|
/** Optional URL to navigate to before the callback runs. */
|
|
28
41
|
readonly url?: string;
|
|
29
42
|
/**
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,9 @@ async function record(optionsOrFn, maybeFn) {
|
|
|
17
17
|
const [options, fn] = typeof optionsOrFn === "function" ? [{}, optionsOrFn] : [optionsOrFn, maybeFn];
|
|
18
18
|
const {
|
|
19
19
|
output,
|
|
20
|
+
name,
|
|
20
21
|
quality,
|
|
22
|
+
captureInputs,
|
|
21
23
|
url,
|
|
22
24
|
viewport,
|
|
23
25
|
headless,
|
|
@@ -47,7 +49,7 @@ async function record(optionsOrFn, maybeFn) {
|
|
|
47
49
|
if (url) await page.goto(url);
|
|
48
50
|
const human = await createHuman(page, createHumanOptions);
|
|
49
51
|
const recording = await human.record(
|
|
50
|
-
{ video: wantsCapture, quality: resolvedQuality },
|
|
52
|
+
{ name, video: wantsCapture, quality: resolvedQuality, captureInputs },
|
|
51
53
|
() => fn(human, page)
|
|
52
54
|
);
|
|
53
55
|
if (wantsCapture && output) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/record/index.ts"],"names":["browser","context","page"],"mappings":";;;;AAoBA,IAAM,uBAAA,GAA0E;AAAA;AAAA,EAE9E,IAAA,EAAM,EAAE,QAAA,EAAU,EAAE,OAAO,IAAA,EAAM,MAAA,EAAQ,KAAI,EAAE;AAAA;AAAA,EAE/C,QAAA,EAAU,EAAE,QAAA,EAAU,EAAE,OAAO,IAAA,EAAM,MAAA,EAAQ,MAAK,EAAE;AAAA;AAAA;AAAA,EAGpD,IAAA,EAAM,EAAE,QAAA,EAAU,EAAE,OAAO,IAAA,EAAM,MAAA,EAAQ,MAAK,EAAE;AAAA;AAAA,EAEhD,QAAA,EAAU,EAAE,QAAA,EAAU,EAAE,OAAO,IAAA,EAAM,MAAA,EAAQ,MAAK;AACpD,CAAA;AAmIA,eAAsB,MAAA,CACpB,aACA,OAAA,EACoB;AACpB,EAAA,MAAM,CAAC,OAAA,EAAS,EAAE,CAAA,GAChB,OAAO,WAAA,KAAgB,UAAA,GACnB,CAAC,EAAC,EAAoB,WAAW,CAAA,GACjC,CAAC,aAAa,OAAyB,CAAA;AAE7C,EAAA,MAAM;AAAA,IACJ,MAAA;AAAA,IACA,OAAA;AAAA,IACA,GAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA,EAAS,cAAA;AAAA,IACT,WAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAA;AAAA,IACA,GAAG;AAAA,GACL,GAAI,OAAA;AAEJ,EAAA,MAAM,kBAAoC,OAAA,IAAW,MAAA;AACrD,EAAA,MAAM,aAAA,GAAgB,wBAAwB,eAAe,CAAA;AAC7D,EAAA,MAAM,gBAAA,GAAmB,QAAA,IAAY,cAAA,EAAgB,QAAA,IAAY,aAAA,CAAc,QAAA;AAC/E,EAAA,MAAM,eAAe,MAAA,KAAW,MAAA;AAEhC,EAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,MAAM,uBAAA,CAAwB;AAAA,IACtD,MAAA;AAAA,IACA,WAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAU,QAAA,IAAY,KAAA;AAAA,IACtB,QAAA,EAAU,gBAAA;AAAA,IACV,MAAA;AAAA,IACA,OAAA,EAAS,cAAA;AAAA,IACT;AAAA,GACD,CAAA;AAED,EAAA,IAAI;AACF,IAAA,IAAI,GAAA,EAAK,MAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAE5B,IAAA,MAAM,KAAA,GAAQ,MAAM,WAAA,CAAY,IAAA,EAAM,kBAAkB,CAAA;AAIxD,IAAA,MAAM,SAAA,GAAY,MAAM,KAAA,CAAM,MAAA;AAAA,MAAO,EAAE,KAAA,EAAO,YAAA,EAAc,OAAA,EAAS,eAAA,EAAgB;AAAA,MAAG,MACtF,EAAA,CAAG,KAAA,EAAO,IAAI;AAAA,KAChB;AAEA,IAAA,IAAI,gBAAgB,MAAA,EAAQ;AAI1B,MAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,MAAM,CAAA,CAAE,WAAA,EAAY;AACxC,MAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,QAAA,MAAM,SAAA,CAAU,MAAM,MAAM,CAAA;AAAA,MAC9B,CAAA,MAAO;AACL,QAAA,MAAM,UAAU,OAAA,CAAQ,MAAA,EAAQ,EAAE,OAAA,EAAS,iBAAiB,CAAA;AAAA,MAC9D;AAAA,IACF;AAEA,IAAA,OAAO,SAAA;AAAA,EACT,CAAA,SAAE;AACA,IAAA,MAAM,OAAA,EAAQ;AAAA,EAChB;AACF;AAoBA,eAAe,wBACb,IAAA,EACuD;AACvD,EAAA,MAAM,gBAAgB,OAAO,IAAA,CAAK,MAAA,KAAW,QAAA,GAAW,KAAK,MAAA,GAAS,MAAA;AACtE,EAAA,MAAM,aAAA,GAAgB,OAAO,GAAA,KAAuC;AAClE,IAAA,IAAI,KAAK,MAAA,KAAW,KAAA,EAAO,MAAM,kBAAA,CAAmB,KAAK,aAAa,CAAA;AAAA,EACxE,CAAA;AAKA,EAAA,IAAI,KAAK,MAAA,EAAQ;AACf,IAAA,MAAMA,QAAAA,GAAU,MAAM,QAAA,CAAS,cAAA,CAAe,KAAK,MAAM,CAAA;AACzD,IAAA,MAAMC,QAAAA,GAAUD,QAAAA,CAAQ,QAAA,EAAS,CAAE,CAAC,CAAA,IAAM,MAAMA,QAAAA,CAAQ,UAAA,CAAW,EAAE,GAAG,IAAA,CAAK,SAAS,CAAA;AACtF,IAAA,MAAM,cAAcC,QAAO,CAAA;AAC3B,IAAA,MAAMC,KAAAA,GAAOD,SAAQ,KAAA,EAAM,CAAE,CAAC,CAAA,IAAM,MAAMA,SAAQ,OAAA,EAAQ;AAC1D,IAAA,OAAO,EAAE,IAAA,EAAAC,KAAAA,EAAM,OAAA,EAAS,YAAY,MAAA,EAAU;AAAA,EAChD;AAIA,EAAA,IAAI,KAAK,WAAA,EAAa;AACpB,IAAA,MAAMD,QAAAA,GAAU,MAAM,QAAA,CAAS,uBAAA,CAAwB,KAAK,WAAA,EAAa;AAAA,MACvE,GAAG,IAAA,CAAK,MAAA;AAAA,MACR,GAAG,IAAA,CAAK,OAAA;AAAA,MACR,OAAA,EAAS,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,MAAA,EAAQ,OAAA;AAAA,MACtC,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,UAAU,IAAA,CAAK;AAAA,KAChB,CAAA;AACD,IAAA,MAAM,cAAcA,QAAO,CAAA;AAC3B,IAAA,MAAMC,KAAAA,GAAOD,SAAQ,KAAA,EAAM,CAAE,CAAC,CAAA,IAAM,MAAMA,SAAQ,OAAA,EAAQ;AAC1D,IAAA,OAAO;AAAA,MACL,IAAA,EAAAC,KAAAA;AAAA,MACA,SAAS,YAAY;AACnB,QAAA,MAAMD,QAAAA,CAAQ,KAAA,EAAM,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AAAA,MAC7C;AAAA,KACF;AAAA,EACF;AAGA,EAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,MAAA,CAAO;AAAA,IACpC,GAAG,IAAA,CAAK,MAAA;AAAA,IACR,OAAA,EAAS,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,MAAA,EAAQ,OAAA;AAAA,IACtC,UAAU,IAAA,CAAK;AAAA,GAChB,CAAA;AACD,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,UAAA,CAAW,EAAE,GAAG,IAAA,CAAK,OAAA,EAAS,QAAA,EAAU,IAAA,CAAK,QAAA,EAAU,CAAA;AACrF,EAAA,MAAM,cAAc,OAAO,CAAA;AAC3B,EAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,OAAA,EAAQ;AACnC,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,SAAS,YAAY;AACnB,MAAA,MAAM,OAAA,CAAQ,KAAA,EAAM,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AAC3C,MAAA,MAAM,OAAA,CAAQ,KAAA,EAAM,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AAAA,IAC7C;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContext,\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /**\n * Viewport dimensions. Overrides the quality preset's viewport. Applies to\n * the default and persistent (`userDataDir`) modes; ignored when attaching\n * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).\n */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Record in a **persistent profile** at this directory so logins and\n * cookies survive across runs — sign in once (in a headed run), and later\n * recordings start authenticated. Uses `launchPersistentContext` under the\n * hood (a single context); `headless`, `launch`, `channel`, and `viewport`\n * still apply. Starts empty the first time.\n */\n readonly userDataDir?: string;\n /**\n * Record by **attaching to a browser you already launched** over CDP (e.g.\n * `\"http://localhost:9222\"`). Reuses that browser's existing context — your\n * real logins, tabs, extensions. Start the browser yourself with\n * `--remote-debugging-port`. HumanJS never closes a browser it attached to;\n * it only borrows it. Takes precedence over `userDataDir`.\n *\n * `launch` / `headless` / `channel` / `viewport` don't apply here — you're\n * borrowing a window HumanJS doesn't own, so the browser's real window size\n * wins. Forcing a `viewport` would only *emulate* one and letterbox the\n * capture. For a fixed recording resolution, launch the browser yourself\n * with `--window-size=1920,1080`, or use the default / persistent modes.\n */\n readonly cdpUrl?: string;\n /**\n * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that\n * installed browser's binary instead of bundled Chromium (applies to the\n * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse\n * your existing profile — pair it with `userDataDir` (persistent) or\n * `cdpUrl` (attach) for real logins.\n */\n readonly channel?: string;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches (or attaches to) a browser, opens a\n * page, creates a humanized session, runs `fn`, and returns a\n * {@link Recording} you can export to video, JSON timeline, or read in-memory.\n *\n * Browser source (default → ephemeral fresh profile):\n * - `userDataDir` — a persistent profile that keeps logins across runs.\n * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);\n * never closed on finish, only released.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Stay signed in across runs (persistent profile)\n * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {\n * await human.goto('https://app.example.com/dashboard');\n * });\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n quality,\n url,\n viewport,\n headless,\n launch,\n context: contextOptions,\n userDataDir,\n cdpUrl,\n channel,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const { page, dispose } = await acquireRecordingContext({\n cdpUrl,\n userDataDir,\n channel,\n headless: headless ?? false,\n viewport: resolvedViewport,\n launch,\n context: contextOptions,\n cursor,\n });\n\n try {\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record({ video: wantsCapture, quality: resolvedQuality }, () =>\n fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await dispose();\n }\n}\n\n/** Internal options for {@link acquireRecordingContext}. */\ninterface AcquireOptions {\n readonly cdpUrl?: string;\n readonly userDataDir?: string;\n readonly channel?: string;\n readonly headless: boolean;\n readonly viewport: { readonly width: number; readonly height: number };\n readonly launch?: LaunchOptions;\n readonly context?: BrowserContextOptions;\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/**\n * Resolves the browser source — CDP attach > persistent profile > ephemeral\n * — and returns the page to drive plus a `dispose` that tears down only what\n * we created. A CDP-attached browser is borrowed: `dispose` leaves it (and\n * its context) untouched so we never close the caller's real browser.\n */\nasync function acquireRecordingContext(\n opts: AcquireOptions,\n): Promise<{ page: Page; dispose: () => Promise<void> }> {\n const cursorOptions = typeof opts.cursor === 'object' ? opts.cursor : undefined;\n const installCursor = async (ctx: BrowserContext): Promise<void> => {\n if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);\n };\n\n // Attach to a browser the caller launched. Reuse its existing context so we\n // record their real session; only make a fresh one if there's none. Never\n // close it on dispose — it's borrowed.\n if (opts.cdpUrl) {\n const browser = await chromium.connectOverCDP(opts.cdpUrl);\n const context = browser.contexts()[0] ?? (await browser.newContext({ ...opts.context }));\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return { page, dispose: async () => undefined };\n }\n\n // Persistent profile — the context owns its browser, so closing it on\n // dispose tears everything down.\n if (opts.userDataDir) {\n const context = await chromium.launchPersistentContext(opts.userDataDir, {\n ...opts.launch,\n ...opts.context,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n viewport: opts.viewport,\n });\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n },\n };\n }\n\n // Ephemeral (default) — a fresh throwaway browser + context.\n const browser = await chromium.launch({\n ...opts.launch,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n });\n const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });\n await installCursor(context);\n const page = await context.newPage();\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/record/index.ts"],"names":["browser","context","page"],"mappings":"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","file":"index.js","sourcesContent":["import { extname } from 'node:path';\nimport {\n type BrowserContext,\n type BrowserContextOptions,\n type CreateHumanOptions,\n chromium,\n createHuman,\n type Human,\n type InstallMouseHelperOptions,\n installMouseHelper,\n type LaunchOptions,\n type Page,\n type Recording,\n type RecordingQuality,\n} from '@humanjs/playwright';\n\ninterface QualityBrowserPreset {\n readonly viewport: { readonly width: number; readonly height: number };\n}\n\nconst QUALITY_BROWSER_PRESETS: Record<RecordingQuality, QualityBrowserPreset> = {\n // 720p source — small files, fast encoding, good for iteration.\n fast: { viewport: { width: 1280, height: 720 } },\n // 1080p source — balanced default for tests and dashboards.\n standard: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, visually-lossless encoding (slow preset + animation tune).\n // Recommended for marketing / portfolio output.\n high: { viewport: { width: 1920, height: 1080 } },\n // 1080p source, archival-quality encoding (very slow, low CRF).\n lossless: { viewport: { width: 1920, height: 1080 } },\n};\n\n/**\n * Options for {@link record}. Most fields are passed straight through to\n * Playwright's `chromium.launch()` and `browser.newContext()` so a one-call\n * recording can configure anything a full Playwright setup could.\n */\nexport interface RecordOptions extends CreateHumanOptions {\n /**\n * Output path. Extension determines format — `.mp4` / `.webm` for video,\n * or `.gif` for an animated GIF (palette-optimized, defaults to 15fps).\n * Omit to skip capture entirely; the returned {@link Recording} still has\n * the structured action timeline via `.toTimeline()` / `.timeline`.\n */\n readonly output?: string;\n /**\n * Optional label for the recording — becomes the title of a generated\n * `toPlaywright()` test. See `@humanjs/playwright`'s `HumanRecordOptions.name`.\n */\n readonly name?: string;\n /**\n * Quality preset. Picks both source viewport and ffmpeg encoding settings.\n * Defaults to `'high'` (visually-lossless 1080p).\n *\n * - `'fast'`: 720p, CRF 23, preset fast\n * - `'standard'`: 1080p, CRF 20, preset fast\n * - `'high'` (default): 1080p, CRF 18, preset slow, tune animation\n * - `'lossless'`: 1080p, CRF 12, preset veryslow, tune animation\n */\n readonly quality?: RecordingQuality;\n /**\n * Capture actual typed/pasted text into the timeline, so `toHumanJS()` /\n * `toPlaywright()` exports include the values. Defaults to `true`; password\n * fields are always masked. Set `false` to record no input values (exports\n * emit empty-string placeholders). See `@humanjs/playwright`'s\n * `HumanRecordOptions.captureInputs`.\n */\n readonly captureInputs?: boolean;\n /** Optional URL to navigate to before the callback runs. */\n readonly url?: string;\n /**\n * Viewport dimensions. Overrides the quality preset's viewport. Applies to\n * the default and persistent (`userDataDir`) modes; ignored when attaching\n * over `cdpUrl`, where the real browser window's size wins (see `cdpUrl`).\n */\n readonly viewport?: { readonly width: number; readonly height: number };\n /** Run headless. Defaults to `false` so users can watch the recording happen. */\n readonly headless?: boolean;\n /** Forwarded to `chromium.launch()` (alongside `headless`). */\n readonly launch?: LaunchOptions;\n /** Forwarded to `browser.newContext()` (alongside `viewport`). */\n readonly context?: BrowserContextOptions;\n /**\n * Record in a **persistent profile** at this directory so logins and\n * cookies survive across runs — sign in once (in a headed run), and later\n * recordings start authenticated. Uses `launchPersistentContext` under the\n * hood (a single context); `headless`, `launch`, `channel`, and `viewport`\n * still apply. Starts empty the first time.\n */\n readonly userDataDir?: string;\n /**\n * Record by **attaching to a browser you already launched** over CDP (e.g.\n * `\"http://localhost:9222\"`). Reuses that browser's existing context — your\n * real logins, tabs, extensions. Start the browser yourself with\n * `--remote-debugging-port`. HumanJS never closes a browser it attached to;\n * it only borrows it. Takes precedence over `userDataDir`.\n *\n * `launch` / `headless` / `channel` / `viewport` don't apply here — you're\n * borrowing a window HumanJS doesn't own, so the browser's real window size\n * wins. Forcing a `viewport` would only *emulate* one and letterbox the\n * capture. For a fixed recording resolution, launch the browser yourself\n * with `--window-size=1920,1080`, or use the default / persistent modes.\n */\n readonly cdpUrl?: string;\n /**\n * Playwright browser channel — e.g. `'chrome'`, `'msedge'`. Launches that\n * installed browser's binary instead of bundled Chromium (applies to the\n * default and `userDataDir` modes). NOTE: a channel alone does NOT reuse\n * your existing profile — pair it with `userDataDir` (persistent) or\n * `cdpUrl` (attach) for real logins.\n */\n readonly channel?: string;\n /**\n * Install the HumanJS visible cursor overlay so recorded videos show\n * mouse motion — Playwright's synthetic mouse doesn't render a cursor\n * by itself, so without this the recording would look like text and\n * UI changing on their own.\n *\n * - `true` (default): install with default styling (HumanJS amber, 22px)\n * - `false`: don't install — the user will install their own, or the\n * recording intentionally has no visible cursor\n * - {@link InstallMouseHelperOptions}: install with custom color / size /\n * click-ripple / halo settings\n *\n * The helper is installed on the context via `addInitScript` + a\n * DOMContentLoaded listener, so it persists across `page.setContent()`\n * and navigation inside the callback.\n */\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/** The callback shape both overloads of {@link record} accept. */\nexport type RecordCallback = (human: Human, page: Page) => Promise<void>;\n\n/**\n * One-call session recording. Launches (or attaches to) a browser, opens a\n * page, creates a humanized session, runs `fn`, and returns a\n * {@link Recording} you can export to video, JSON timeline, or read in-memory.\n *\n * Browser source (default → ephemeral fresh profile):\n * - `userDataDir` — a persistent profile that keeps logins across runs.\n * - `cdpUrl` — attach to a browser you launched yourself (real logins/tabs);\n * never closed on finish, only released.\n *\n * If `options.output` is set, the output file is written to that path before\n * `record()` resolves — extension dispatches to `toVideo` (`.mp4` / `.webm`)\n * or `toGif` (`.gif`). The returned Recording is still useful for additional\n * exports via `toVideo` / `toGif` / `toTimeline` (all repeatable). If\n * `output` is omitted, frame capture is skipped entirely (no encoding\n * overhead) and only the timeline is captured.\n *\n * @example\n * ```ts\n * // Video + timeline\n * const rec = await record({ output: 'demo.mp4' }, async (human) => {\n * await human.click('#login');\n * });\n * await rec.toTimeline('demo.json');\n * console.log(rec.durationMs, rec.timeline.events.length);\n * ```\n *\n * @example\n * ```ts\n * // Stay signed in across runs (persistent profile)\n * await record({ output: 'dashboard.mp4', userDataDir: './.humanjs-profile' }, async (human) => {\n * await human.goto('https://app.example.com/dashboard');\n * });\n * ```\n *\n * For multi-page flows or recording a slice of a larger session, use\n * `human.record()` from `@humanjs/playwright` directly.\n */\nexport function record(fn: RecordCallback): Promise<Recording>;\nexport function record(options: RecordOptions, fn: RecordCallback): Promise<Recording>;\nexport async function record(\n optionsOrFn: RecordCallback | RecordOptions,\n maybeFn?: RecordCallback,\n): Promise<Recording> {\n const [options, fn] =\n typeof optionsOrFn === 'function'\n ? [{} as RecordOptions, optionsOrFn]\n : [optionsOrFn, maybeFn as RecordCallback];\n\n const {\n output,\n name,\n quality,\n captureInputs,\n url,\n viewport,\n headless,\n launch,\n context: contextOptions,\n userDataDir,\n cdpUrl,\n channel,\n cursor,\n ...createHumanOptions\n } = options;\n\n const resolvedQuality: RecordingQuality = quality ?? 'high';\n const browserPreset = QUALITY_BROWSER_PRESETS[resolvedQuality];\n const resolvedViewport = viewport ?? contextOptions?.viewport ?? browserPreset.viewport;\n const wantsCapture = output !== undefined;\n\n const { page, dispose } = await acquireRecordingContext({\n cdpUrl,\n userDataDir,\n channel,\n headless: headless ?? false,\n viewport: resolvedViewport,\n launch,\n context: contextOptions,\n cursor,\n });\n\n try {\n if (url) await page.goto(url);\n\n const human = await createHuman(page, createHumanOptions);\n\n // Capture runs only when the caller asked for an output file — saves\n // the screenshot + disk-write overhead for timeline-only recordings.\n const recording = await human.record(\n { name, video: wantsCapture, quality: resolvedQuality, captureInputs },\n () => fn(human, page),\n );\n\n if (wantsCapture && output) {\n // Dispatch by extension: .gif goes through the GIF exporter (which\n // ignores `quality` — GIF doesn't have a CRF concept), everything\n // else flows into the mp4/webm encoder with the chosen preset.\n const ext = extname(output).toLowerCase();\n if (ext === '.gif') {\n await recording.toGif(output);\n } else {\n await recording.toVideo(output, { quality: resolvedQuality });\n }\n }\n\n return recording;\n } finally {\n await dispose();\n }\n}\n\n/** Internal options for {@link acquireRecordingContext}. */\ninterface AcquireOptions {\n readonly cdpUrl?: string;\n readonly userDataDir?: string;\n readonly channel?: string;\n readonly headless: boolean;\n readonly viewport: { readonly width: number; readonly height: number };\n readonly launch?: LaunchOptions;\n readonly context?: BrowserContextOptions;\n readonly cursor?: boolean | InstallMouseHelperOptions;\n}\n\n/**\n * Resolves the browser source — CDP attach > persistent profile > ephemeral\n * — and returns the page to drive plus a `dispose` that tears down only what\n * we created. A CDP-attached browser is borrowed: `dispose` leaves it (and\n * its context) untouched so we never close the caller's real browser.\n */\nasync function acquireRecordingContext(\n opts: AcquireOptions,\n): Promise<{ page: Page; dispose: () => Promise<void> }> {\n const cursorOptions = typeof opts.cursor === 'object' ? opts.cursor : undefined;\n const installCursor = async (ctx: BrowserContext): Promise<void> => {\n if (opts.cursor !== false) await installMouseHelper(ctx, cursorOptions);\n };\n\n // Attach to a browser the caller launched. Reuse its existing context so we\n // record their real session; only make a fresh one if there's none. Never\n // close it on dispose — it's borrowed.\n if (opts.cdpUrl) {\n const browser = await chromium.connectOverCDP(opts.cdpUrl);\n const context = browser.contexts()[0] ?? (await browser.newContext({ ...opts.context }));\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return { page, dispose: async () => undefined };\n }\n\n // Persistent profile — the context owns its browser, so closing it on\n // dispose tears everything down.\n if (opts.userDataDir) {\n const context = await chromium.launchPersistentContext(opts.userDataDir, {\n ...opts.launch,\n ...opts.context,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n viewport: opts.viewport,\n });\n await installCursor(context);\n const page = context.pages()[0] ?? (await context.newPage());\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n },\n };\n }\n\n // Ephemeral (default) — a fresh throwaway browser + context.\n const browser = await chromium.launch({\n ...opts.launch,\n channel: opts.channel ?? opts.launch?.channel,\n headless: opts.headless,\n });\n const context = await browser.newContext({ ...opts.context, viewport: opts.viewport });\n await installCursor(context);\n const page = await context.newPage();\n return {\n page,\n dispose: async () => {\n await context.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanjs/recorder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "One-call session recording for HumanJS — capture a humanized Playwright session as mp4, webm, gif, or a structured JSON timeline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"humanjs",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"node": ">=20"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@humanjs/playwright": "0.
|
|
52
|
+
"@humanjs/playwright": "0.7.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"playwright": ">=1.40.0"
|