@skrillex1224/playwright-toolkit 2.1.18 → 2.1.20
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/index.cjs +193 -33
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +193 -33
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -562,6 +562,173 @@ var Humanize = {
|
|
|
562
562
|
throw error;
|
|
563
563
|
}
|
|
564
564
|
},
|
|
565
|
+
/**
|
|
566
|
+
* 渐进式滚动到元素可见(仅处理 Y 轴滚动)
|
|
567
|
+
* 使用鼠标滚轮模拟人类滚动,并返回 restore 用于回滚
|
|
568
|
+
*
|
|
569
|
+
* @param {import('playwright').Page} page
|
|
570
|
+
* @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
|
|
571
|
+
* @param {Object} [options]
|
|
572
|
+
* @param {number} [options.maxSteps=25] - 最大滚动步数
|
|
573
|
+
* @param {number} [options.minStep=80] - 单次滚动最小步长
|
|
574
|
+
* @param {number} [options.maxStep=220] - 单次滚动最大步长
|
|
575
|
+
*/
|
|
576
|
+
async humanScroll(page, target, options = {}) {
|
|
577
|
+
const cursor = $GetCursor(page);
|
|
578
|
+
const { maxSteps = 25, minStep = 80, maxStep = 220 } = options;
|
|
579
|
+
let element;
|
|
580
|
+
if (typeof target === "string") {
|
|
581
|
+
element = await page.$(target);
|
|
582
|
+
if (!element) {
|
|
583
|
+
return { element: null, didScroll: false, restore: async () => {
|
|
584
|
+
} };
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
element = target;
|
|
588
|
+
}
|
|
589
|
+
const isScrollable = (node) => {
|
|
590
|
+
if (!node || node === document.body) return false;
|
|
591
|
+
const style = window.getComputedStyle(node);
|
|
592
|
+
const overflowY = style.overflowY || style.overflow;
|
|
593
|
+
return (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && node.scrollHeight > node.clientHeight + 1;
|
|
594
|
+
};
|
|
595
|
+
const needsScroll = await element.evaluate((el) => {
|
|
596
|
+
const rect = el.getBoundingClientRect();
|
|
597
|
+
if (!rect || rect.width === 0 || rect.height === 0) return true;
|
|
598
|
+
const inViewport = rect.top >= 0 && rect.bottom <= window.innerHeight;
|
|
599
|
+
if (!inViewport) return true;
|
|
600
|
+
let current = el.parentElement;
|
|
601
|
+
while (current) {
|
|
602
|
+
if (isScrollable(current)) {
|
|
603
|
+
const crect = current.getBoundingClientRect();
|
|
604
|
+
if (rect.top < crect.top || rect.bottom > crect.bottom) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
current = current.parentElement;
|
|
609
|
+
}
|
|
610
|
+
return false;
|
|
611
|
+
});
|
|
612
|
+
if (!needsScroll) {
|
|
613
|
+
return { element, didScroll: false, restore: async () => {
|
|
614
|
+
} };
|
|
615
|
+
}
|
|
616
|
+
const scrollablesHandle = await element.evaluateHandle((el) => {
|
|
617
|
+
const scrollables2 = [];
|
|
618
|
+
let current = el.parentElement;
|
|
619
|
+
while (current) {
|
|
620
|
+
if (isScrollable(current)) scrollables2.push(current);
|
|
621
|
+
current = current.parentElement;
|
|
622
|
+
}
|
|
623
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
624
|
+
if (scrollingElement) scrollables2.push(scrollingElement);
|
|
625
|
+
return scrollables2;
|
|
626
|
+
});
|
|
627
|
+
const scrollables = [];
|
|
628
|
+
try {
|
|
629
|
+
const lengthHandle = await scrollablesHandle.getProperty("length");
|
|
630
|
+
const length = await lengthHandle.jsonValue();
|
|
631
|
+
await lengthHandle.dispose();
|
|
632
|
+
for (let i = 0; i < length; i++) {
|
|
633
|
+
const itemHandle = await scrollablesHandle.getProperty(String(i));
|
|
634
|
+
const itemEl = itemHandle.asElement();
|
|
635
|
+
if (itemEl) {
|
|
636
|
+
const top = await itemEl.evaluate((el) => el.scrollTop);
|
|
637
|
+
scrollables.push({ el: itemEl, top });
|
|
638
|
+
} else {
|
|
639
|
+
await itemHandle.dispose();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} finally {
|
|
643
|
+
await scrollablesHandle.dispose();
|
|
644
|
+
}
|
|
645
|
+
const pickTarget = async () => {
|
|
646
|
+
const handle = await element.evaluateHandle((el) => {
|
|
647
|
+
const rect = el.getBoundingClientRect();
|
|
648
|
+
if (!rect || rect.width === 0 || rect.height === 0) {
|
|
649
|
+
return { inView: false, container: null, direction: 0 };
|
|
650
|
+
}
|
|
651
|
+
const scrollables2 = [];
|
|
652
|
+
let current = el.parentElement;
|
|
653
|
+
while (current) {
|
|
654
|
+
if (isScrollable(current)) scrollables2.push(current);
|
|
655
|
+
current = current.parentElement;
|
|
656
|
+
}
|
|
657
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
658
|
+
if (scrollingElement) scrollables2.push(scrollingElement);
|
|
659
|
+
for (const container of scrollables2) {
|
|
660
|
+
const crect = container === scrollingElement ? { top: 0, bottom: window.innerHeight } : container.getBoundingClientRect();
|
|
661
|
+
if (rect.top < crect.top + 2) {
|
|
662
|
+
return { inView: false, container, direction: -1 };
|
|
663
|
+
}
|
|
664
|
+
if (rect.bottom > crect.bottom - 2) {
|
|
665
|
+
return { inView: false, container, direction: 1 };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { inView: true, container: null, direction: 0 };
|
|
669
|
+
});
|
|
670
|
+
const inViewHandle = await handle.getProperty("inView");
|
|
671
|
+
const inView = await inViewHandle.jsonValue();
|
|
672
|
+
await inViewHandle.dispose();
|
|
673
|
+
if (inView) {
|
|
674
|
+
await handle.dispose();
|
|
675
|
+
return { inView: true };
|
|
676
|
+
}
|
|
677
|
+
const directionHandle = await handle.getProperty("direction");
|
|
678
|
+
const direction = await directionHandle.jsonValue();
|
|
679
|
+
await directionHandle.dispose();
|
|
680
|
+
const containerHandle = (await handle.getProperty("container")).asElement();
|
|
681
|
+
await handle.dispose();
|
|
682
|
+
return { inView: false, direction, container: containerHandle };
|
|
683
|
+
};
|
|
684
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
685
|
+
const { inView, direction, container } = await pickTarget();
|
|
686
|
+
if (inView) break;
|
|
687
|
+
if (!container || !direction) break;
|
|
688
|
+
try {
|
|
689
|
+
const box = await container.boundingBox();
|
|
690
|
+
if (!box) break;
|
|
691
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
|
|
692
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
|
|
693
|
+
await cursor.actions.move({ x, y });
|
|
694
|
+
const step = minStep + Math.floor(Math.random() * (maxStep - minStep));
|
|
695
|
+
await page.mouse.wheel(0, direction * step);
|
|
696
|
+
if (Math.random() < 0.12) {
|
|
697
|
+
await (0, import_delay2.default)(this.jitterMs(60, 0.5));
|
|
698
|
+
const backStep = Math.max(20, Math.floor(step * (0.15 + Math.random() * 0.2)));
|
|
699
|
+
await page.mouse.wheel(0, -direction * backStep);
|
|
700
|
+
}
|
|
701
|
+
} finally {
|
|
702
|
+
await container.dispose();
|
|
703
|
+
}
|
|
704
|
+
await (0, import_delay2.default)(this.jitterMs(120, 0.4));
|
|
705
|
+
}
|
|
706
|
+
const restore = async () => {
|
|
707
|
+
for (const item of scrollables) {
|
|
708
|
+
try {
|
|
709
|
+
const box = await item.el.boundingBox();
|
|
710
|
+
if (!box) continue;
|
|
711
|
+
for (let i = 0; i < 12; i++) {
|
|
712
|
+
const current = await item.el.evaluate((el) => el.scrollTop);
|
|
713
|
+
const delta = item.top - current;
|
|
714
|
+
if (Math.abs(delta) <= 1) break;
|
|
715
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
|
|
716
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
|
|
717
|
+
await cursor.actions.move({ x, y });
|
|
718
|
+
const step = minStep + Math.floor(Math.random() * (maxStep - minStep));
|
|
719
|
+
const move = Math.sign(delta) * Math.min(Math.abs(delta), step);
|
|
720
|
+
await page.mouse.wheel(0, move);
|
|
721
|
+
await (0, import_delay2.default)(this.jitterMs(90, 0.4));
|
|
722
|
+
}
|
|
723
|
+
} catch (error) {
|
|
724
|
+
logger4.warn(`humanScroll: restore failed: ${error.message}`);
|
|
725
|
+
} finally {
|
|
726
|
+
await item.el.dispose();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
return { element, didScroll: true, restore };
|
|
731
|
+
},
|
|
565
732
|
/**
|
|
566
733
|
* 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
|
|
567
734
|
*
|
|
@@ -577,6 +744,18 @@ var Humanize = {
|
|
|
577
744
|
const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true } = options;
|
|
578
745
|
const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
|
|
579
746
|
logger4.start("humanClick", `target=${targetDesc}`);
|
|
747
|
+
let restoreScroll = async () => {
|
|
748
|
+
};
|
|
749
|
+
let restored = false;
|
|
750
|
+
const restoreOnce = async () => {
|
|
751
|
+
if (restored) return;
|
|
752
|
+
restored = true;
|
|
753
|
+
try {
|
|
754
|
+
await restoreScroll();
|
|
755
|
+
} catch (restoreError) {
|
|
756
|
+
logger4.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
|
|
757
|
+
}
|
|
758
|
+
};
|
|
580
759
|
try {
|
|
581
760
|
if (target == null) {
|
|
582
761
|
await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
|
|
@@ -597,49 +776,30 @@ var Humanize = {
|
|
|
597
776
|
} else {
|
|
598
777
|
element = target;
|
|
599
778
|
}
|
|
779
|
+
if (scrollIfNeeded) {
|
|
780
|
+
const scrollResult = await this.humanScroll(page, element);
|
|
781
|
+
restoreScroll = scrollResult.restore || restoreScroll;
|
|
782
|
+
}
|
|
600
783
|
const box = await element.boundingBox();
|
|
601
784
|
if (!box) {
|
|
785
|
+
await restoreOnce();
|
|
602
786
|
if (throwOnMissing) {
|
|
603
787
|
throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
604
788
|
}
|
|
605
789
|
logger4.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
|
|
606
790
|
return false;
|
|
607
791
|
}
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
await (0, import_delay2.default)(this.jitterMs(300, 0.3));
|
|
616
|
-
const newBox = await element.boundingBox();
|
|
617
|
-
if (newBox) {
|
|
618
|
-
const x = newBox.x + newBox.width / 2 + (Math.random() - 0.5) * newBox.width * 0.3;
|
|
619
|
-
const y = newBox.y + newBox.height / 2 + (Math.random() - 0.5) * newBox.height * 0.3;
|
|
620
|
-
await cursor.actions.move({ x, y });
|
|
621
|
-
await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
|
|
622
|
-
await cursor.actions.click();
|
|
623
|
-
} else {
|
|
624
|
-
throw new Error("\u6EDA\u52A8\u540E\u4ECD\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
625
|
-
}
|
|
626
|
-
if (originalScrollY !== null) {
|
|
627
|
-
await (0, import_delay2.default)(this.jitterMs(200, 0.3));
|
|
628
|
-
await page.evaluate((scrollY) => {
|
|
629
|
-
window.scrollTo({ top: scrollY, behavior: "smooth" });
|
|
630
|
-
}, originalScrollY);
|
|
631
|
-
logger4.debug(`\u5DF2\u6EDA\u52A8\u56DE\u539F\u4F4D\u7F6E: ${originalScrollY}`);
|
|
632
|
-
}
|
|
633
|
-
} else {
|
|
634
|
-
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
|
|
635
|
-
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
636
|
-
await cursor.actions.move({ x, y });
|
|
637
|
-
await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
|
|
638
|
-
await cursor.actions.click();
|
|
639
|
-
}
|
|
792
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
|
|
793
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
794
|
+
await cursor.actions.move({ x, y });
|
|
795
|
+
await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
|
|
796
|
+
await cursor.actions.click();
|
|
797
|
+
await (0, import_delay2.default)(this.jitterMs(180, 0.4));
|
|
798
|
+
await restoreOnce();
|
|
640
799
|
logger4.success("humanClick");
|
|
641
800
|
return true;
|
|
642
801
|
} catch (error) {
|
|
802
|
+
await restoreOnce();
|
|
643
803
|
logger4.fail("humanClick", error);
|
|
644
804
|
throw error;
|
|
645
805
|
}
|
|
@@ -693,7 +853,7 @@ var Humanize = {
|
|
|
693
853
|
} = options;
|
|
694
854
|
try {
|
|
695
855
|
const locator = page.locator(selector);
|
|
696
|
-
await
|
|
856
|
+
await Humanize.humanClick(page, locator);
|
|
697
857
|
await (0, import_delay2.default)(this.jitterMs(200, 0.4));
|
|
698
858
|
for (let i = 0; i < text.length; i++) {
|
|
699
859
|
const char = text[i];
|