@skrillex1224/playwright-toolkit 2.1.19 → 2.1.21
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 +183 -32
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +183 -32
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -562,6 +562,164 @@ 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 { maxSteps = 25, minStep = 80, maxStep = 220 } = options;
|
|
578
|
+
let element;
|
|
579
|
+
if (typeof target === "string") {
|
|
580
|
+
element = await page.$(target);
|
|
581
|
+
if (!element) {
|
|
582
|
+
return { element: null, didScroll: false, restore: async () => {
|
|
583
|
+
} };
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
element = target;
|
|
587
|
+
}
|
|
588
|
+
const needsScroll = await element.evaluate((el) => {
|
|
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 rect = el.getBoundingClientRect();
|
|
596
|
+
if (!rect || rect.width === 0 || rect.height === 0) return true;
|
|
597
|
+
const inViewport = rect.top >= 0 && rect.bottom <= window.innerHeight;
|
|
598
|
+
if (!inViewport) return true;
|
|
599
|
+
let current = el.parentElement;
|
|
600
|
+
while (current) {
|
|
601
|
+
if (isScrollable(current)) {
|
|
602
|
+
const crect = current.getBoundingClientRect();
|
|
603
|
+
if (rect.top < crect.top || rect.bottom > crect.bottom) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
current = current.parentElement;
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
});
|
|
611
|
+
if (!needsScroll) {
|
|
612
|
+
return { element, didScroll: false, restore: async () => {
|
|
613
|
+
} };
|
|
614
|
+
}
|
|
615
|
+
const scrollStateHandle = await element.evaluateHandle((el) => {
|
|
616
|
+
const isScrollable = (node) => {
|
|
617
|
+
if (!node || node === document.body) return false;
|
|
618
|
+
const style = window.getComputedStyle(node);
|
|
619
|
+
const overflowY = style.overflowY || style.overflow;
|
|
620
|
+
return (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && node.scrollHeight > node.clientHeight + 1;
|
|
621
|
+
};
|
|
622
|
+
const scrollables = [];
|
|
623
|
+
const addNode = (node) => {
|
|
624
|
+
if (!node || scrollables.some((item) => item.el === node)) return;
|
|
625
|
+
scrollables.push({
|
|
626
|
+
el: node,
|
|
627
|
+
top: node.scrollTop
|
|
628
|
+
});
|
|
629
|
+
};
|
|
630
|
+
let current = el.parentElement;
|
|
631
|
+
while (current) {
|
|
632
|
+
if (isScrollable(current)) addNode(current);
|
|
633
|
+
current = current.parentElement;
|
|
634
|
+
}
|
|
635
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
636
|
+
if (scrollingElement) addNode(scrollingElement);
|
|
637
|
+
return scrollables;
|
|
638
|
+
});
|
|
639
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
640
|
+
const step = minStep + Math.floor(Math.random() * (maxStep - minStep));
|
|
641
|
+
const result = await element.evaluate((el, stepPx) => {
|
|
642
|
+
const isScrollable = (node) => {
|
|
643
|
+
if (!node || node === document.body) return false;
|
|
644
|
+
const style = window.getComputedStyle(node);
|
|
645
|
+
const overflowY = style.overflowY || style.overflow;
|
|
646
|
+
return (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && node.scrollHeight > node.clientHeight + 1;
|
|
647
|
+
};
|
|
648
|
+
const rect = el.getBoundingClientRect();
|
|
649
|
+
if (!rect || rect.width === 0 || rect.height === 0) {
|
|
650
|
+
return { moved: false, inView: false };
|
|
651
|
+
}
|
|
652
|
+
const scrollables = [];
|
|
653
|
+
let current = el.parentElement;
|
|
654
|
+
while (current) {
|
|
655
|
+
if (isScrollable(current)) scrollables.push(current);
|
|
656
|
+
current = current.parentElement;
|
|
657
|
+
}
|
|
658
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
659
|
+
if (scrollingElement) scrollables.push(scrollingElement);
|
|
660
|
+
let target2 = null;
|
|
661
|
+
for (const container of scrollables) {
|
|
662
|
+
const crect = container === scrollingElement ? { top: 0, bottom: window.innerHeight } : container.getBoundingClientRect();
|
|
663
|
+
if (rect.top < crect.top + 2) {
|
|
664
|
+
target2 = { container, direction: -1 };
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
if (rect.bottom > crect.bottom - 2) {
|
|
668
|
+
target2 = { container, direction: 1 };
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!target2) {
|
|
673
|
+
return { moved: false, inView: true };
|
|
674
|
+
}
|
|
675
|
+
const maxScroll = target2.container.scrollHeight - target2.container.clientHeight;
|
|
676
|
+
if (maxScroll <= 0) {
|
|
677
|
+
return { moved: false, inView: false };
|
|
678
|
+
}
|
|
679
|
+
const before = target2.container.scrollTop;
|
|
680
|
+
let next = before + target2.direction * stepPx;
|
|
681
|
+
if (next < 0) next = 0;
|
|
682
|
+
if (next > maxScroll) next = maxScroll;
|
|
683
|
+
if (next === before) {
|
|
684
|
+
return { moved: false, inView: false };
|
|
685
|
+
}
|
|
686
|
+
target2.container.scrollTop = next;
|
|
687
|
+
return { moved: true, inView: false };
|
|
688
|
+
}, step);
|
|
689
|
+
if (result.inView) break;
|
|
690
|
+
if (!result.moved) break;
|
|
691
|
+
await (0, import_delay2.default)(this.jitterMs(120, 0.4));
|
|
692
|
+
}
|
|
693
|
+
const restore = async () => {
|
|
694
|
+
if (!scrollStateHandle) return;
|
|
695
|
+
try {
|
|
696
|
+
const restoreOnce = async () => page.evaluate((state) => {
|
|
697
|
+
if (!Array.isArray(state) || state.length === 0) return true;
|
|
698
|
+
let done = true;
|
|
699
|
+
for (const item of state) {
|
|
700
|
+
if (!item || !item.el) continue;
|
|
701
|
+
const current = item.el.scrollTop;
|
|
702
|
+
const target2 = item.top;
|
|
703
|
+
if (Math.abs(current - target2) > 1) {
|
|
704
|
+
done = false;
|
|
705
|
+
const delta = target2 - current;
|
|
706
|
+
const step = Math.sign(delta) * Math.max(20, Math.min(120, Math.abs(delta) * 0.3));
|
|
707
|
+
item.el.scrollTop = current + step;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return done;
|
|
711
|
+
}, scrollStateHandle);
|
|
712
|
+
for (let i = 0; i < 10; i++) {
|
|
713
|
+
const done = await restoreOnce();
|
|
714
|
+
if (done) break;
|
|
715
|
+
await (0, import_delay2.default)(this.jitterMs(80, 0.4));
|
|
716
|
+
}
|
|
717
|
+
} finally {
|
|
718
|
+
await scrollStateHandle.dispose();
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
return { element, didScroll: true, restore };
|
|
722
|
+
},
|
|
565
723
|
/**
|
|
566
724
|
* 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
|
|
567
725
|
*
|
|
@@ -597,49 +755,42 @@ var Humanize = {
|
|
|
597
755
|
} else {
|
|
598
756
|
element = target;
|
|
599
757
|
}
|
|
758
|
+
let restoreScroll = async () => {
|
|
759
|
+
};
|
|
760
|
+
let restored = false;
|
|
761
|
+
const restoreOnce = async () => {
|
|
762
|
+
if (restored) return;
|
|
763
|
+
restored = true;
|
|
764
|
+
try {
|
|
765
|
+
await restoreScroll();
|
|
766
|
+
} catch (restoreError) {
|
|
767
|
+
logger4.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
if (scrollIfNeeded) {
|
|
771
|
+
const scrollResult = await this.humanScroll(page, element);
|
|
772
|
+
restoreScroll = scrollResult.restore || restoreScroll;
|
|
773
|
+
}
|
|
600
774
|
const box = await element.boundingBox();
|
|
601
775
|
if (!box) {
|
|
776
|
+
await restoreOnce();
|
|
602
777
|
if (throwOnMissing) {
|
|
603
778
|
throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
604
779
|
}
|
|
605
780
|
logger4.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
|
|
606
781
|
return false;
|
|
607
782
|
}
|
|
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
|
-
}
|
|
783
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
|
|
784
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
785
|
+
await cursor.actions.move({ x, y });
|
|
786
|
+
await (0, import_delay2.default)(this.jitterMs(reactionDelay, 0.4));
|
|
787
|
+
await cursor.actions.click();
|
|
788
|
+
await (0, import_delay2.default)(this.jitterMs(180, 0.4));
|
|
789
|
+
await restoreOnce();
|
|
640
790
|
logger4.success("humanClick");
|
|
641
791
|
return true;
|
|
642
792
|
} catch (error) {
|
|
793
|
+
logger4.fail("humanClick", error);
|
|
643
794
|
logger4.fail("humanClick", error);
|
|
644
795
|
throw error;
|
|
645
796
|
}
|