@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.
- package/dist/{chunk-CCZEDNOF.js → chunk-3TXDODCO.js} +200 -118
- package/dist/chunk-3TXDODCO.js.map +1 -0
- package/dist/{chunk-RCMSDC3N.cjs → chunk-3X36PFTS.cjs} +201 -117
- package/dist/chunk-3X36PFTS.cjs.map +1 -0
- package/dist/index.cjs +31 -23
- package/dist/index.d.cts +63 -14
- package/dist/index.d.ts +63 -14
- package/dist/index.js +1 -1
- package/dist/test.cjs +5 -2
- package/dist/test.cjs.map +1 -1
- package/dist/test.js +4 -1
- package/dist/test.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-CCZEDNOF.js.map +0 -1
- package/dist/chunk-RCMSDC3N.cjs.map +0 -1
|
@@ -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-
|
|
1915
|
-
//# sourceMappingURL=chunk-
|
|
1995
|
+
export { Recording, createHuman, generateHumanJS, generatePlaywrightTest, installMouseHelper };
|
|
1996
|
+
//# sourceMappingURL=chunk-3TXDODCO.js.map
|
|
1997
|
+
//# sourceMappingURL=chunk-3TXDODCO.js.map
|