@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
|
@@ -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-
|
|
1998
|
-
//# sourceMappingURL=chunk-
|
|
2081
|
+
//# sourceMappingURL=chunk-3X36PFTS.cjs.map
|
|
2082
|
+
//# sourceMappingURL=chunk-3X36PFTS.cjs.map
|