@humanjs/playwright 0.8.0 → 0.10.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
@@ -380,6 +380,26 @@ Generated tests are built to *be tests*: they run `speed: process.env.CI ? 'inst
380
380
 
381
381
  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.
382
382
 
383
+ ### Replaying a timeline
384
+
385
+ `replayTimeline(page, timeline, options?)` runs a recorded `Timeline` against a live page, driving it through the same humanized primitives the exported test uses — without spawning a test runner. It runs each event in order, reports per-step status via `onStep`, and **stops at the first failure** (like a real test).
386
+
387
+ ```ts
388
+ import { chromium, replayTimeline } from '@humanjs/playwright';
389
+
390
+ const page = await (await chromium.launch({ headless: false })).newPage();
391
+ const result = await replayTimeline(page, recording.timeline, {
392
+ personality: 'careful',
393
+ onStep: ({ index, type, status }) => console.log(`${index} ${type}: ${status}`),
394
+ });
395
+
396
+ result.status; // 'pass' | 'fail'
397
+ result.failedIndex; // index of the failing step, when failed
398
+ result.steps; // per-step { index, type, status, error? }
399
+ ```
400
+
401
+ `assert` events are evaluated with plain Playwright APIs — there's no `@playwright/test` dependency — so they approximate `expect` (`toBeVisible` via `waitFor`, `toHaveText` via normalized text equality, `toHaveURL` via `page.url()`) without its auto-retry on text/url. Pass an `AbortSignal` via `{ signal }` to cancel a run; it rejects with an `AbortError`. The cursor is on and `speed` is `'human'` by default so the replay is watchable. You own `page`'s lifecycle — `replayTimeline` doesn't close it. This is the engine behind the `@humanjs/generator` dashboard's **Run** button.
402
+
383
403
  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:
384
404
 
385
405
  ```ts
@@ -580,6 +580,51 @@ async function executeUpload(target, files, ctx) {
580
580
  }
581
581
  await locator.setInputFiles(files);
582
582
  }
