@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
package/README.md
CHANGED
|
@@ -380,6 +380,26 @@ Generated tests are built to *be tests*: they run `speed: process.env.CI ? 'inst
|
|
|
380
380
|
|
|
381
381
|
Two more options: `{ steps: true }` groups the actions into `test.step(...)` blocks (a new step per navigation) for collapsible sections in the HTML report and trace; `{ baseUrl: true }` rewrites same-origin `goto`s to relative paths and adds a note to set `use.baseURL` in your `playwright.config.ts` — so the same test runs against local / staging / prod.
|
|
382
382
|
|
|
383
|
+
### Replaying a timeline
|
|
384
|
+
|
|
385
|
+
`replayTimeline(page, timeline, options?)` runs a recorded `Timeline` against a live page, driving it through the same humanized primitives the exported test uses — without spawning a test runner. It runs each event in order, reports per-step status via `onStep`, and **stops at the first failure** (like a real test).
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { chromium, replayTimeline } from '@humanjs/playwright';
|
|
389
|
+
|
|
390
|
+
const page = await (await chromium.launch({ headless: false })).newPage();
|
|
391
|
+
const result = await replayTimeline(page, recording.timeline, {
|
|
392
|
+
personality: 'careful',
|
|
393
|
+
onStep: ({ index, type, status }) => console.log(`${index} ${type}: ${status}`),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
result.status; // 'pass' | 'fail'
|
|
397
|
+
result.failedIndex; // index of the failing step, when failed
|
|
398
|
+
result.steps; // per-step { index, type, status, error? }
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`assert` events are evaluated with plain Playwright APIs — there's no `@playwright/test` dependency — so they approximate `expect` (`toBeVisible` via `waitFor`, `toHaveText` via normalized text equality, `toHaveURL` via `page.url()`) without its auto-retry on text/url. Pass an `AbortSignal` via `{ signal }` to cancel a run; it rejects with an `AbortError`. The cursor is on and `speed` is `'human'` by default so the replay is watchable. You own `page`'s lifecycle — `replayTimeline` doesn't close it. This is the engine behind the `@humanjs/generator` dashboard's **Run** button.
|
|
402
|
+
|
|
383
403
|
By default the actual typed/pasted text is captured into the timeline (and the exported code). Values typed into `input[type="password"]` are always masked; set `captureInputs: false` to record none — exports then emit empty-string placeholders:
|
|
384
404
|
|
|
385
405
|
```ts
|
|
@@ -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;
|
|
@@ -788,8 +945,17 @@ async function detectKindFromTag(locator) {
|
|
|
788
945
|
return void 0;
|
|
789
946
|
}
|
|
790
947
|
|
|
791
|
-
// src/recording/
|
|
948
|
+
// src/recording/targets.ts
|
|
792
949
|
var POINT_RE = /^point\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)$/;
|
|
950
|
+
function parsePointTarget(desc) {
|
|
951
|
+
const match = String(desc ?? "").match(POINT_RE);
|
|
952
|
+
return match ? { x: Number(match[1]), y: Number(match[2]) } : null;
|
|
953
|
+
}
|
|
954
|
+
function resolveMouseTarget(desc) {
|
|
955
|
+
return parsePointTarget(desc) ?? String(desc ?? "");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/recording/codegen.ts
|
|
793
959
|
var POINT_COMMENT = " // raw coordinate \u2014 replace with a locator for a stable selector";
|
|
794
960
|
var UNCAPTURED_COMMENT = " // input not captured (masked or captureInputs disabled) \u2014 fill in (e.g. process.env.X)";
|
|
795
961
|
function q(value) {
|
|
@@ -855,6 +1021,13 @@ function emitAction(e, opts = {}) {
|
|
|
855
1021
|
const { code } = targetArg(p.target);
|
|
856
1022
|
return ` await human.${e.type}(${code});`;
|
|
857
1023
|
}
|
|
1024
|
+
case "selectText": {
|
|
1025
|
+
const { code } = targetArg(p.target);
|
|
1026
|
+
if (typeof p.text === "string" && p.text.length > 0) {
|
|
1027
|
+
return ` await human.selectText(${code}, { text: ${q(p.text)} });`;
|
|
1028
|
+
}
|
|
1029
|
+
return ` await human.selectText(${code});`;
|
|
1030
|
+
}
|
|
858
1031
|
case "selectOption": {
|
|
859
1032
|
const { code } = targetArg(p.target);
|
|
860
1033
|
return ` await human.selectOption(${code}, ${serializeSelectValues(p.values)});`;
|
|
@@ -904,6 +1077,14 @@ function emitAction(e, opts = {}) {
|
|
|
904
1077
|
return " await human.goBack();";
|
|
905
1078
|
case "goForward":
|
|
906
1079
|
return " await human.goForward();";
|
|
1080
|
+
case "assert": {
|
|
1081
|
+
const kind = String(p.kind ?? "visible");
|
|
1082
|
+
if (kind === "url") return ` await expect(page).toHaveURL(${q(p.value)});`;
|
|
1083
|
+
const { code } = targetArg(p.target);
|
|
1084
|
+
if (kind === "text")
|
|
1085
|
+
return ` await expect(page.locator(${code})).toHaveText(${q(p.value)});`;
|
|
1086
|
+
return ` await expect(page.locator(${code})).toBeVisible();`;
|
|
1087
|
+
}
|
|
907
1088
|
default:
|
|
908
1089
|
return ` // unsupported action: ${e.type}`;
|
|
909
1090
|
}
|
|
@@ -913,7 +1094,7 @@ function needsSleepImport(timeline) {
|
|
|
913
1094
|
}
|
|
914
1095
|
function generateHumanJS(timeline) {
|
|
915
1096
|
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");
|
|
1097
|
+
const body = timeline.events.filter((e) => e.type !== "assert").map((e) => emitAction(e)).join("\n");
|
|
917
1098
|
return `${imports}
|
|
918
1099
|
|
|
919
1100
|
async function main() {
|
|
@@ -1010,6 +1191,150 @@ ${todo}
|
|
|
1010
1191
|
});
|
|
1011
1192
|
`;
|
|
1012
1193
|
}
|
|
1194
|
+
function abortError() {
|
|
1195
|
+
const error = new Error("Replay aborted");
|
|
1196
|
+
error.name = "AbortError";
|
|
1197
|
+
return error;
|
|
1198
|
+
}
|
|
1199
|
+
async function replayTimeline(page, timeline, options = {}) {
|
|
1200
|
+
const events = Array.isArray(timeline) ? timeline : timeline.events;
|
|
1201
|
+
const { onStep, signal } = options;
|
|
1202
|
+
if (signal?.aborted) throw abortError();
|
|
1203
|
+
const human = await createHuman(page, {
|
|
1204
|
+
personality: options.personality ?? "careful",
|
|
1205
|
+
speed: options.speed ?? "human",
|
|
1206
|
+
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
1207
|
+
...options.cursor !== void 0 ? { cursor: options.cursor } : {}
|
|
1208
|
+
});
|
|
1209
|
+
const startedAt = Date.now();
|
|
1210
|
+
const steps = [];
|
|
1211
|
+
for (const [index, event] of events.entries()) {
|
|
1212
|
+
if (signal?.aborted) throw abortError();
|
|
1213
|
+
onStep?.({ index, type: event.type, status: "running" });
|
|
1214
|
+
try {
|
|
1215
|
+
await runEvent(human, page, event);
|
|
1216
|
+
} catch (cause) {
|
|
1217
|
+
const error = cause instanceof Error ? cause.message : String(cause);
|
|
1218
|
+
steps.push({ index, type: event.type, status: "fail", error });
|
|
1219
|
+
onStep?.({ index, type: event.type, status: "fail", error });
|
|
1220
|
+
return { status: "fail", steps, failedIndex: index, durationMs: Date.now() - startedAt };
|
|
1221
|
+
}
|
|
1222
|
+
steps.push({ index, type: event.type, status: "pass" });
|
|
1223
|
+
onStep?.({ index, type: event.type, status: "pass" });
|
|
1224
|
+
}
|
|
1225
|
+
return { status: "pass", steps, durationMs: Date.now() - startedAt };
|
|
1226
|
+
}
|
|
1227
|
+
function parseScrollTarget(target) {
|
|
1228
|
+
const value = String(target ?? "natural");
|
|
1229
|
+
const by = value.match(/^by:(-?\d+(?:\.\d+)?)$/);
|
|
1230
|
+
if (by) return { by: Number(by[1]) };
|
|
1231
|
+
const to = value.match(/^to:(-?\d+(?:\.\d+)?)$/);
|
|
1232
|
+
if (to) return { to: Number(to[1]) };
|
|
1233
|
+
return value;
|
|
1234
|
+
}
|
|
1235
|
+
var normalizeText = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
1236
|
+
async function runAssert(page, params) {
|
|
1237
|
+
const kind = String(params.kind ?? "visible");
|
|
1238
|
+
if (kind === "url") {
|
|
1239
|
+
const actual = page.url();
|
|
1240
|
+
const expected = String(params.value ?? "");
|
|
1241
|
+
if (actual !== expected) {
|
|
1242
|
+
throw new Error(`expected URL ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const locator = page.locator(String(params.target ?? "")).first();
|
|
1247
|
+
await locator.waitFor({ state: "visible" });
|
|
1248
|
+
if (kind === "text") {
|
|
1249
|
+
const actual = normalizeText(await locator.textContent());
|
|
1250
|
+
const expected = normalizeText(String(params.value ?? ""));
|
|
1251
|
+
if (actual !== expected) {
|
|
1252
|
+
throw new Error(`expected text ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function runEvent(human, page, event) {
|
|
1257
|
+
const p = event.params;
|
|
1258
|
+
switch (event.type) {
|
|
1259
|
+
case "goto":
|
|
1260
|
+
await human.goto(String(p.url ?? ""));
|
|
1261
|
+
return;
|
|
1262
|
+
case "click":
|
|
1263
|
+
await human.click(resolveMouseTarget(p.target));
|
|
1264
|
+
return;
|
|
1265
|
+
case "rightClick":
|
|
1266
|
+
await human.rightClick(resolveMouseTarget(p.target));
|
|
1267
|
+
return;
|
|
1268
|
+
case "doubleClick":
|
|
1269
|
+
await human.doubleClick(resolveMouseTarget(p.target));
|
|
1270
|
+
return;
|
|
1271
|
+
case "move":
|
|
1272
|
+
await human.move(resolveMouseTarget(p.target));
|
|
1273
|
+
return;
|
|
1274
|
+
case "hover":
|
|
1275
|
+
await human.hover(String(p.target ?? ""));
|
|
1276
|
+
return;
|
|
1277
|
+
case "drag":
|
|
1278
|
+
await human.drag(resolveMouseTarget(p.from), resolveMouseTarget(p.to));
|
|
1279
|
+
return;
|
|
1280
|
+
case "type":
|
|
1281
|
+
await human.type(String(p.target ?? ""), event.inputValue ?? "");
|
|
1282
|
+
return;
|
|
1283
|
+
case "paste":
|
|
1284
|
+
await human.paste(String(p.target ?? ""), event.inputValue ?? "");
|
|
1285
|
+
return;
|
|
1286
|
+
case "clear":
|
|
1287
|
+
await human.clear(String(p.target ?? ""));
|
|
1288
|
+
return;
|
|
1289
|
+
case "check":
|
|
1290
|
+
await human.check(String(p.target ?? ""));
|
|
1291
|
+
return;
|
|
1292
|
+
case "uncheck":
|
|
1293
|
+
await human.uncheck(String(p.target ?? ""));
|
|
1294
|
+
return;
|
|
1295
|
+
case "selectText":
|
|
1296
|
+
await human.selectText(
|
|
1297
|
+
String(p.target ?? ""),
|
|
1298
|
+
typeof p.text === "string" ? { text: p.text } : void 0
|
|
1299
|
+
);
|
|
1300
|
+
return;
|
|
1301
|
+
case "selectOption":
|
|
1302
|
+
await human.selectOption(String(p.target ?? ""), p.values);
|
|
1303
|
+
return;
|
|
1304
|
+
case "upload":
|
|
1305
|
+
await human.upload(String(p.target ?? ""), p.files);
|
|
1306
|
+
return;
|
|
1307
|
+
case "press":
|
|
1308
|
+
await human.press(String(p.key ?? ""));
|
|
1309
|
+
return;
|
|
1310
|
+
case "scroll":
|
|
1311
|
+
await human.scroll(parseScrollTarget(p.target));
|
|
1312
|
+
return;
|
|
1313
|
+
case "read": {
|
|
1314
|
+
const target = String(p.target ?? "");
|
|
1315
|
+
if (/^\d+ words$/.test(target) || /^text:\d+ chars$/.test(target)) return;
|
|
1316
|
+
await human.read(target);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
case "sleep":
|
|
1320
|
+
await core.sleep(Number(p.ms) || 0);
|
|
1321
|
+
return;
|
|
1322
|
+
case "reload":
|
|
1323
|
+
await human.reload();
|
|
1324
|
+
return;
|
|
1325
|
+
case "goBack":
|
|
1326
|
+
await human.goBack();
|
|
1327
|
+
return;
|
|
1328
|
+
case "goForward":
|
|
1329
|
+
await human.goForward();
|
|
1330
|
+
return;
|
|
1331
|
+
case "assert":
|
|
1332
|
+
await runAssert(page, p);
|
|
1333
|
+
return;
|
|
1334
|
+
default:
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1013
1338
|
|
|
1014
1339
|
// src/recording/index.ts
|
|
1015
1340
|
var pendingFrameCleanups = /* @__PURE__ */ new Set();
|
|
@@ -1435,120 +1760,6 @@ async function startCapture(page, options = {}) {
|
|
|
1435
1760
|
}
|
|
1436
1761
|
};
|
|
1437
1762
|
}
|
|
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
1763
|
async function createHuman(page, options = {}) {
|
|
1553
1764
|
const personality = core.resolvePersonality(options.personality ?? "careful");
|
|
1554
1765
|
const rng = core.createRng(options.seed);
|
|
@@ -1558,6 +1769,9 @@ async function createHuman(page, options = {}) {
|
|
|
1558
1769
|
for (const plugin of plugins) {
|
|
1559
1770
|
await plugin.install?.(context);
|
|
1560
1771
|
}
|
|
1772
|
+
if (options.cursor !== false && typeof page.addInitScript === "function") {
|
|
1773
|
+
await installMouseHelper(page, typeof options.cursor === "object" ? options.cursor : {});
|
|
1774
|
+
}
|
|
1561
1775
|
let hasRecorded = false;
|
|
1562
1776
|
let activeRecordingEvents = null;
|
|
1563
1777
|
let activeRecordingStartMs = 0;
|
|
@@ -1740,6 +1954,27 @@ async function createHuman(page, options = {}) {
|
|
|
1740
1954
|
() => executeSelectOption(target, values, mouseCtx())
|
|
1741
1955
|
);
|
|
1742
1956
|
},
|
|
1957
|
+
async selectText(target, options2) {
|
|
1958
|
+
const text = options2?.text;
|
|
1959
|
+
await performAction(
|
|
1960
|
+
{
|
|
1961
|
+
type: "selectText",
|
|
1962
|
+
params: { target: describeMouseTarget(target), ...text !== void 0 ? { text } : {} }
|
|
1963
|
+
},
|
|
1964
|
+
async () => {
|
|
1965
|
+
if (speed !== "instant") {
|
|
1966
|
+
await executeMove(target, mouseCtx());
|
|
1967
|
+
}
|
|
1968
|
+
const locator = typeof target === "string" ? page.locator(target) : target;
|
|
1969
|
+
if (text === void 0) {
|
|
1970
|
+
await locator.selectText();
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const found = await locator.evaluate(selectSubstringInElement, text);
|
|
1974
|
+
if (!found) await locator.selectText();
|
|
1975
|
+
}
|
|
1976
|
+
);
|
|
1977
|
+
},
|
|
1743
1978
|
async upload(target, files) {
|
|
1744
1979
|
await performAction(
|
|
1745
1980
|
{
|
|
@@ -1993,6 +2228,9 @@ Object.defineProperty(exports, "webkit", {
|
|
|
1993
2228
|
});
|
|
1994
2229
|
exports.Recording = Recording;
|
|
1995
2230
|
exports.createHuman = createHuman;
|
|
2231
|
+
exports.generateHumanJS = generateHumanJS;
|
|
2232
|
+
exports.generatePlaywrightTest = generatePlaywrightTest;
|
|
1996
2233
|
exports.installMouseHelper = installMouseHelper;
|
|
1997
|
-
|
|
1998
|
-
//# sourceMappingURL=chunk-
|
|
2234
|
+
exports.replayTimeline = replayTimeline;
|
|
2235
|
+
//# sourceMappingURL=chunk-665R4N7R.cjs.map
|
|
2236
|
+
//# sourceMappingURL=chunk-665R4N7R.cjs.map
|