@humanjs/playwright 0.8.0 → 0.9.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.
@@ -575,6 +575,51 @@ async function executeUpload(target, files, ctx) {
575
575
  }
576
576
  await locator.setInputFiles(files);
577
577
  }
578
+
579
+ // src/internal/select-substring.ts
580
+ function selectSubstringInElement(el, needleRaw) {
581
+ const needle = needleRaw.replace(/\s+/g, " ").trim();
582
+ if (!needle) return false;
583
+ const map = [];
584
+ let normalized = "";
585
+ let prevSpace = true;
586
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
587
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
588
+ const text = node;
589
+ const data = text.data;
590
+ for (let i = 0; i < data.length; i++) {
591
+ const ch = data.charAt(i);
592
+ if (/\s/.test(ch)) {
593
+ if (!prevSpace) {
594
+ normalized += " ";
595
+ map.push([text, i]);
596
+ }
597
+ prevSpace = true;
598
+ } else {
599
+ normalized += ch;
600
+ map.push([text, i]);
601
+ prevSpace = false;
602
+ }
603
+ }
604
+ }
605
+ if (normalized.endsWith(" ")) {
606
+ normalized = normalized.slice(0, -1);
607
+ map.pop();
608
+ }
609
+ const start = normalized.indexOf(needle);
610
+ if (start === -1) return false;
611
+ const startPos = map[start];
612
+ const endPos = map[start + needle.length - 1];
613
+ if (!startPos || !endPos) return false;
614
+ const range = document.createRange();
615
+ range.setStart(startPos[0], startPos[1]);
616
+ range.setEnd(endPos[0], endPos[1] + 1);
617
+ const selection = window.getSelection();
618
+ if (!selection) return false;
619
+ selection.removeAllRanges();
620
+ selection.addRange(range);
621
+ return true;
622
+ }
578
623
  async function executeType(target, value, ctx) {
579
624
  const locator = typeof target === "string" ? ctx.page.locator(target) : target;
580
625
  if (value.length === 0) {
@@ -691,6 +736,118 @@ function normalizeKey(key) {
691
736
  function isMac() {
692
737
  return process.platform === "darwin";
693
738
  }
739
+
740
+ // src/mouse-helper/index.ts
741
+ var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
742
+ var INSTALLED_FLAG = /* @__PURE__ */ Symbol.for("@humanjs/playwright:mouse-helper:installed");
743
+ async function installMouseHelper(target, options = {}) {
744
+ const tagged = target;
745
+ if (tagged[INSTALLED_FLAG]) return;
746
+ tagged[INSTALLED_FLAG] = true;
747
+ const config = {
748
+ color: options.color ?? "#f5a55c",
749
+ stroke: "#020203",
750
+ size: options.size ?? 22,
751
+ showClicks: options.showClicks ?? true,
752
+ haloOpacity: options.haloOpacity ?? 0.18,
753
+ path: CURSOR_PATH
754
+ };
755
+ await target.addInitScript(installScript, config);
756
+ const attachPageHooks = (page) => {
757
+ page.on("domcontentloaded", () => {
758
+ page.evaluate(installScript, config).catch(() => void 0);
759
+ });
760
+ };
761
+ const pages = "pages" in target ? target.pages() : [target];
762
+ for (const page of pages) attachPageHooks(page);
763
+ if ("on" in target && "newPage" in target) {
764
+ target.on("page", attachPageHooks);
765
+ }
766
+ await Promise.all(
767
+ pages.map((page) => page.evaluate(installScript, config).catch(() => void 0))
768
+ );
769
+ }
770
+ function installScript(config) {
771
+ if (document.querySelector("[data-humanjs-cursor]")) return;
772
+ const attach = () => {
773
+ const cursor = document.createElement("div");
774
+ cursor.setAttribute("aria-hidden", "true");
775
+ cursor.setAttribute("data-humanjs-cursor", "true");
776
+ cursor.style.cssText = [
777
+ "position: fixed",
778
+ "left: 0",
779
+ "top: 0",
780
+ `width: ${config.size}px`,
781
+ `height: ${config.size + 4}px`,
782
+ "pointer-events: none",
783
+ "z-index: 2147483647",
784
+ // Start visible at (0, 0) so the cursor is on screen from the moment
785
+ // the page loads — without this the helper looks like nothing happened
786
+ // until the first mousemove arrives.
787
+ "opacity: 1",
788
+ "transform: translate(0px, 0px)",
789
+ // CSS interpolates between successive `mousemove` updates so the
790
+ // cursor reads as continuous motion instead of discrete hops. Slightly
791
+ // longer than the path-walker's typical step interval (~30–80ms) so
792
+ // each tween is still settling when the next move lands → no pauses.
793
+ "transition: transform 110ms ease-out, opacity 0.18s ease-out",
794
+ "will-change: transform"
795
+ ].join("; ");
796
+ const haloRadius = Math.round(config.size * 0.6);
797
+ cursor.innerHTML = `
798
+ <svg width="${config.size}" height="${config.size + 4}" viewBox="0 0 22 24" style="overflow: visible;">
799
+ <circle cx="0" cy="0" r="${haloRadius}" fill="${config.color}" opacity="${config.haloOpacity}" />
800
+ <path d="${config.path}" fill="${config.color}" stroke="${config.stroke}" stroke-width="0.7" stroke-linejoin="round" />
801
+ </svg>
802
+ `;
803
+ document.body.appendChild(cursor);
804
+ let lastX = 0;
805
+ let lastY = 0;
806
+ const onMove = (e) => {
807
+ lastX = e.clientX;
808
+ lastY = e.clientY;
809
+ cursor.style.transform = `translate(${lastX}px, ${lastY}px)`;
810
+ cursor.style.opacity = "1";
811
+ };
812
+ window.addEventListener("mousemove", onMove, { capture: true, passive: true });
813
+ document.addEventListener("mousemove", onMove, { capture: true, passive: true });
814
+ document.addEventListener(
815
+ "mouseleave",
816
+ () => {
817
+ cursor.style.opacity = "0";
818
+ },
819
+ { capture: true, passive: true }
820
+ );
821
+ if (config.showClicks) {
822
+ const styleEl = document.createElement("style");
823
+ 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; } }";
824
+ document.head.appendChild(styleEl);
825
+ window.addEventListener(
826
+ "mousedown",
827
+ () => {
828
+ const ripple = document.createElement("div");
829
+ ripple.style.cssText = [
830
+ "position: fixed",
831
+ `left: ${lastX}px`,
832
+ `top: ${lastY}px`,
833
+ "width: 28px",
834
+ "height: 28px",
835
+ "border-radius: 50%",
836
+ `border: 1.5px solid ${config.color}`,
837
+ "pointer-events: none",
838
+ "z-index: 2147483646",
839
+ "animation: humanjs-ripple 0.45s ease-out forwards"
840
+ ].join("; ");
841
+ document.body.appendChild(ripple);
842
+ window.setTimeout(() => ripple.remove(), 500);
843
+ },
844
+ { capture: true, passive: true }
845
+ );
846
+ }
847
+ };
848
+ if (document.body) attach();
849
+ else document.addEventListener("DOMContentLoaded", attach, { once: true });
850
+ }
694
851
  async function executeRead(target, ctx, options = {}) {
695
852
  let words = 0;
696
853
  let locator;
@@ -850,6 +1007,13 @@ function emitAction(e, opts = {}) {
850
1007
  const { code } = targetArg(p.target);
851
1008
  return ` await human.${e.type}(${code});`;
852
1009
  }
1010
+ case "selectText": {
1011
+ const { code } = targetArg(p.target);
1012
+ if (typeof p.text === "string" && p.text.length > 0) {
1013
+ return ` await human.selectText(${code}, { text: ${q(p.text)} });`;
1014
+ }
1015
+ return ` await human.selectText(${code});`;
1016
+ }
853
1017
  case "selectOption": {
854
1018
  const { code } = targetArg(p.target);
855
1019
  return ` await human.selectOption(${code}, ${serializeSelectValues(p.values)});`;
@@ -899,6 +1063,14 @@ function emitAction(e, opts = {}) {
899
1063
  return " await human.goBack();";
900
1064
  case "goForward":
901
1065
  return " await human.goForward();";
1066
+ case "assert": {
1067
+ const kind = String(p.kind ?? "visible");
1068
+ if (kind === "url") return ` await expect(page).toHaveURL(${q(p.value)});`;
1069
+ const { code } = targetArg(p.target);
1070
+ if (kind === "text")
1071
+ return ` await expect(page.locator(${code})).toHaveText(${q(p.value)});`;
1072
+ return ` await expect(page.locator(${code})).toBeVisible();`;
1073
+ }
902
1074
  default:
903
1075
  return ` // unsupported action: ${e.type}`;
904
1076
  }
@@ -908,7 +1080,7 @@ function needsSleepImport(timeline) {
908
1080
  }
909
1081
  function generateHumanJS(timeline) {
910
1082
  const imports = needsSleepImport(timeline) ? "import { chromium, createHuman, sleep } from '@humanjs/playwright';" : "import { chromium, createHuman } from '@humanjs/playwright';";
911
- const body = timeline.events.map((e) => emitAction(e)).join("\n");
1083
+ const body = timeline.events.filter((e) => e.type !== "assert").map((e) => emitAction(e)).join("\n");
912
1084
  return `${imports}
913
1085
 
914
1086
  async function main() {
@@ -1430,120 +1602,6 @@ async function startCapture(page, options = {}) {
1430
1602
  }
1431
1603
  };
1432
1604
  }
1433
-
1434
- // src/mouse-helper/index.ts
1435
- var CURSOR_PATH = "M 0 0 L 16 6 L 8 9.5 L 5 19 Z";
1436
- var INSTALLED_FLAG = /* @__PURE__ */ Symbol.for("@humanjs/playwright:mouse-helper:installed");
1437
- async function installMouseHelper(target, options = {}) {
1438
- const tagged = target;
1439
- if (tagged[INSTALLED_FLAG]) return;
1440
- tagged[INSTALLED_FLAG] = true;
1441
- const config = {
1442
- color: options.color ?? "#f5a55c",
1443
- stroke: "#020203",
1444
- size: options.size ?? 22,
1445
- showClicks: options.showClicks ?? true,
1446
- haloOpacity: options.haloOpacity ?? 0.18,
1447
- path: CURSOR_PATH
1448
- };
1449
- await target.addInitScript(installScript, config);
1450
- const attachPageHooks = (page) => {
1451
- page.on("domcontentloaded", () => {
1452
- page.evaluate(installScript, config).catch(() => void 0);
1453
- });
1454
- };
1455
- const pages = "pages" in target ? target.pages() : [target];
1456
- for (const page of pages) attachPageHooks(page);
1457
- if ("on" in target && "newPage" in target) {
1458
- target.on("page", attachPageHooks);
1459
- }
1460
- await Promise.all(
1461
- pages.map((page) => page.evaluate(installScript, config).catch(() => void 0))
1462
- );
1463
- }
1464
- function installScript(config) {
1465
- if (document.querySelector("[data-humanjs-cursor]")) return;
1466
- const attach = () => {
1467
- const cursor = document.createElement("div");
1468
- cursor.setAttribute("aria-hidden", "true");
1469
- cursor.setAttribute("data-humanjs-cursor", "true");
1470
- cursor.style.cssText = [
1471
- "position: fixed",
1472
- "left: 0",
1473
- "top: 0",
1474
- `width: ${config.size}px`,
1475
- `height: ${config.size + 4}px`,
1476
- "pointer-events: none",
1477
- "z-index: 2147483647",
1478
- // Start visible at (0, 0) so the cursor is on screen from the moment
1479
- // the page loads — without this the helper looks like nothing happened
1480
- // until the first mousemove arrives.
1481
- "opacity: 1",
1482
- "transform: translate(0px, 0px)",
1483
- // CSS interpolates between successive `mousemove` updates so the
1484
- // cursor reads as continuous motion instead of discrete hops. Slightly
1485
- // longer than the path-walker's typical step interval (~30–80ms) so
1486
- // each tween is still settling when the next move lands → no pauses.
1487
- "transition: transform 110ms ease-out, opacity 0.18s ease-out",
1488
- "will-change: transform"
1489
- ].join("; ");
1490
- const haloRadius = Math.round(config.size * 0.6);
1491
- cursor.innerHTML = `
1492
- <svg width="${config.size}" height="${config.size + 4}" viewBox="0 0 22 24" style="overflow: visible;">
1493
- <circle cx="0" cy="0" r="${haloRadius}" fill="${config.color}" opacity="${config.haloOpacity}" />
1494
- <path d="${config.path}" fill="${config.color}" stroke="${config.stroke}" stroke-width="0.7" stroke-linejoin="round" />
1495
- </svg>
1496
- `;
1497
- document.body.appendChild(cursor);
1498
- let lastX = 0;
1499
- let lastY = 0;
1500
- const onMove = (e) => {
1501
- lastX = e.clientX;
1502
- lastY = e.clientY;
1503
- cursor.style.transform = `translate(${lastX}px, ${lastY}px)`;
1504
- cursor.style.opacity = "1";
1505
- };
1506
- window.addEventListener("mousemove", onMove, { capture: true, passive: true });
1507
- document.addEventListener("mousemove", onMove, { capture: true, passive: true });
1508
- document.addEventListener(
1509
- "mouseleave",
1510
- () => {
1511
- cursor.style.opacity = "0";
1512
- },
1513
- { capture: true, passive: true }
1514
- );
1515
- if (config.showClicks) {
1516
- const styleEl = document.createElement("style");
1517
- 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; } }";
1518
- document.head.appendChild(styleEl);
1519
- window.addEventListener(
1520
- "mousedown",
1521
- () => {
1522
- const ripple = document.createElement("div");
1523
- ripple.style.cssText = [
1524
- "position: fixed",
1525
- `left: ${lastX}px`,
1526
- `top: ${lastY}px`,
1527
- "width: 28px",
1528
- "height: 28px",
1529
- "border-radius: 50%",
1530
- `border: 1.5px solid ${config.color}`,
1531
- "pointer-events: none",
1532
- "z-index: 2147483646",
1533
- "animation: humanjs-ripple 0.45s ease-out forwards"
1534
- ].join("; ");
1535
- document.body.appendChild(ripple);
1536
- window.setTimeout(() => ripple.remove(), 500);
1537
- },
1538
- { capture: true, passive: true }
1539
- );
1540
- }
1541
- };
1542
- if (document.body) attach();
1543
- else document.addEventListener("DOMContentLoaded", attach, { once: true });
1544
- }
1545
-
1546
- // src/index.ts
1547
1605
  async function createHuman(page, options = {}) {
1548
1606
  const personality = resolvePersonality(options.personality ?? "careful");
1549
1607
  const rng = createRng(options.seed);
@@ -1553,6 +1611,9 @@ async function createHuman(page, options = {}) {
1553
1611
  for (const plugin of plugins) {
1554
1612
  await plugin.install?.(context);
1555
1613
  }
1614
+ if (options.cursor !== false && typeof page.addInitScript === "function") {
1615
+ await installMouseHelper(page, typeof options.cursor === "object" ? options.cursor : {});
1616
+ }
1556
1617
  let hasRecorded = false;
1557
1618
  let activeRecordingEvents = null;
1558
1619
  let activeRecordingStartMs = 0;
@@ -1735,6 +1796,27 @@ async function createHuman(page, options = {}) {
1735
1796
  () => executeSelectOption(target, values, mouseCtx())
1736
1797
  );
1737
1798
  },
1799
+ async selectText(target, options2) {
1800
+ const text = options2?.text;
1801
+ await performAction(
1802
+ {
1803
+ type: "selectText",
1804
+ params: { target: describeMouseTarget(target), ...text !== void 0 ? { text } : {} }
1805
+ },
1806
+ async () => {
1807
+ if (speed !== "instant") {
1808
+ await executeMove(target, mouseCtx());
1809
+ }
1810
+ const locator = typeof target === "string" ? page.locator(target) : target;
1811
+ if (text === void 0) {
1812
+ await locator.selectText();
1813
+ return;
1814
+ }
1815
+ const found = await locator.evaluate(selectSubstringInElement, text);
1816
+ if (!found) await locator.selectText();
1817
+ }
1818
+ );
1819
+ },
1738
1820
  async upload(target, files) {
1739
1821
  await performAction(
1740
1822
  {
@@ -1910,6 +1992,6 @@ function describeReadTarget(target) {
1910
1992
  return target.toString?.() ?? "locator";
1911
1993
  }
1912
1994
 
1913
- export { Recording, createHuman, installMouseHelper };
1914
- //# sourceMappingURL=chunk-CCZEDNOF.js.map
1915
- //# sourceMappingURL=chunk-CCZEDNOF.js.map
1995
+ export { Recording, createHuman, generateHumanJS, generatePlaywrightTest, installMouseHelper };
1996
+ //# sourceMappingURL=chunk-3TXDODCO.js.map
1997
+ //# sourceMappingURL=chunk-3TXDODCO.js.map