@humanjs/playwright 0.5.0 → 0.7.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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # @humanjs/playwright
2
2
 
3
+ <p>
4
+ <a href="https://www.npmjs.com/package/@humanjs/playwright"><img alt="npm" src="https://img.shields.io/npm/v/@humanjs/playwright"></a>
5
+ <a href="https://www.npmjs.com/package/@humanjs/playwright"><img alt="downloads" src="https://img.shields.io/npm/dt/@humanjs/playwright"></a>
6
+ <a href="https://github.com/totigm/humanjs"><img alt="GitHub" src="https://img.shields.io/badge/GitHub-totigm%2Fhumanjs-181717?logo=github"></a>
7
+ <a href="https://github.com/totigm/humanjs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/totigm/humanjs/actions/workflows/ci.yml/badge.svg"></a>
8
+ <a href="https://github.com/totigm/humanjs/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/@humanjs/playwright"></a>
9
+ <a href="https://humanjs.dev"><img alt="docs" src="https://img.shields.io/badge/docs-humanjs.dev-emerald"></a>
10
+ </p>
11
+
3
12
  Humanize Playwright sessions for AI agents, QA tests, and demos. Drop-in adapter for an existing Playwright `Page`.
4
13
 
5
14
  ## Install
@@ -332,7 +341,32 @@ await rec.toTimeline('session.json'); // → JSON on disk
332
341
  const timeline = rec.timeline; // → in-memory object
