@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 +20 -0
- package/dist/{chunk-RCMSDC3N.cjs → chunk-665R4N7R.cjs} +356 -118
- package/dist/chunk-665R4N7R.cjs.map +1 -0
- package/dist/{chunk-CCZEDNOF.js → chunk-I2PQGZU7.js} +355 -120
- package/dist/chunk-I2PQGZU7.js.map +1 -0
- package/dist/index.cjs +35 -23
- package/dist/index.d.cts +114 -14
- package/dist/index.d.ts +114 -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sleep as sleep$1, resolvePersonality, createRng, planScroll, countWords, computeReadingDwellMs, planReadingScan, planTypeKeystrokes, bezierPath, humanizePath } from '@humanjs/core';
|
|
2
2
|
export { applyMicroJitter, applyVelocityProfile, bezierPath, blend, careful, computeReadingDwellMs, countWords, createRng, distracted, fast, humanizePath, planScroll, planTypeKeystrokes, precise, resolvePersonality, sleep } from '@humanjs/core';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { rmSync } from 'fs';
|
|
@@ -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;
|
|
@@ -783,8 +940,17 @@ async function detectKindFromTag(locator) {
|
|
|
783
940
|
return void 0;
|
|
784
941
|
}
|
|
785
942
|
|
|
786
|
-
// src/recording/
|
|
943
|
+
// src/recording/targets.ts
|
|
787
944
|
var POINT_RE = /^point\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)$/;
|
|
945
|
+
function parsePointTarget(desc) {
|
|
946
|
+
const match = String(desc ?? "").match(POINT_RE);
|
|
947
|
+
return match ? { x: Number(match[1]), y: Number(match[2]) } : null;
|
|
948
|
+
}
|
|
949
|
+
function resolveMouseTarget(desc) {
|
|
950
|
+
return parsePointTarget(desc) ?? String(desc ?? "");
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/recording/codegen.ts
|
|
788
954
|
var POINT_COMMENT = " // raw coordinate \u2014 replace with a locator for a stable selector";
|
|
789
955
|
var UNCAPTURED_COMMENT = " // input not captured (masked or captureInputs disabled) \u2014 fill in (e.g. process.env.X)";
|
|
790
956
|
function q(value) {
|
|
@@ -850,6 +1016,13 @@ function emitAction(e, opts = {}) {
|
|
|
850
1016
|
const { code } = targetArg(p.target);
|
|
851
1017
|
return ` await human.${e.type}(${code});`;
|
|
852
1018
|
}
|
|
1019
|
+
case "selectText": {
|
|
1020
|
+
const { code } = targetArg(p.target);
|
|
1021
|
+
if (typeof p.text === "string" && p.text.length > 0) {
|
|
1022
|
+
return ` await human.selectText(${code}, { text: ${q(p.text)} });`;
|
|
1023
|
+
}
|
|
1024
|
+
return ` await human.selectText(${code});`;
|
|
1025
|
+
}
|
|
853
1026
|
case "selectOption": {
|
|
854
1027
|
const { code } = targetArg(p.target);
|
|
855
1028
|
return ` await human.selectOption(${code}, ${serializeSelectValues(p.values)});`;
|
|
@@ -899,6 +1072,14 @@ function emitAction(e, opts = {}) {
|
|
|
899
1072
|
return " await human.goBack();";
|
|
900
1073
|
case "goForward":
|
|
901
1074
|
return " await human.goForward();";
|
|
1075
|
+
case "assert": {
|
|
1076
|
+
const kind = String(p.kind ?? "visible");
|
|
1077
|
+
if (kind === "url") return ` await expect(page).toHaveURL(${q(p.value)});`;
|
|
1078
|
+
const { code } = targetArg(p.target);
|
|
1079
|
+
if (kind === "text")
|
|
1080
|
+
return ` await expect(page.locator(${code})).toHaveText(${q(p.value)});`;
|
|
1081
|
+
return ` await expect(page.locator(${code})).toBeVisible();`;
|
|
1082
|
+
}
|
|
902
1083
|
default:
|
|
903
1084
|
return ` // unsupported action: ${e.type}`;
|
|
904
1085
|
}
|
|
@@ -908,7 +1089,7 @@ function needsSleepImport(timeline) {
|
|
|
908
1089
|
}
|
|
909
1090
|
function generateHumanJS(timeline) {
|
|
910
1091
|
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");
|
|
1092
|
+
const body = timeline.events.filter((e) => e.type !== "assert").map((e) => emitAction(e)).join("\n");
|
|
912
1093
|
return `${imports}
|
|
913
1094
|
|
|
914
1095
|
async function main() {
|
|
@@ -1005,6 +1186,150 @@ ${todo}
|
|
|
1005
1186
|
});
|
|
1006
1187
|
`;
|
|
1007
1188
|
}
|
|
1189
|
+
function abortError() {
|
|
1190
|
+
const error = new Error("Replay aborted");
|
|
1191
|
+
error.name = "AbortError";
|
|
1192
|
+
return error;
|
|
1193
|
+
}
|
|
1194
|
+
async function replayTimeline(page, timeline, options = {}) {
|
|
1195
|
+
const events = Array.isArray(timeline) ? timeline : timeline.events;
|
|
1196
|
+
const { onStep, signal } = options;
|
|
1197
|
+
if (signal?.aborted) throw abortError();
|
|
1198
|
+
const human = await createHuman(page, {
|
|
1199
|
+
personality: options.personality ?? "careful",
|
|
1200
|
+
speed: options.speed ?? "human",
|
|
1201
|
+
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
1202
|
+
...options.cursor !== void 0 ? { cursor: options.cursor } : {}
|
|
1203
|
+
});
|
|
1204
|
+
const startedAt = Date.now();
|
|
1205
|
+
const steps = [];
|
|
1206
|
+
for (const [index, event] of events.entries()) {
|
|
1207
|
+
if (signal?.aborted) throw abortError();
|
|
1208
|
+
onStep?.({ index, type: event.type, status: "running" });
|
|
1209
|
+
try {
|
|
1210
|
+
await runEvent(human, page, event);
|
|
1211
|
+
} catch (cause) {
|
|
1212
|
+
const error = cause instanceof Error ? cause.message : String(cause);
|
|
1213
|
+
steps.push({ index, type: event.type, status: "fail", error });
|
|
1214
|
+
onStep?.({ index, type: event.type, status: "fail", error });
|
|
1215
|
+
return { status: "fail", steps, failedIndex: index, durationMs: Date.now() - startedAt };
|
|
1216
|
+
}
|
|
1217
|
+
steps.push({ index, type: event.type, status: "pass" });
|
|
1218
|
+
onStep?.({ index, type: event.type, status: "pass" });
|
|
1219
|
+
}
|
|
1220
|
+
return { status: "pass", steps, durationMs: Date.now() - startedAt };
|
|
1221
|
+
}
|
|
1222
|
+
function parseScrollTarget(target) {
|
|
1223
|
+
const value = String(target ?? "natural");
|
|
1224
|
+
const by = value.match(/^by:(-?\d+(?:\.\d+)?)$/);
|
|
1225
|
+
if (by) return { by: Number(by[1]) };
|
|
1226
|
+
const to = value.match(/^to:(-?\d+(?:\.\d+)?)$/);
|
|
1227
|
+
if (to) return { to: Number(to[1]) };
|
|
1228
|
+
return value;
|
|
1229
|
+
}
|
|
1230
|
+
var normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
1231
|
+
async function runAssert(page, params) {
|
|
1232
|
+
const kind = String(params.kind ?? "visible");
|
|
1233
|
+
if (kind === "url") {
|
|
1234
|
+
const actual = page.url();
|
|
1235
|
+
const expected = String(params.value ?? "");
|
|
1236
|
+
if (actual !== expected) {
|
|
1237
|
+
throw new Error(`expected URL ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
1238
|
+
}
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const locator = page.locator(String(params.target ?? "")).first();
|
|
1242
|
+
await locator.waitFor({ state: "visible" });
|
|
1243
|
+
if (kind === "text") {
|
|
1244
|
+
const actual = normalizeText(await locator.textContent());
|
|
1245
|
+
const expected = normalizeText(String(params.value ?? ""));
|
|
1246
|
+
if (actual !== expected) {
|
|
1247
|
+
throw new Error(`expected text ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function runEvent(human, page, event) {
|
|
1252
|
+
const p = event.params;
|
|
1253
|
+
switch (event.type) {
|
|
1254
|
+
case "goto":
|
|
1255
|
+
await human.goto(String(p.url ?? ""));
|
|
1256
|
+
return;
|
|
1257
|
+
case "click":
|
|
1258
|
+
await human.click(resolveMouseTarget(p.target));
|
|
1259
|
+
return;
|
|
1260
|
+
case "rightClick":
|
|
1261
|
+
await human.rightClick(resolveMouseTarget(p.target));
|
|
1262
|
+
return;
|
|
1263
|
+
case "doubleClick":
|
|
1264
|
+
await human.doubleClick(resolveMouseTarget(p.target));
|
|
1265
|
+
return;
|
|
1266
|
+
case "move":
|
|
1267
|
+
await human.move(resolveMouseTarget(p.target));
|
|
1268
|
+
return;
|
|
1269
|
+
case "hover":
|
|
1270
|
+
await human.hover(String(p.target ?? ""));
|
|
1271
|
+
return;
|
|
1272
|
+
case "drag":
|
|
1273
|
+
await human.drag(resolveMouseTarget(p.from), resolveMouseTarget(p.to));
|
|
1274
|
+
return;
|
|
1275
|
+
case "type":
|
|
1276
|
+
await human.type(String(p.target ?? ""), event.inputValue ?? "");
|
|
1277
|
+
return;
|
|
1278
|
+
case "paste":
|
|
1279
|
+
await human.paste(String(p.target ?? ""), event.inputValue ?? "");
|
|
1280
|
+
return;
|
|
1281
|
+
case "clear":
|
|
1282
|
+
await human.clear(String(p.target ?? ""));
|
|
1283
|
+
return;
|
|
1284
|
+
case "check":
|
|
1285
|
+
await human.check(String(p.target ?? ""));
|
|
1286
|
+
return;
|
|
1287
|
+
case "uncheck":
|
|
1288
|
+
await human.uncheck(String(p.target ?? ""));
|
|
1289
|
+
return;
|
|
1290
|
+
case "selectText":
|
|
1291
|
+
await human.selectText(
|
|
1292
|
+
String(p.target ?? ""),
|
|
1293
|
+
typeof p.text === "string" ? { text: p.text } : void 0
|
|
1294
|
+
);
|
|
1295
|
+
return;
|
|
1296
|
+
case "selectOption":
|
|
1297
|
+
await human.selectOption(String(p.target ?? ""), p.values);
|
|
1298
|
+
return;
|
|
1299
|
+
case "upload":
|
|
1300
|
+
await human.upload(String(p.target ?? ""), p.files);
|
|
1301
|
+
return;
|
|
1302
|
+
case "press":
|
|
1303
|
+
await human.press(String(p.key ?? ""));
|
|
1304
|
+
return;
|
|
1305
|
+
case "scroll":
|
|
1306
|
+
await human.scroll(parseScrollTarget(p.target));
|
|
1307
|
+
return;
|
|
1308
|
+
case "read": {
|
|
1309
|
+
const target = String(p.target ?? "");
|
|
1310
|
+
if (/^\d+ words$/.test(target) || /^text:\d+ chars$/.test(target)) return;
|
|
1311
|
+
await human.read(target);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
case "sleep":
|
|
1315
|
+
await sleep$1(Number(p.ms) || 0);
|
|
1316
|
+
return;
|
|
1317
|
+
case "reload":
|
|
1318
|
+
await human.reload();
|
|
1319
|
+
return;
|
|
1320
|
+
case "goBack":
|
|
1321
|
+
await human.goBack();
|
|
1322
|
+
return;
|
|
1323
|
+
case "goForward":
|
|
1324
|
+
await human.goForward();
|
|
1325
|
+
return;
|
|
1326
|
+
case "assert":
|
|
1327
|
+
await runAssert(page, p);
|
|
1328
|
+
return;
|
|
1329
|
+
default:
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1008
1333
|
|
|
1009
1334
|
// src/recording/index.ts
|
|
1010
1335
|
var pendingFrameCleanups = /* @__PURE__ */ new Set();
|
|
@@ -1430,120 +1755,6 @@ async function startCapture(page, options = {}) {
|
|
|
1430
1755
|
}
|
|
1431
1756
|
};
|
|
1432
1757
|
}
|
|
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
1758
|
async function createHuman(page, options = {}) {
|
|
1548
1759
|
const personality = resolvePersonality(options.personality ?? "careful");
|
|
1549
1760
|
const rng = createRng(options.seed);
|
|
@@ -1553,6 +1764,9 @@ async function createHuman(page, options = {}) {
|
|
|
1553
1764
|
for (const plugin of plugins) {
|
|
1554
1765
|
await plugin.install?.(context);
|
|
1555
1766
|
}
|
|
1767
|
+
if (options.cursor !== false && typeof page.addInitScript === "function") {
|
|
1768
|
+
await installMouseHelper(page, typeof options.cursor === "object" ? options.cursor : {});
|
|
1769
|
+
}
|
|
1556
1770
|
let hasRecorded = false;
|
|
1557
1771
|
let activeRecordingEvents = null;
|
|
1558
1772
|
let activeRecordingStartMs = 0;
|
|
@@ -1735,6 +1949,27 @@ async function createHuman(page, options = {}) {
|
|
|
1735
1949
|
() => executeSelectOption(target, values, mouseCtx())
|
|
1736
1950
|
);
|
|
1737
1951
|
},
|
|
1952
|
+
async selectText(target, options2) {
|
|
1953
|
+
const text = options2?.text;
|
|
1954
|
+
await performAction(
|
|
1955
|
+
{
|
|
1956
|
+
type: "selectText",
|
|
1957
|
+
params: { target: describeMouseTarget(target), ...text !== void 0 ? { text } : {} }
|
|
1958
|
+
},
|
|
1959
|
+
async () => {
|
|
1960
|
+
if (speed !== "instant") {
|
|
1961
|
+
await executeMove(target, mouseCtx());
|
|
1962
|
+
}
|
|
1963
|
+
const locator = typeof target === "string" ? page.locator(target) : target;
|
|
1964
|
+
if (text === void 0) {
|
|
1965
|
+
await locator.selectText();
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const found = await locator.evaluate(selectSubstringInElement, text);
|
|
1969
|
+
if (!found) await locator.selectText();
|
|
1970
|
+
}
|
|
1971
|
+
);
|
|
1972
|
+
},
|
|
1738
1973
|
async upload(target, files) {
|
|
1739
1974
|
await performAction(
|
|
1740
1975
|
{
|
|
@@ -1910,6 +2145,6 @@ function describeReadTarget(target) {
|
|
|
1910
2145
|
return target.toString?.() ?? "locator";
|
|
1911
2146
|
}
|
|
1912
2147
|
|
|
1913
|
-
export { Recording, createHuman, installMouseHelper };
|
|
1914
|
-
//# sourceMappingURL=chunk-
|
|
1915
|
-
//# sourceMappingURL=chunk-
|
|
2148
|
+
export { Recording, createHuman, generateHumanJS, generatePlaywrightTest, installMouseHelper, replayTimeline };
|
|
2149
|
+
//# sourceMappingURL=chunk-I2PQGZU7.js.map
|
|
2150
|
+
//# sourceMappingURL=chunk-I2PQGZU7.js.map
|