@humanjs/playwright 0.2.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/README.md +231 -4
- package/dist/index.cjs +1013 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +478 -4
- package/dist/index.d.ts +478 -4
- package/dist/index.js +978 -39
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -20,15 +20,242 @@ const browser = await chromium.launch();
|
|
|
20
20
|
const page = await browser.newPage();
|
|
21
21
|
|
|
22
22
|
const human = await createHuman(page, {
|
|
23
|
-
personality: 'careful',
|
|
24
|
-
seed: 'session-42',
|
|
25
|
-
speed: 'human',
|
|
23
|
+
personality: 'careful', // careful | fast | distracted | precise
|
|
24
|
+
seed: 'session-42', // deterministic for tests
|
|
25
|
+
speed: 'human', // human | fast | instant
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
await human.goto('https://example.com');
|
|
29
|
+
|
|
30
|
+
// Mouse: real Bezier path, velocity profile, pre-click hover dwell.
|
|
31
|
+
await human.click('button:has-text("Sign in")');
|
|
32
|
+
|
|
33
|
+
// Keyboard: per-key rhythm, optional QWERTY typos, Backspace recovery,
|
|
34
|
+
// occasional mid-word think pauses. The typed string is *not* echoed to
|
|
35
|
+
// plugin params — `params.length` only, by design.
|
|
36
|
+
await human.type('input[name="email"]', 'gonzalo@example.com');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Speed modes
|
|
40
|
+
|
|
41
|
+
- `'human'` (default) — full humanization on every action.
|
|
42
|
+
- `'fast'` — humanized but accelerated.
|
|
43
|
+
- `'instant'` — bypass humanization entirely; uses Playwright's native methods. Per-key events still fire for `type()`. Right for CI.
|
|
44
|
+
|
|
45
|
+
### Determinism
|
|
46
|
+
|
|
47
|
+
Pass a `seed` and every random decision (path curvature, typo placement, keystroke jitter) becomes reproducible. Same seed + same personality + same value = same keystrokes.
|
|
48
|
+
|
|
49
|
+
### Reading
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
await human.read('p.welcome');
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`human.read()` dwells like a real reader — pause-time scaled by the target's word count and the personality's reading WPM (with personality-controlled jitter).
|
|
56
|
+
|
|
57
|
+
**Target options:**
|
|
58
|
+
|
|
59
|
+
- `string` — Playwright-compatible selector
|
|
60
|
+
- `Locator` — a pre-built Locator
|
|
61
|
+
- `{ text: '...' }` — literal text, no DOM lookup
|
|
62
|
+
- `{ words: 42 }` — pre-counted; skips text extraction entirely
|
|
63
|
+
|
|
64
|
+
**Reading kinds** scale the dwell on top of `personality.reading.wpm`:
|
|
65
|
+
|
|
66
|
+
- `'prose'` (1.0×) — default for non-code targets
|
|
67
|
+
- `'code'` (0.4×) — slower; auto-detected when the target is a `<pre>` or `<code>` element
|
|
68
|
+
- `'scan'` (1.8×) — explicit skim mode
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
await human.read('.article-body'); // prose, default
|
|
72
|
+
await human.read('pre.snippet'); // 'code' auto-detected from <pre>
|
|
73
|
+
await human.read('ul.changelog', { kind: 'scan' }); // explicit skim
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Explicit `kind` always wins over auto-detection.
|
|
77
|
+
|
|
78
|
+
**Eye-scan cursor motion** runs during the dwell by default:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
await human.read('article'); // motion: on
|
|
82
|
+
await human.read('article', { withMotion: false }); // motion: off
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The cursor walks a humanized L→R sweep through every line of rendered text and emits a small return-saccade between lines — same `mousemove` events a real reader would dispatch (so reading-time tooltip / hover handlers fire). Pass `{ withMotion: false }` when you only care about the temporal pattern (typical AI-agent use case).
|
|
86
|
+
|
|
87
|
+
For demos and screen recordings, pair it with `installMouseHelper(page)` to render a visible cursor that follows the synthetic motion:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createHuman, installMouseHelper } from '@humanjs/playwright';
|
|
91
|
+
|
|
92
|
+
const page = await context.newPage();
|
|
93
|
+
await page.goto('https://example.com/article');
|
|
94
|
+
await installMouseHelper(page);
|
|
95
|
+
|
|
96
|
+
const human = await createHuman(page, { personality: 'careful' });
|
|
97
|
+
await human.read('article');
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Returns** a `ReadResult`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const { words, durationMs, kind } = await human.read('main');
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Useful for test assertions or surfacing reading metadata in a UI.
|
|
107
|
+
|
|
108
|
+
**Privacy**: the read text is never echoed to plugin params. `read` actions surface only `{ target, kind }` plus inert length metadata — the content itself stays out of telemetry by design, same posture as `human.type()`.
|
|
109
|
+
|
|
110
|
+
### Scrolling
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
await human.scroll(); // ~one viewport down, humanized
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`human.scroll()` produces multi-segment scroll motion with a bell-curve velocity profile (slow start, fast middle, slow end), optional mid-scroll micro-pauses, and — for the `distracted` personality — occasional overshoot + correction. Page scrolls dispatch real `wheel` events; container scrolls advance the element's scroll position directly (more reliable inside nested overflow containers).
|
|
117
|
+
|
|
118
|
+
**Target options:**
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
await human.scroll(); // 'natural' — ~one viewport
|
|
122
|
+
await human.scroll('top'); // to the top
|
|
123
|
+
await human.scroll('end'); // to the bottom
|
|
124
|
+
await human.scroll({ by: 800 }); // relative pixel delta (negative = up)
|
|
125
|
+
await human.scroll({ to: 1500 }); // absolute scroll position on the chosen axis
|
|
126
|
+
await human.scroll('#pricing'); // by selector — scroll until in view
|
|
127
|
+
await human.scroll(locator); // by Locator
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Element-target alignment** matches native `scrollIntoView`:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
await human.scroll('#hero', { block: 'center' }); // 'start' | 'center' | 'end' | 'nearest'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`'nearest'` is a useful default for "make sure this element is visible without moving more than necessary" — it stays put if the element is already fully in view, otherwise scrolls to the closest edge.
|
|
137
|
+
|
|
138
|
+
**Scroll inside a scrollable container**, not the page:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
await human.scroll('end', { within: '#messages' }); // chat thread to latest
|
|
142
|
+
await human.scroll('#newest-item', { within: '.feed', block: 'end' });
|
|
143
|
+
await human.scroll({ by: -200 }, { within: modalBody }); // scroll up inside a modal
|
|
29
144
|
```
|
|
30
145
|
|
|
31
|
-
|
|
146
|
+
Every target shape (`'natural'`, `'top'`, `'end'`, selectors, `{ by }`, `{ to }`) applies relative to the container. In humanized mode the cursor parks over the container's center (so an `installMouseHelper` overlay reads as "human hand on the wheel") and each segment advances the container's `scrollLeft` / `scrollTop` directly — more reliable than wheel events inside nested overflow containers. In `'instant'` mode the container's scroll position is set with a single `scrollTo` call.
|
|
147
|
+
|
|
148
|
+
**Horizontal scroll** via `axis: 'x'` — same target shapes apply to the X axis:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await human.scroll('end', { axis: 'x' }); // to the right edge
|
|
152
|
+
await human.scroll({ by: 400 }, { axis: 'x' }); // 400px right
|
|
153
|
+
await human.scroll('#card-5', { axis: 'x', block: 'center' }); // carousel to a card
|
|
154
|
+
await human.scroll('end', { within: '#kanban', axis: 'x' }); // kanban board to the right end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Defaults to `'y'`. Combine with `within` for horizontal scrolling inside a container (carousels, kanban boards, sideways galleries).
|
|
158
|
+
|
|
159
|
+
**Force overshoot** even when the personality wouldn't choose one — useful for demos and screen recordings where the humanization signal needs to read clearly:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
await human.scroll('#footer', { overshoot: true });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Returns** a `ScrollResult`:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const { from, to, distance, durationMs } = await human.scroll('end');
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
In `speed: 'instant'`, the page jumps directly via `window.scrollTo` — no wheel events — but the action still fires for observability.
|
|
172
|
+
|
|
173
|
+
See [humanjs.dev](https://humanjs.dev) for the full feature set and personality reference.
|
|
174
|
+
|
|
175
|
+
### Recording
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { chromium, createHuman, installMouseHelper } from '@humanjs/playwright';
|
|
179
|
+
|
|
180
|
+
const browser = await chromium.launch({ headless: false });
|
|
181
|
+
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
|
|
182
|
+
const page = await context.newPage();
|
|
183
|
+
|
|
184
|
+
// Visible cursor overlay so the recorded video shows mouse motion.
|
|
185
|
+
await installMouseHelper(context);
|
|
186
|
+
|
|
187
|
+
const human = await createHuman(page);
|
|
188
|
+
|
|
189
|
+
const rec = await human.record(async () => {
|
|
190
|
+
await human.click('#login');
|
|
191
|
+
await human.type('#email', 'demo@humanjs.dev');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await rec.toVideo('demo.mp4');
|
|
195
|
+
await rec.toTimeline('demo.json');
|
|
196
|
+
await browser.close();
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`human.record(cb)` polls `page.screenshot()` at the target FPS, writes each frame to a temp directory, then assembles them via ffmpeg when you call an exporter:
|
|
200
|
+
|
|
201
|
+
- `rec.toVideo(path)` — `.mp4` (H.264 / yuv420p) or `.webm` (VP9)
|
|
202
|
+
- `rec.toGif(path, { fps?, width? })` — palette-optimized animated GIF (`palettegen` + `paletteuse`, Bayer dither). Defaults to 15 fps, source viewport size.
|
|
203
|
+
|
|
204
|
+
Both exporters are **repeatable and interleavable** — they read the captured frames, they don't consume them. Want an mp4 for the landing page *and* a GIF for the README from the same recording? Just call both:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const rec = await human.record(fn);
|
|
208
|
+
await rec.toVideo('demo.mp4');
|
|
209
|
+
await rec.toGif('demo.gif', { width: 720 });
|
|
210
|
+
// No explicit cleanup needed for one-shot scripts — see below.
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Captured frames live in a temp directory under `os.tmpdir()`. Cleanup happens automatically at process exit (a single `process.on('exit')` handler sweeps any un-disposed frame dirs), so casual scripts don't have to think about it. For long-running services, batch jobs, or anywhere you want predictable disk usage, release them proactively:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
await rec.dispose(); // explicit, idempotent
|
|
217
|
+
// or with TS ≥ 5.2 / Node ≥ 20.4:
|
|
218
|
+
await using rec = await human.record(fn); // auto-disposes at scope exit
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The same `Recording` exposes a **structured action timeline** of everything that happened during the callback:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
await rec.toTimeline('session.json'); // → JSON on disk
|
|
225
|
+
const timeline = rec.timeline; // → in-memory object
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The shape (`Timeline` with `personality`, `seed`, `speed`, `durationMs`, and an `events` array of `{ type, params, tMs, durationMs }`) is intended for observability pipelines, replay infrastructure, analytics, and debugger UIs. `toTimeline()` doesn't touch the browser context — call it before or after `toVideo()`, multiple times, in any order.
|
|
229
|
+
|
|
230
|
+
**Quality presets** trade off file size, encoding time, and visual fidelity. Defaults to `'high'`:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
await rec.toVideo('demo.mp4', { quality: 'high' });
|
|
234
|
+
// 'fast' — JPEG q=85, CRF 23, preset fast (iteration)
|
|
235
|
+
// 'standard' — JPEG q=90, CRF 20, preset fast (balanced)
|
|
236
|
+
// 'high' — JPEG q=95, CRF 18, preset slow, animation (DEFAULT)
|
|
237
|
+
// 'lossless' — PNG capture, CRF 12, preset veryslow (archival)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Individual ffmpeg knobs (`crf`, `preset`, `tune`) can override the preset for fine-grained control.
|
|
241
|
+
|
|
242
|
+
**Timeline-only mode** — skip the capture overhead entirely when you only need the action timeline:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const rec = await human.record({ video: false }, async () => {
|
|
246
|
+
await human.click('#login');
|
|
247
|
+
});
|
|
248
|
+
await rec.toTimeline('session.json'); // works
|
|
249
|
+
// rec.toVideo('demo.mp4') // throws with a clear message
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Lifecycle notes**:
|
|
253
|
+
|
|
254
|
+
- Each session can produce **one** recording. `human.record()` throws if called twice on the same session — open a new context (and a new human) to record a separate clip.
|
|
255
|
+
- `Recording.toVideo()` / `Recording.toGif()` are repeatable and interleavable. Frames live until `rec.dispose()` (or `await using` goes out of scope, or the process exits — a sweep-on-exit handler covers forgotten disposes).
|
|
256
|
+
- For a one-call API that owns the entire lifecycle (launch → record → close), use [`@humanjs/recorder`](../recorder)'s `record(options, fn)` instead.
|
|
257
|
+
|
|
258
|
+
Every recording is a regular plugin action — `beforeAction` and `afterAction` observe `{ type: 'record' }` exactly like `'click'` or `'scroll'`.
|
|
32
259
|
|
|
33
260
|
## License
|
|
34
261
|
|