@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.
@@ -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;
@@ -855,6 +1012,13 @@ function emitAction(e, opts = {}) {
855
1012
  const { code } = targetArg(p.target);
856
1013
  return ` await human.${e.type}(${code});`;
857
1014
  }
1015
+ case "selectText": {
1016
+ const { code } = targetArg(p.target);
1017
+ if (typeof p.text === "string" && p.text.length > 0) {
1018
+ return ` await human.selectText(${code}, { text: ${q(p.text)} });`;
1019
+ }
1020
+ return ` await human.selectText(${code});`;
1021
+ }
858
1022
  case "selectOption": {
859
1023
  const { code } = targetArg(p.target);
860
1024
  return ` await human.selectOption(${code}, ${serializeSelectValues(p.values)});`;
@@ -904,6 +1068,14 @@ function emitAction(e, opts = {}) {
904
1068
  return " await human.goBack();";
905
1069
  case "goForward":
906
1070
  return " await human.goForward();";
1071
+ case "assert": {
1072
+ const kind = String(p.kind ?? "visible");
1073
+ if (kind === "url") return ` await expect(page).toHaveURL(${q(p.value)});`;
1074
+ const { code } = targetArg(p.target);
1075
+ if (kind === "text")
1076
+ return ` await expect(page.locator(${code})).toHaveText(${q(p.value)});`;
1077
+ return ` await expect(page.locator(${code})).toBeVisible();`;
1078
+ }
907
1079
  default:
908
1080
  return ` // unsupported action: ${e.type}`;
909
1081
  }
@@ -913,7 +1085,7 @@ function needsSleepImport(timeline) {
913
1085
  }
914
1086
  function generateHumanJS(timeline) {
915
1087
  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");
1088
+ const body = timeline.events.filter((e) => e.type !== "assert").map((e) => emitAction(e)).join("\n");
917
1089
  return `${imports}
918
1090
 
919
1091
  async function main() {
@@ -1435,120 +1607,6 @@ async function startCapture(page, options = {}) {
1435
1607
  }
1436
1608
  };
1437
1609
  }
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
1610
  async function createHuman(page, options = {}) {
1553
1611
  const personality = core.resolvePersonality(options.personality ?? "careful");
1554
1612
  const rng = core.createRng(options.seed);
@@ -1558,6 +1616,9 @@ async function createHuman(page, options = {}) {
1558
1616
  for (const plugin of plugins) {
1559
1617
  await plugin.install?.(context);
1560
1618
  }
1619
+ if (options.cursor !== false && typeof page.addInitScript === "function") {
1620
+ await installMouseHelper(page, typeof options.cursor === "object" ? options.cursor : {});
1621
+ }
1561
1622
  let hasRecorded = false;
1562
1623
  let activeRecordingEvents = null;
1563
1624
  let activeRecordingStartMs = 0;
@@ -1740,6 +1801,27 @@ async function createHuman(page, options = {}) {
1740
1801
  () => executeSelectOption(target, values, mouseCtx())
1741
1802
  );
1742
1803
  },
1804
+ async selectText(target, options2) {
1805
+ const text = options2?.text;
1806
+ await performAction(
1807
+ {
1808
+ type: "selectText",
1809
+ params: { target: describeMouseTarget(target), ...text !== void 0 ? { text } : {} }
1810
+ },
1811
+ async () => {
1812
+ if (speed !== "instant") {
1813
+ await executeMove(target, mouseCtx());
1814
+ }
1815
+ const locator = typeof target === "string" ? page.locator(target) : target;
1816
+ if (text === void 0) {
1817
+ await locator.selectText();
1818
+ return;
1819
+ }
1820
+ const found = await locator.evaluate(selectSubstringInElement, text);
1821
+ if (!found) await locator.selectText();
1822
+ }
1823
+ );
1824
+ },
1743
1825
  async upload(target, files) {
1744
1826
  await performAction(
1745
1827
  {
@@ -1993,6 +2075,8 @@ Object.defineProperty(exports, "webkit", {
1993
2075
  });
1994
2076
  exports.Recording = Recording;
1995
2077
  exports.createHuman = createHuman;
2078
+ exports.generateHumanJS = generateHumanJS;
2079
+ exports.generatePlaywrightTest = generatePlaywrightTest;
1996
2080
  exports.installMouseHelper = installMouseHelper;
1997
- //# sourceMappingURL=chunk-RCMSDC3N.cjs.map
1998
- //# sourceMappingURL=chunk-RCMSDC3N.cjs.map
2081
+ //# sourceMappingURL=chunk-3X36PFTS.cjs.map
2082
+ //# sourceMappingURL=chunk-3X36PFTS.cjs.map