333
342
  ```
334
343
 
335
- 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.
344
+ The shape (`Timeline` with `personality`, `seed`, `speed`, `durationMs`, and an `events` array of `{ type, params, tMs, durationMs }`, plus `inputValue` on captured `type`/`paste` events) 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.
345
+
346
+ **Code export** — turn the same recording into runnable code:
347
+
348
+ ```ts
349
+ await rec.toHumanJS('session.ts'); // standalone HumanJS script
350
+ await rec.toPlaywright('session.spec.ts'); // @playwright/test spec (humanized)
351
+ ```
352
+
353
+ `toHumanJS()` emits a standalone script (`createHuman` + `human.*`); `toPlaywright()` emits a `@playwright/test` spec that drives the page through HumanJS, so the generated test runs humanized too. String selectors round-trip verbatim. Both work on timeline-only recordings and are unaffected by `dispose()`.
354
+
355
+ `toPlaywright()` also derives the assertions it safely can from the recording — a `read` implies its target was visible (`toBeVisible`), a captured input implies its value (`toHaveValue`) — and leaves a `TODO` for outcome assertions (URL changed, text appeared) that can't be inferred from actions alone. It never fabricates assertions that might fail on a correct run.
356
+
357
+ Generated tests are built to *be tests*: they run `speed: process.env.CI ? 'instant' : '<recorded>'` (instant in CI, recorded feel locally) and drop recorded `sleep()` pauses (timing fidelity belongs in a demo, not a test — pass `toPlaywright(path, { keepSleeps: true })` to keep them). The test title comes from the recording's name (`human.record({ name })`) and is overridable with `{ title }`.
358
+
359
+ Two more options: `{ steps: true }` groups the actions into `test.step(...)` blocks (a new step per navigation) for collapsible sections in the HTML report and trace; `{ baseUrl: true }` rewrites same-origin `goto`s to relative paths and adds a note to set `use.baseURL` in your `playwright.config.ts` — so the same test runs against local / staging / prod.
360
+
361
+ By default the actual typed/pasted text is captured into the timeline (and the exported code). Values typed into `input[type="password"]` are always masked; set `captureInputs: false` to record none — exports then emit empty-string placeholders:
362
+
363
+ ```ts
364
+ await human.record({ captureInputs: false }, fn);
365
+ ```
366
+
367
+ > Captured input values land in the timeline JSON and any exported code — treat those artifacts with the same care as the values themselves.
368
+
369
+ Two limits, by design: a target passed as a `Locator` or a raw `point(x, y)` doesn't round-trip to a clean selector (points are emitted verbatim with a flag comment — locator/point → selector synthesis is a planned follow-up), and reads driven by word-count or raw text emit a note instead of code.
336
370
 
337
371
  **Quality presets** trade off file size, encoding time, and visual fidelity. Defaults to `'high'`:
338
372
 
@@ -364,6 +398,39 @@ await rec.toTimeline('session.json'); // works
364
398
 
365
399
  Every recording is a regular plugin action — `beforeAction` and `afterAction` observe `{ type: 'record' }` exactly like `'click'` or `'scroll'`.
366
400
 
401
+ ## Using your own browser or a persistent profile
402
+
403
+ `createHuman(page)` wraps **any** Playwright `Page` — so reusing a saved login, your installed Chrome, or an already-running browser is just a matter of how you create that page. HumanJS adds nothing special here; these are standard Playwright entry points, collected so you don't have to hunt for them.
404
+
405
+ **Persistent profile** — keep cookies, local storage, and logins across runs. The first run signs in; later runs are already authenticated:
406
+
407
+ ```ts
408
+ import { chromium, createHuman } from '@humanjs/playwright';
409
+
410
+ const context = await chromium.launchPersistentContext('./.humanjs-profile', {
411
+ headless: false,
412
+ channel: 'chrome', // optional: use installed Google Chrome instead of bundled Chromium
413
+ });
414
+ const page = context.pages()[0] ?? (await context.newPage());
415
+
416
+ const human = await createHuman(page, { personality: 'careful' });
417
+ // …drive the page; state persists in ./.humanjs-profile for next time
418
+ ```
419
+
420
+ **Attach to an already-running browser** — drive a Chrome you started yourself, with all its existing tabs, extensions, and sessions. Launch Chrome with a debugging port first (`chrome --remote-debugging-port=9222`), then:
421
+
422
+ ```ts
423
+ import { chromium, createHuman } from '@humanjs/playwright';
424
+
425
+ const browser = await chromium.connectOverCDP('http://localhost:9222');
426
+ const context = browser.contexts()[0];
427
+ const page = context.pages()[0] ?? (await context.newPage());
428
+
429
+ const human = await createHuman(page, { personality: 'careful' });
430
+ ```
431
+
432
+ > **Heads up:** a persistent profile or a connected real browser carries whatever you're signed into. Driving it means the automation can act with those sessions' privileges — keep that in mind for anything sensitive, and be wary of pages that try to manipulate an agent into actions while logged in.
433
+
367
434
  ## License
368
435
 
369
436
  MIT
package/dist/index.cjs CHANGED
@@ -325,15 +325,22 @@ function clamp(value, min, max) {
325
325
  // src/mouse/index.ts
326
326
  async function executeClick(target, ctx, options = {}) {
327
327
  const button = options.button ?? "left";
328
- const locator = typeof target === "string" ? ctx.page.locator(target) : target;
329
328
  if (ctx.speed === "instant") {
330
- const box = await locator.boundingBox();
329
+ if (isPoint(target)) {
330
+ await ctx.page.mouse.click(target.x, target.y, { button });
331
+ ctx.setMousePosition(target);
332
+ return { target };
333
+ }
334
+ const locator = typeof target === "string" ? ctx.page.locator(target) : target;
335
+ const box2 = await locator.boundingBox();
331
336
  await locator.click({ button });
332
- const center = box ? { x: box.x + box.width / 2, y: box.y + box.height / 2 } : ctx.getMousePosition();
337
+ const center = box2 ? { x: box2.x + box2.width / 2, y: box2.y + box2.height / 2 } : ctx.getMousePosition();
333
338
  ctx.setMousePosition(center);
334
339
  return { target: center };
335
340
  }
336
- const targetPoint = await moveToTarget(target, ctx, "click");
341
+ const { point: targetPoint, box } = await resolveTargetPointAndBox(target, ctx, "click");
342
+ await maybeMisclickBeat(ctx, box, targetPoint);
343
+ await walkBezierTo(targetPoint, ctx);
337
344
  const preClickMs = computeDwellTime(
338
345
  ctx.personality.dwell.preClickMs,
339
346
  ctx.personality.dwell.preClickJitter,
@@ -362,7 +369,7 @@ async function executeHover(target, ctx) {
362
369
  ctx.setMousePosition(center);
363
370
  return { target: center };
364
371
  }
365
- const targetPoint = await moveToTarget(target, ctx, "hover");
372
+ const targetPoint = await moveToTarget(target, ctx);
366
373
  const dwellMs = computeDwellTime(
367
374
  ctx.personality.dwell.preClickMs,
368
375
  ctx.personality.dwell.preClickJitter,
@@ -445,10 +452,9 @@ async function executeMove(target, ctx) {
445
452
  ctx.setMousePosition(point);
446
453
  return { target: point };
447
454
  }
448
- async function moveToTarget(target, ctx, action) {
449
- const box = await readBoxWithAutoScroll(target, ctx, action);
455
+ async function moveToTarget(target, ctx) {
456
+ const box = await readBoxWithAutoScroll(target, ctx, "hover");
450
457
  const targetPoint = pickClickPoint(box, ctx.rng, ctx.personality.mouse.clickSpread);
451
- if (action === "click") await maybeMisclickBeat(ctx, box, targetPoint);
452
458
  await walkBezierTo(targetPoint, ctx);
453
459
  return targetPoint;
454
460
  }
@@ -711,6 +717,205 @@ async function detectKindFromTag(locator) {
711
717
  if (tag === "pre" || tag === "code") return "code";
712
718
  return void 0;
713
719
  }
720
+
721
+ // src/recording/codegen.ts
722
+ var POINT_RE = /^point\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)$/;
723
+ var POINT_COMMENT = " // raw coordinate \u2014 replace with a locator for a stable selector";
724
+ var UNCAPTURED_COMMENT = " // input not captured (masked or captureInputs disabled) \u2014 fill in (e.g. process.env.X)";
725
+ function q(value) {
726
+ return `'${String(value ?? "").replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r")}'`;
727
+ }
728
+ function targetArg(desc) {
729
+ const s = String(desc ?? "");
730
+ const m = s.match(POINT_RE);
731
+ if (m) return { code: `{ x: ${m[1]}, y: ${m[2]} }`, isPoint: true };
732
+ return { code: q(s), isPoint: false };
733
+ }
734
+ function createHumanOptions(timeline, ciSpeed = false) {
735
+ const parts = [` personality: ${q(timeline.personality)},`];
736
+ if (timeline.seed !== null) parts.push(` seed: ${q(timeline.seed)},`);
737
+ parts.push(
738
+ ciSpeed ? ` speed: process.env.CI ? 'instant' : ${q(timeline.speed)},` : ` speed: ${q(timeline.speed)},`
739
+ );
740
+ return `{
741
+ ${parts.join("\n")}
742
+ }`;
743
+ }
744
+ function emitScroll(target) {
745
+ const s = String(target ?? "natural");
746
+ if (s === "natural") return " await human.scroll('natural');";
747
+ const by = s.match(/^by:(-?\d+(?:\.\d+)?)$/);
748
+ if (by) return ` await human.scroll({ by: ${by[1]} });`;
749
+ const to = s.match(/^to:(-?\d+(?:\.\d+)?)$/);
750
+ if (to) return ` await human.scroll({ to: ${to[1]} });`;
751
+ return ` await human.scroll(${q(s)});`;
752
+ }
753
+ function emitAction(e, opts = {}) {
754
+ const p = e.params;
755
+ switch (e.type) {
756
+ case "goto": {
757
+ const url = String(p.url ?? "");
758
+ if (opts.baseOrigin && url.startsWith(opts.baseOrigin)) {
759
+ return ` await human.goto(${q(url.slice(opts.baseOrigin.length) || "/")});`;
760
+ }
761
+ return ` await human.goto(${q(url)});`;
762
+ }
763
+ case "click":
764
+ case "rightClick":
765
+ case "hover":
766
+ case "move": {
767
+ const { code, isPoint: isPoint2 } = targetArg(p.target);
768
+ return ` await human.${e.type}(${code});${isPoint2 ? POINT_COMMENT : ""}`;
769
+ }
770
+ case "drag": {
771
+ const from = targetArg(p.from);
772
+ const to = targetArg(p.to);
773
+ const comment = from.isPoint || to.isPoint ? POINT_COMMENT : "";
774
+ return ` await human.drag(${from.code}, ${to.code});${comment}`;
775
+ }
776
+ case "type":
777
+ case "paste": {
778
+ const { code, isPoint: isPoint2 } = targetArg(p.target);
779
+ if (e.inputValue === void 0) {
780
+ return ` await human.${e.type}(${code}, '');${UNCAPTURED_COMMENT}`;
781
+ }
782
+ const call = ` await human.${e.type}(${code}, ${q(e.inputValue)});`;
783
+ if (opts.asserts && !isPoint2) {
784
+ return `${call}
785
+ await expect(page.locator(${code})).toHaveValue(${q(e.inputValue)});`;
786
+ }
787
+ return call;
788
+ }
789
+ case "press":
790
+ return ` await human.press(${q(p.key)});`;
791
+ case "scroll":
792
+ return emitScroll(p.target);
793
+ case "read": {
794
+ const desc = String(p.target ?? "");
795
+ if (/^\d+ words$/.test(desc) || /^text:\d+ chars$/.test(desc)) {
796
+ return ` // human.read(...) \u2014 ${desc}; original target not captured`;
797
+ }
798
+ const call = ` await human.read(${q(desc)});`;
799
+ if (opts.asserts) return `${call}
800
+ await expect(page.locator(${q(desc)})).toBeVisible();`;
801
+ return call;
802
+ }
803
+ case "sleep":
804
+ return ` await sleep(${Number(p.ms) || 0});`;
805
+ case "reload":
806
+ return " await human.reload();";
807
+ case "goBack":
808
+ return " await human.goBack();";
809
+ case "goForward":
810
+ return " await human.goForward();";
811
+ default:
812
+ return ` // unsupported action: ${e.type}`;
813
+ }
814
+ }
815
+ function needsSleepImport(timeline) {
816
+ return timeline.events.some((e) => e.type === "sleep");
817
+ }
818
+ function generateHumanJS(timeline) {
819
+ const imports = needsSleepImport(timeline) ? "import { chromium, createHuman, sleep } from '@humanjs/playwright';" : "import { chromium, createHuman } from '@humanjs/playwright';";
820
+ const body = timeline.events.map((e) => emitAction(e)).join("\n");
821
+ return `${imports}
822
+
823
+ async function main() {
824
+ const browser = await chromium.launch({ headless: false });
825
+ const page = await browser.newPage();
826
+ const human = await createHuman(page, ${createHumanOptions(timeline)});
827
+
828
+ ${body}
829
+
830
+ await browser.close();
831
+ }
832
+
833
+ main();
834
+ `;
835
+ }
836
+ var NAV_TYPES = /* @__PURE__ */ new Set(["goto", "reload", "goBack", "goForward"]);
837
+ function sharedGotoOrigin(events) {
838
+ let origin;
839
+ for (const e of events) {
840
+ if (e.type !== "goto") continue;
841
+ try {
842
+ const o = new URL(String(e.params.url ?? "")).origin;
843
+ if (origin === void 0) origin = o;
844
+ else if (origin !== o) return void 0;
845
+ } catch {
846
+ return void 0;
847
+ }
848
+ }
849
+ return origin;
850
+ }
851
+ function indentLines(block, pad) {
852
+ return block.split("\n").map((line) => line.length > 0 ? pad + line : line).join("\n");
853
+ }
854
+ function stepLabel(event, index, baseOrigin) {
855
+ switch (event.type) {
856
+ case "goto": {
857
+ const url = String(event.params.url ?? "");
858
+ const path = baseOrigin && url.startsWith(baseOrigin) ? url.slice(baseOrigin.length) || "/" : url;
859
+ return `go to ${path}`;
860
+ }
861
+ case "reload":
862
+ return "reload";
863
+ case "goBack":
864
+ return "go back";
865
+ case "goForward":
866
+ return "go forward";
867
+ default:
868
+ return `step ${index + 1}`;
869
+ }
870
+ }
871
+ function emitSteps(events, opts) {
872
+ const groups = [];
873
+ for (const e of events) {
874
+ const last = groups[groups.length - 1];
875
+ if (last === void 0 || NAV_TYPES.has(e.type)) groups.push([e]);
876
+ else last.push(e);
877
+ }
878
+ return groups.map((group, i) => {
879
+ const [first] = group;
880
+ const label = first ? stepLabel(first, i, opts.baseOrigin) : `step ${i + 1}`;
881
+ const inner = group.map((e) => indentLines(emitAction(e, opts), " ")).join("\n");
882
+ return ` await test.step(${q(label)}, async () => {
883
+ ${inner}
884
+ });`;
885
+ }).join("\n\n");
886
+ }
887
+ function generatePlaywrightTest(timeline, options = {}) {
888
+ const events = options.keepSleeps ? timeline.events : timeline.events.filter((e) => e.type !== "sleep");
889
+ const baseOrigin = options.baseUrl ? sharedGotoOrigin(events) : void 0;
890
+ const emitOpts = { asserts: true, baseOrigin };
891
+ const body = options.steps ? emitSteps(events, emitOpts) : events.map((e) => emitAction(e, emitOpts)).join("\n");
892
+ const needsSleep = events.some((e) => e.type === "sleep");
893
+ const hasAsserts = body.includes("await expect(");
894
+ const testImport = hasAsserts ? "import { expect, test } from '@playwright/test';" : "import { test } from '@playwright/test';";
895
+ const humanImport = needsSleep ? "import { createHuman, sleep } from '@humanjs/playwright';" : "import { createHuman } from '@humanjs/playwright';";
896
+ const title = options.title ?? timeline.name ?? "recorded session";
897
+ const baseUrlNote = baseOrigin ? ` // Set use.baseURL = ${q(baseOrigin)} in playwright.config.ts for these relative paths.
898
+
899
+ ` : "";
900
+ const todo = [
901
+ hasAsserts ? " // TODO: add assertions for the outcome of this flow, e.g.:" : " // TODO: assert the outcome \u2014 import { expect } from '@playwright/test', e.g.:",
902
+ " // await expect(page).toHaveURL(/dashboard/);",
903
+ " // await expect(page.getByText('Welcome back')).toBeVisible();"
904
+ ].join("\n");
905
+ return `${testImport}
906
+ ${humanImport}
907
+
908
+ test(${q(title)}, async ({ page }) => {
909
+ const human = await createHuman(page, ${createHumanOptions(timeline, true)});
910
+
911
+ ${baseUrlNote}${body}
912
+
913
+ ${todo}
914
+ });
915
+ `;
916
+ }
917
+
918
+ // src/recording/index.ts
714
919
  var pendingFrameCleanups = /* @__PURE__ */ new Set();