583
+
584
+ // src/internal/select-substring.ts
585
+ function selectSubstringInElement(el, needleRaw) {
586
+ const needle = needleRaw.replace(/\s+/g, " ").trim();
587
+ if (!needle) return false;
588
+ const map = [];
589
+ let normalized = "";
590
+ let prevSpace = true;
591
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
592
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
593
+ const text = node;
594
+ const data = text.data;
595
+ for (let i = 0; i < data.length; i++) {
596
+ const ch = data.charAt(i);
597
+ if (/\s/.test(ch)) {
598
+ if (!prevSpace) {
599
+ normalized += " ";
600
+ map.push([text, i]);
601
+ }
602
+ prevSpace = true;
603
+ } else {
604
+ normalized += ch;
605
+ map.push([text, i]);
606
+ prevSpace = false;
607
+ }
608
+ }
609
+ }
610
+ if (normalized.endsWith(" ")) {
611
+ normalized = normalized.slice(0, -1);
612
+ map.pop();
613
+ }
614
+ const start = normalized.indexOf(needle);
615
+ if (start === -1) return false;
616
+ const startPos = map[start];
617
+ const endPos = map[start + needle.length - 1];
618
+ if (!startPos || !endPos) return false;
619
+ const range = document.createRange();
620
+ range.setStart(startPos[0], startPos[1]);
621
+ range.setEnd(endPos[0], endPos[1] + 1);
622
+ const selection = window.getSelection();
623
+ if (!selection) return false;
624
+ selection.removeAllRanges();
625
+ selection.addRange(range);
626
+ return true;
627
+ }
583
628
  async function executeType(target, value, ctx) {
584
629
  const locator = typeof target === "string" ? ctx.page.locator(target) : target;
585
630
  if (value.length === 0) {
@@ -696,6 +741,118 @@ function normalizeKey(key) {
696
741
  function isMac() {
697
742
  return process.platform === "darwin";
698
743
  }
744
+
745
+ // src/mouse-helper/index.ts
746
+ var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
747
+ var INSTALLED_FLAG = /* @__PURE__ */ Symbol.for("@humanjs/playwright:mouse-helper:installed");
748
+ async function installMouseHelper(target, options = {}) {
749
+ const tagged = target;
750
+ if (tagged[INSTALLED_FLAG]) return;
751
+ tagged[INSTALLED_FLAG] = true;
752
+ const config = {
753
+ color: options.color ?? "#f5a55c",
754
+ stroke: "#020203",
755
+ size: options.size ?? 22,
756
+ showClicks: options.showClicks ?? true,
757
+ haloOpacity: options.haloOpacity ?? 0.18,
758
+ path: CURSOR_PATH
759
+ };
760
+ await target.addInitScript(installScript, config);
761
+ const attachPageHooks = (page) => {
762
+ page.on("domcontentloaded", () => {
763
+ page.evaluate(installScript, config).catch(() => void 0);
764
+ });
765
+ };
766
+ const pages = "pages" in target ? target.pages() : [target];
767
+ for (const page of pages) attachPageHooks(page);
768
+ if ("on" in target && "newPage" in target) {
769
+ target.on("page", attachPageHooks);
770
+ }
771
+ await Promise.all(
772
+ pages.map((page) => page.evaluate(installScript, config).catch(() => void 0))
773
+ );
774
+ }
775
+ function installScript(config) {
776
+ if (document.querySelector("[data-humanjs-cursor]")) return;
777
+ const attach = () => {
778
+ const cursor = document.createElement("div");
779
+ cursor.setAttribute("aria-hidden", "true");
780
+ cursor.setAttribute("data-humanjs-cursor", "true");
781
+ cursor.style.cssText = [
782
+ "position: fixed",
783
+ "left: 0",
784
+ "top: 0",
785
+ `width: ${config.size}px`,
786
+ `height: ${config.size + 4}px`,
787
+ "pointer-events: none",
788
+ "z-index: 2147483647",
789
+ // Start visible at (0, 0) so the cursor is on screen from the moment
790
+ // the page loads — without this the helper looks like nothing happened
791
+ // until the first mousemove arrives.
792
+ "opacity: 1",
793
+ "transform: translate(0px, 0px)",
794
+ // CSS interpolates between successive `mousemove` updates so the
795
+ // cursor reads as continuous motion instead of discrete hops. Slightly
796
+ // longer than the path-walker's typical step interval (~30–80ms) so
797
+ // each tween is still settling when the next move lands → no pauses.
798
+ "transition: transform 110ms ease-out, opacity 0.18s ease-out",
799
+ "will-change: transform"
800
+ ].join("; ");
801
+ const haloRadius = Math.round(config.size * 0.6);
802
+ cursor.innerHTML = `
803
+ <svg width="${config.size}" height="${config.size + 4}" viewBox="0 0 22 24" style="overflow: visible;">
804
+ <circle cx="0" cy="0" r="${haloRadius}" fill="${config.color}" opacity="${config.haloOpacity}" />
805
+ <path d="${config.path}" fill="${config.color}" stroke="${config.stroke}" stroke-width="0.7" stroke-linejoin="round" />
806
+ </svg>
807
+ `;
808
+ document.body.appendChild(cursor);
809
+ let lastX = 0;
810
+ let lastY = 0;
811
+ const onMove = (e) => {
812
+ lastX = e.clientX;
813
+ lastY = e.clientY;
814
+ cursor.style.transform = `translate(${lastX}px, ${lastY}px)`;
815
+ cursor.style.opacity = "1";
816
+ };
817
+ window.addEventListener("mousemove", onMove, { capture: true, passive: true });
818
+ document.addEventListener("mousemove", onMove, { capture: true, passive: true });
819
+ document.addEventListener(
820
+ "mouseleave",
821
+ () => {
822
+ cursor.style.opacity = "0";
823
+ },
824
+ { capture: true, passive: true }
825
+ );
826
+ if (config.showClicks) {
827
+ const styleEl = document.createElement("style");
828
+ styleEl.textContent = "@keyframes humanjs-ripple { 0% { transform: translate(-50%, -50%) scale(0.4); opacity: 0.9; } 100% { transform: translate(-50%, -50%) scale(2); opacity: 0; } }";
829
+ document.head.appendChild(styleEl);
830
+ window.addEventListener(
831
+ "mousedown",
832
+ () => {
833
+ const ripple = document.createElement("div");
834
+ ripple.style.cssText = [
835
+ "position: fixed",
836
+ `left: ${lastX}px`,
837
+ `top: ${lastY}px`,
838
+ "width: 28px",
839
+ "height: 28px",
840
+ "border-radius: 50%",
841
+ `border: 1.5px solid ${config.color}`,
842
+ "pointer-events: none",
843
+ "z-index: 2147483646",
844
+ "animation: humanjs-ripple 0.45s ease-out forwards"
845
+ ].join("; ");
846
+ document.body.appendChild(ripple);
847
+ window.setTimeout(() => ripple.remove(), 500);
848
+ },
849
+ { capture: true, passive: true }
850
+ );
851
+ }
852
+ };
853
+ if (document.body) attach();
854
+ else document.addEventListener("DOMContentLoaded", attach, { once: true });
855
+ }
699
856
  async function executeRead(target, ctx, options = {}) {
700
857
  let words = 0;
701
858
  let locator;
@@ -788,8 +945,17 @@ async function detectKindFromTag(locator) {
788
945
  return void 0;
789
946
  }
790
947
 
791
- // src/recording/codegen.ts
948
+ // src/recording/targets.ts
792
949
  var POINT_RE = /^point\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)$/;
950
+ function parsePointTarget(desc) {
951
+ const match = String(desc ?? "").match(POINT_RE);
952
+ return match ? { x: Number(match[1]), y: Number(match[2]) } : null;
953
+ }
954
+ function resolveMouseTarget(desc) {
955
+ return parsePointTarget(desc) ?? String(desc ?? "");
956
+ }
957
+
958
+ // src/recording/codegen.ts
793
959
  var POINT_COMMENT = " // raw coordinate \u2014 replace with a locator for a stable selector";
794
960
  var UNCAPTURED_COMMENT = " // input not captured (masked or captureInputs disabled) \u2014 fill in (e.g. process.env.X)";
795
961
  function q(value) {
@@ -855,6 +1021,13 @@ function emitAction(e, opts = {}) {
855
1021
  const { code } = targetArg(p.target);
856
1022
  return ` await human.${e.type}(${code});`;
857
1023
  }
1024
+ case "selectText": {
1025
+ const { code } = targetArg(p.target);
1026
+ if (typeof p.text === "string" && p.text.length > 0) {
1027
+ return ` await human.selectText(${code}, { text: ${q(p.text)} });`;
1028
+ }
1029
+ return ` await human.selectText(${code});`;
1030
+ }
858
1031
  case "selectOption": {
859
1032
  const { code } = targetArg(p.target);
860
1033
  return ` await human.selectOption(${code}, ${serializeSelectValues(p.values)});`;
@@ -904,6 +1077,14 @@ function emitAction(e, opts = {}) {
904
1077
  return " await human.goBack();";
905
1078
  case "goForward":
906
1079
  return " await human.goForward();";
1080
+ case "assert": {
1081
+ const kind = String(p.kind ?? "visible");
1082
+ if (kind === "url") return ` await expect(page).toHaveURL(${q(p.value)});`;
1083
+ const { code } = targetArg(p.target);
1084
+ if (kind === "text")
1085
+ return ` await expect(page.locator(${code})).toHaveText(${q(p.value)});`;
1086
+ return ` await expect(page.locator(${code})).toBeVisible();`;
1087
+ }
907
1088
  default:
908
1089
  return ` // unsupported action: ${e.type}`;
909
1090
  }
@@ -913,7 +1094,7 @@ function needsSleepImport(timeline) {
913
1094
  }
914
1095
  function generateHumanJS(timeline) {
915
1096
  const imports = needsSleepImport(timeline) ? "import { chromium, createHuman, sleep } from '@humanjs/playwright';" : "import { chromium, createHuman } from '@humanjs/playwright';";
916
- const body = timeline.events.map((e) => emitAction(e)).join("\n");
1097
+ const body = timeline.events.filter((e) => e.type !== "assert").map((e) => emitAction(e)).join("\n");
917
1098
  return `${imports}
918
1099
 
919
1100
  async function main() {
@@ -1010,6 +1191,150 @@ ${todo}
1010
1191
  });
1011
1192
  `;
1012
1193
  }
1194
+ function abortError() {
1195
+ const error = new Error("Replay aborted");
1196
+ error.name = "AbortError";
1197
+ return error;
1198
+ }
1199
+ async function replayTimeline(page, timeline, options = {}) {
1200
+ const events = Array.isArray(timeline) ? timeline : timeline.events;
1201
+ const { onStep, signal } = options;
1202
+ if (signal?.aborted) throw abortError();
1203
+ const human = await createHuman(page, {
1204
+ personality: options.personality ?? "careful",
1205
+ speed: options.speed ?? "human",
1206
+ ...options.seed !== void 0 ? { seed: options.seed } : {},
1207
+ ...options.cursor !== void 0 ? { cursor: options.cursor } : {}
1208
+ });
1209
+ const startedAt = Date.now();
1210
+ const steps = [];
1211
+ for (const [index, event] of events.entries()) {
1212
+ if (signal?.aborted) throw abortError();
1213
+ onStep?.({ index, type: event.type, status: "running" });
1214
+ try {
1215
+ await runEvent(human, page, event);
1216
+ } catch (cause) {
1217
+ const error = cause instanceof Error ? cause.message : String(cause);
1218
+ steps.push({ index, type: event.type, status: "fail", error });
1219
+ onStep?.({ index, type: event.type, status: "fail", error });
1220
+ return { status: "fail", steps, failedIndex: index, durationMs: Date.now() - startedAt };
1221
+ }
1222
+ steps.push({ index, type: event.type, status: "pass" });
1223
+ onStep?.({ index, type: event.type, status: "pass" });
1224
+ }
1225
+ return { status: "pass", steps, durationMs: Date.now() - startedAt };
1226
+ }
1227
+ function parseScrollTarget(target) {
1228
+ const value = String(target ?? "natural");
1229
+ const by = value.match(/^by:(-?\d+(?:\.\d+)?)$/);
1230
+ if (by) return { by: Number(by[1]) };
1231
+ const to = value.match(/^to:(-?\d+(?:\.\d+)?)$/);
1232
+ if (to) return { to: Number(to[1]) };
1233
+ return value;
1234
+ }
1235
+ var normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
1236
+ async function runAssert(page, params) {
1237
+ const kind = String(params.kind ?? "visible");
1238
+ if (kind === "url") {
1239
+ const actual = page.url();
1240
+ const expected = String(params.value ?? "");
1241
+ if (actual !== expected) {
1242
+ throw new Error(`expected URL ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
1243
+ }
1244
+ return;
1245
+ }
1246
+ const locator = page.locator(String(params.target ?? "")).first();
1247
+ await locator.waitFor({ state: "visible" });
1248
+ if (kind === "text") {
1249
+ const actual = normalizeText(await locator.textContent());
1250
+ const expected = normalizeText(String(params.value ?? ""));
1251
+ if (actual !== expected) {
1252
+ throw new Error(`expected text ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
1253
+ }
1254
+ }
1255
+ }
1256
+ async function runEvent(human, page, event) {
1257
+ const p = event.params;
1258
+ switch (event.type) {
1259
+ case "goto":
1260
+ await human.goto(String(p.url ?? ""));
1261
+ return;
1262
+ case "click":
1263
+ await human.click(resolveMouseTarget(p.target));
1264
+ return;
1265
+ case "rightClick":
1266
+ await human.rightClick(resolveMouseTarget(p.target));
1267
+ return;
1268
+ case "doubleClick":
1269
+ await human.doubleClick(resolveMouseTarget(p.target));
1270
+ return;
1271
+ case "move":
1272
+ await human.move(resolveMouseTarget(p.target));
1273
+ return;
1274
+ case "hover":
1275
+ await human.hover(String(p.target ?? ""));
1276
+ return;
1277
+ case "drag":
1278
+ await human.drag(resolveMouseTarget(p.from), resolveMouseTarget(p.to));
1279
+ return;
1280
+ case "type":
1281
+ await human.type(String(p.target ?? ""), event.inputValue ?? "");
1282
+ return;
1283
+ case "paste":
1284
+ await human.paste(String(p.target ?? ""), event.inputValue ?? "");
1285
+ return;
1286
+ case "clear":
1287
+ await human.clear(String(p.target ?? ""));
1288
+ return;
1289
+ case "check":
1290
+ await human.check(String(p.target ?? ""));
1291
+ return;
1292
+ case "uncheck":
1293
+ await human.uncheck(String(p.target ?? ""));
1294
+ return;
1295
+ case "selectText":
1296
+ await human.selectText(
1297
+ String(p.target ?? ""),
1298
+ typeof p.text === "string" ? { text: p.text } : void 0
1299
+ );
1300
+ return;
1301
+ case "selectOption":
1302
+ await human.selectOption(String(p.target ?? ""), p.values);
1303
+ return;
1304
+ case "upload":
1305
+ await human.upload(String(p.target ?? ""), p.files);
1306
+ return;
1307
+ case "press":
1308
+ await human.press(String(p.key ?? ""));
1309
+ return;
1310
+ case "scroll":
1311
+ await human.scroll(parseScrollTarget(p.target));
1312
+ return;
1313
+ case "read": {
1314
+ const target = String(p.target ?? "");
1315
+ if (/^\d+ words$/.test(target) || /^text:\d+ chars$/.test(target)) return;
1316
+ await human.read(target);
1317
+ return;
1318
+ }
1319
+ case "sleep":
1320
+ await core.sleep(Number(p.ms) || 0);
1321
+ return;
1322
+ case "reload":
1323
+ await human.reload();
1324
+ return;
1325
+ case "goBack":
1326
+ await human.goBack();
1327
+ return;
1328
+ case "goForward":
1329
+ await human.goForward();
1330
+ return;
1331
+ case "assert":
1332
+ await runAssert(page, p);
1333
+ return;
1334
+ default:
1335
+ return;
1336
+ }
1337
+ }
1013
1338
 
1014
1339
  // src/recording/index.ts
1015
1340
  var pendingFrameCleanups = /* @__PURE__ */ new Set();
@@ -1435,120 +1760,6 @@ async function startCapture(page, options = {}) {
1435
1760
  }
1436
1761
  };
1437
1762
  }
1438
-
1439
- // src/mouse-helper/index.ts
1440
- var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
1441
- var INSTALLED_FLAG = /* @__PURE__ */ Symbol.for("@humanjs/playwright:mouse-helper:installed");
1442
- async function installMouseHelper(target, options = {}) {
1443
- const tagged = target;
1444
- if (tagged[INSTALLED_FLAG]) return;
1445
- tagged[INSTALLED_FLAG] = true;
1446
- const config = {
1447
- color: options.color ?? "#f5a55c",
1448
- stroke: "#020203",
1449
- size: options.size ?? 22,
1450
- showClicks: options.showClicks ?? true,
1451
- haloOpacity: options.haloOpacity ?? 0.18,
1452
- path: CURSOR_PATH
1453
- };
1454
- await target.addInitScript(installScript, config);
1455
- const attachPageHooks = (page) => {
1456
- page.on("domcontentloaded", () => {
1457
- page.evaluate(installScript, config).catch(() => void 0);
1458
- });
1459
- };
1460
- const pages = "pages" in target ? target.pages() : [target];
1461
- for (const page of pages) attachPageHooks(page);
1462
- if ("on" in target && "newPage" in target) {
1463
- target.on("page", attachPageHooks);
1464
- }
1465
- await Promise.all(
1466
- pages.map((page) => page.evaluate(installScript, config).catch(() => void 0))
1467
- );
1468
- }
1469
- function installScript(config) {
1470
- if (document.querySelector("[data-humanjs-cursor]")) return;
1471
- const attach = () => {
1472
- const cursor = document.createElement("div");
1473
- cursor.setAttribute("aria-hidden", "true");
1474
- cursor.setAttribute("data-humanjs-cursor", "true");
1475
- cursor.style.cssText = [
1476
- "position: fixed",
1477
- "left: 0",
1478
- "top: 0",
1479
- `width: ${config.size}px`,
1480
- `height: ${config.size + 4}px`,
1481
- "pointer-events: none",
1482
- "z-index: 2147483647",
1483
- // Start visible at (0, 0) so the cursor is on screen from the moment
1484
- // the page loads — without this the helper looks like nothing happened
1485
- // until the first mousemove arrives.
1486
- "opacity: 1",
1487
- "transform: translate(0px, 0px)",
1488
- // CSS interpolates between successive `mousemove` updates so the
1489
- // cursor reads as continuous motion instead of discrete hops. Slightly
1490
- // longer than the path-walker's typical step interval (~30–80ms) so
1491
- // each tween is still settling when the next move lands → no pauses.
1492
- "transition: transform 110ms ease-out, opacity 0.18s ease-out",
1493
- "will-change: transform"
1494
- ].join("; ");
1495
- const haloRadius = Math.round(config.size * 0.6);
1496
- cursor.innerHTML = `
1497
- <svg width="${config.size}" height="${config.size + 4}" viewBox="0 0 22 24" style="overflow: visible;">
1498
- <circle cx="0" cy="0" r="${haloRadius}" fill="${config.color}" opacity="${config.haloOpacity}" />
1499
- <path d="${config.path}" fill="${config.color}" stroke="${config.stroke}" stroke-width="0.7" stroke-linejoin="round" />
1500
- </svg>
1501
- `;
1502
- document.body.appendChild(cursor);
1503
- let lastX = 0;
1504
- let lastY = 0;
1505
- const onMove = (e) => {
1506
- lastX = e.clientX;
1507
- lastY = e.clientY;
1508
- cursor.style.transform = `translate(${lastX}px, ${lastY}px)`;
1509
- cursor.style.opacity = "1";
1510
- };
1511
- window.addEventListener("mousemove", onMove, { capture: true, passive: true });
1512
- document.addEventListener("mousemove", onMove, { capture: true, passive: true });
1513
- document.addEventListener(
1514
- "mouseleave",
1515
- () => {
1516
- cursor.style.opacity = "0";
1517
- },
1518
- { capture: true, passive: true }
1519
- );
1520
- if (config.showClicks) {
1521
- const styleEl = document.createElement("style");
1522
- styleEl.textContent = "@keyframes humanjs-ripple { 0% { transform: translate(-50%, -50%) scale(0.4); opacity: 0.9; } 100% { transform: translate(-50%, -50%) scale(2); opacity: 0; } }";
1523
- document.head.appendChild(styleEl);
1524
- window.addEventListener(
1525
- "mousedown",
1526
- () => {
1527
- const ripple = document.createElement("div");
1528
- ripple.style.cssText = [
1529
- "position: fixed",
1530
- `left: ${lastX}px`,
1531
- `top: ${lastY}px`,
1532
- "width: 28px",
1533
- "height: 28px",
1534
- "border-radius: 50%",
1535
- `border: 1.5px solid ${config.color}`,
1536
- "pointer-events: none",
1537
- "z-index: 2147483646",
1538
- "animation: humanjs-ripple 0.45s ease-out forwards"
1539
- ].join("; ");
1540
- document.body.appendChild(ripple);
1541
- window.setTimeout(() => ripple.remove(), 500);
1542
- },
1543
- { capture: true, passive: true }
1544
- );
1545
- }
1546
- };
1547
- if (document.body) attach();
1548
- else document.addEventListener("DOMContentLoaded", attach, { once: true });
1549
- }
1550
-
1551
- // src/index.ts
1552
1763
  async function createHuman(page, options = {}) {
1553
1764
  const personality = core.resolvePersonality(options.personality ?? "careful");
1554
1765
  const rng = core.createRng(options.seed);
@@ -1558,6 +1769,9 @@ async function createHuman(page, options = {}) {
1558
1769
  for (const plugin of plugins) {
1559
1770
  await plugin.install?.(context);
1560
1771
  }
1772
+ if (options.cursor !== false && typeof page.addInitScript === "function") {
1773
+ await installMouseHelper(page, typeof options.cursor === "object" ? options.cursor : {});
1774
+ }
1561
1775
  let hasRecorded = false;
1562
1776
  let activeRecordingEvents = null;
1563
1777
  let activeRecordingStartMs = 0;
@@ -1740,6 +1954,27 @@ async function createHuman(page, options = {}) {
1740
1954
  () => executeSelectOption(target, values, mouseCtx())
1741
1955
  );
1742
1956
  },
1957
+ async selectText(target, options2) {
1958
+ const text = options2?.text;
1959
+ await performAction(
1960
+ {
1961
+ type: "selectText",
1962
+ params: { target: describeMouseTarget(target), ...text !== void 0 ? { text } : {} }
1963
+ },
1964
+ async () => {
1965
+ if (speed !== "instant") {
1966
+ await executeMove(target, mouseCtx());
1967
+ }
1968
+ const locator = typeof target === "string" ? page.locator(target) : target;
1969
+ if (text === void 0) {
1970
+ await locator.selectText();
1971
+ return;
1972
+ }
1973
+ const found = await locator.evaluate(selectSubstringInElement, text);
1974
+ if (!found) await locator.selectText();
1975
+ }
1976
+ );
1977
+ },
1743
1978
  async upload(target, files) {
1744
1979
  await performAction(
1745
1980
  {
@@ -1993,6 +2228,9 @@ Object.defineProperty(exports, "webkit", {
1993
2228
  });
1994
2229
  exports.Recording = Recording;
1995
2230
  exports.createHuman = createHuman;
2231
+ exports.generateHumanJS = generateHumanJS;
2232
+ exports.generatePlaywrightTest = generatePlaywrightTest;
1996
2233
  exports.installMouseHelper = installMouseHelper;
1997
- //# sourceMappingURL=chunk-RCMSDC3N.cjs.map
1998
- //# sourceMappingURL=chunk-RCMSDC3N.cjs.map
2234
+ exports.replayTimeline = replayTimeline;
2235
+ //# sourceMappingURL=chunk-665R4N7R.cjs.map
2236
+ //# sourceMappingURL=chunk-665R4N7R.cjs.map