715
920
  var exitHandlerInstalled = false;
716
921
  function ensureExitHandler() {
@@ -806,6 +1011,7 @@ var Recording = class {
806
1011
  get timeline() {
807
1012
  return {
808
1013
  version: 1,
1014
+ ...this.#timelineSource.name !== void 0 ? { name: this.#timelineSource.name } : {},
809
1015
  personality: this.#timelineSource.personality,
810
1016
  seed: this.#timelineSource.seed,
811
1017
  speed: this.#timelineSource.speed,
@@ -956,6 +1162,38 @@ var Recording = class {
956
1162
  `, "utf8");
957
1163
  return outputPath;
958
1164
  }
1165
+ /**
1166
+ * Generates a standalone, runnable HumanJS script from the timeline and
1167
+ * writes it to `outputPath`. String selectors round-trip verbatim; typed
1168
+ * values are included when `captureInputs` was on (passwords masked).
1169
+ *
1170
+ * Independent of frame capture — works on timeline-only recordings and is
1171
+ * unaffected by `dispose()`.
1172
+ *
1173
+ * @returns the resolved output path.
1174
+ */
1175
+ async toHumanJS(outputPath) {
1176
+ await promises.mkdir(path.dirname(outputPath), { recursive: true });
1177
+ await promises.writeFile(outputPath, generateHumanJS(this.timeline), "utf8");
1178
+ return outputPath;
1179
+ }
1180
+ /**
1181
+ * Generates a `@playwright/test` spec from the timeline — a humanized test
1182
+ * (uses `createHuman` + `human.*`), not raw Playwright — and writes it to
1183
+ * `outputPath`. Runs instant in CI / recorded speed locally, drops timing
1184
+ * `sleep()`s (pass `{ keepSleeps: true }` to keep them), and derives the
1185
+ * assertions it safely can.
1186
+ *
1187
+ * Independent of frame capture — works on timeline-only recordings and is
1188
+ * unaffected by `dispose()`.
1189
+ *
1190
+ * @returns the resolved output path.
1191
+ */
1192
+ async toPlaywright(outputPath, options) {
1193
+ await promises.mkdir(path.dirname(outputPath), { recursive: true });
1194
+ await promises.writeFile(outputPath, generatePlaywrightTest(this.timeline, options), "utf8");
1195
+ return outputPath;
1196
+ }
959
1197
  /**
960
1198
  * Releases the captured-frames temp directory. After this call, `toVideo()`
961
1199
  * and `toGif()` throw — but `toTimeline()` and the in-memory `timeline`
@@ -1227,7 +1465,8 @@ async function createHuman(page, options = {}) {
1227
1465
  let hasRecorded = false;
1228
1466
  let activeRecordingEvents = null;
1229
1467
  let activeRecordingStartMs = 0;
1230
- async function performAction(action, actionFn) {
1468
+ let activeRecordingCaptureInputs = false;
1469
+ async function performAction(action, actionFn, recordMeta) {
1231
1470
  for (const plugin of plugins) {
1232
1471
  await plugin.beforeAction?.(action);
1233
1472
  }
@@ -1241,7 +1480,8 @@ async function createHuman(page, options = {}) {
1241
1480
  type: action.type,
1242
1481
  params: action.params ?? {},
1243
1482
  tMs: startedAt - activeRecordingStartMs,
1244
- durationMs
1483
+ durationMs,
1484
+ ...recordMeta?.inputValue !== void 0 ? { inputValue: recordMeta.inputValue } : {}
1245
1485
  });
1246
1486
  }
1247
1487
  for (const plugin of plugins) {
@@ -1255,7 +1495,8 @@ async function createHuman(page, options = {}) {
1255
1495
  params: action.params ?? {},
1256
1496
  tMs: startedAt - activeRecordingStartMs,
1257
1497
  durationMs: Date.now() - startedAt,
1258
- error: error instanceof Error ? error.message : String(error)
1498
+ error: error instanceof Error ? error.message : String(error),
1499
+ ...recordMeta?.inputValue !== void 0 ? { inputValue: recordMeta.inputValue } : {}
1259
1500
  });
1260
1501
  }
1261
1502
  for (const plugin of plugins) {
@@ -1282,6 +1523,21 @@ async function createHuman(page, options = {}) {
1282
1523
  }
1283
1524
  return target.toString?.() ?? "locator";
1284
1525
  };
1526
+ const isPointTarget = (target) => typeof target !== "string" && "x" in target && "y" in target && typeof target.x === "number";
1527
+ const captureInputValue = async (target, value) => {
1528
+ if (activeRecordingEvents === null || !activeRecordingCaptureInputs) return void 0;
1529
+ const locator = typeof target === "string" ? page.locator(target) : isPointTarget(target) ? null : target;
1530
+ if (locator !== null) {
1531
+ try {
1532
+ const fieldType = await locator.first().getAttribute("type", { timeout: 1e3 });
1533
+ if (fieldType?.toLowerCase() === "password") {
1534
+ return void 0;
1535
+ }
1536
+ } catch {
1537
+ }
1538
+ }
1539
+ return value;
1540
+ };
1285
1541
  return {
1286
1542
  personality,
1287
1543
  speed,
@@ -1323,6 +1579,7 @@ async function createHuman(page, options = {}) {
1323
1579
  },
1324
1580
  async type(target, value) {
1325
1581
  const description = describeMouseTarget(target);
1582
+ const inputValue = await captureInputValue(target, value);
1326
1583
  await performAction(
1327
1584
  { type: "type", params: { target: description, length: value.length } },
1328
1585
  async () => {
@@ -1330,11 +1587,13 @@ async function createHuman(page, options = {}) {
1330
1587
  await executeClick(target, mouseCtx());
1331
1588
  }
1332
1589
  await executeType(target, value, { page, personality, rng, speed });
1333
- }
1590
+ },
1591
+ inputValue !== void 0 ? { inputValue } : void 0
1334
1592
  );
1335
1593
  },
1336
1594
  async paste(target, value) {
1337
1595
  const description = describeMouseTarget(target);
1596
+ const inputValue = await captureInputValue(target, value);
1338
1597
  await performAction(
1339
1598
  { type: "paste", params: { target: description, length: value.length } },
1340
1599
  async () => {
@@ -1342,7 +1601,8 @@ async function createHuman(page, options = {}) {
1342
1601
  await executeClick(target, mouseCtx());
1343
1602
  }
1344
1603
  await executePaste(target, value, { page, personality, rng, speed });
1345
- }
1604
+ },
1605
+ inputValue !== void 0 ? { inputValue } : void 0
1346
1606
  );
1347
1607
  },
1348
1608
  async press(key) {
@@ -1411,6 +1671,7 @@ async function createHuman(page, options = {}) {
1411
1671
  const windowStartMs = Date.now();
1412
1672
  activeRecordingEvents = events;
1413
1673
  activeRecordingStartMs = windowStartMs;
1674
+ activeRecordingCaptureInputs = recordOptions.captureInputs !== false;
1414
1675
  let windowEndMs = windowStartMs;
1415
1676
  try {
1416
1677
  await performAction({ type: "record", params: {} }, async () => {
@@ -1425,14 +1686,62 @@ async function createHuman(page, options = {}) {
1425
1686
  throw error;
1426
1687
  } finally {
1427
1688
  activeRecordingEvents = null;
1689
+ activeRecordingCaptureInputs = false;
1428
1690
  }
1429
1691
  const captureResult = captureSession ? await captureSession.stop() : null;
1430
1692
  return new Recording(captureResult, windowStartMs, windowEndMs, {
1693
+ name: recordOptions.name,
1431
1694
  personality: personality.name,
1432
1695
  seed: options.seed === void 0 ? null : String(options.seed),
1433
1696
  speed,
1434
1697
  events
1435
1698
  });
1699
+ },
1700
+ // ────────────────────────────────────────────────────────────────────
1701
+ // Thin re-exports of common Playwright `Page` methods. See the `Human`
1702
+ // interface for the rationale; implementations forward unchanged.
1703
+ // ────────────────────────────────────────────────────────────────────
1704
+ screenshot(opts) {
1705
+ return page.screenshot(opts);
1706
+ },
1707
+ pageText() {
1708
+ return page.innerText("body");
1709
+ },
1710
+ content() {
1711
+ return page.content();
1712
+ },
1713
+ url() {
1714
+ return page.url();
1715
+ },
1716
+ title() {
1717
+ return page.title();
1718
+ },
1719
+ async reload(opts) {
1720
+ await performAction({ type: "reload", params: {} }, async () => {
1721
+ await page.reload(opts);
1722
+ });
1723
+ },
1724
+ async goBack(opts) {
1725
+ await performAction({ type: "goBack", params: {} }, async () => {
1726
+ await page.goBack(opts);
1727
+ });
1728
+ },
1729
+ async goForward(opts) {
1730
+ await performAction({ type: "goForward", params: {} }, async () => {
1731
+ await page.goForward(opts);
1732
+ });
1733
+ },
1734
+ waitForLoadState(state, opts) {
1735
+ return page.waitForLoadState(state, opts);
1736
+ },
1737
+ waitForURL(url, opts) {
1738
+ return page.waitForURL(url, opts);
1739
+ },
1740
+ setViewportSize(size) {
1741
+ return page.setViewportSize(size);
1742
+ },
1743
+ pdf(opts) {
1744
+ return page.pdf(opts);
1436
1745
  }
1437
1746
  };
1438
1747
  }