@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.js
CHANGED
|
@@ -533,6 +533,164 @@ var Humanize = {
|
|
|
533
533
|
throw error;
|
|
534
534
|
}
|
|
535
535
|
},
|
|
536
|
+
/**
|
|
537
|
+
* 渐进式滚动到元素可见(仅处理 Y 轴滚动)
|
|
538
|
+
* 返回 restore 方法,用于将滚动容器恢复到原位置
|
|
539
|
+
*
|
|
540
|
+
* @param {import('playwright').Page} page
|
|
541
|
+
* @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
|
|
542
|
+
* @param {Object} [options]
|
|
543
|
+
* @param {number} [options.maxSteps=25] - 最大滚动步数
|
|
544
|
+
* @param {number} [options.minStep=80] - 单次滚动最小步长
|
|
545
|
+
* @param {number} [options.maxStep=220] - 单次滚动最大步长
|
|
546
|
+
*/
|
|
547
|
+
async humanScroll(page, target, options = {}) {
|
|
548
|
+
const { maxSteps = 25, minStep = 80, maxStep = 220 } = options;
|
|
549
|
+
let element;
|
|
550
|
+
if (typeof target === "string") {
|
|
551
|
+
element = await page.$(target);
|
|
552
|
+
if (!element) {
|
|
553
|
+
return { element: null, didScroll: false, restore: async () => {
|
|
554
|
+
} };
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
element = target;
|
|
558
|
+
}
|
|
559
|
+
const needsScroll = await element.evaluate((el) => {
|
|
560
|
+
const isScrollable = (node) => {
|
|
561
|
+
if (!node || node === document.body) return false;
|
|
562
|
+
const style = window.getComputedStyle(node);
|
|
563
|
+
const overflowY = style.overflowY || style.overflow;
|
|
564
|
+
return (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && node.scrollHeight > node.clientHeight + 1;
|
|
565
|
+
};
|
|
566
|
+
const rect = el.getBoundingClientRect();
|
|
567
|
+
if (!rect || rect.width === 0 || rect.height === 0) return true;
|
|
568
|
+
const inViewport = rect.top >= 0 && rect.bottom <= window.innerHeight;
|
|
569
|
+
if (!inViewport) return true;
|
|
570
|
+
let current = el.parentElement;
|
|
571
|
+
while (current) {
|
|
572
|
+
if (isScrollable(current)) {
|
|
573
|
+
const crect = current.getBoundingClientRect();
|
|
574
|
+
if (rect.top < crect.top || rect.bottom > crect.bottom) {
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
current = current.parentElement;
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
});
|
|
582
|
+
if (!needsScroll) {
|
|
583
|
+
return { element, didScroll: false, restore: async () => {
|
|
584
|
+
} };
|
|
585
|
+
}
|
|
586
|
+
const scrollStateHandle = await element.evaluateHandle((el) => {
|
|
587
|
+
const isScrollable = (node) => {
|
|
588
|
+
if (!node || node === document.body) return false;
|
|
589
|
+
const style = window.getComputedStyle(node);
|
|
590
|
+
const overflowY = style.overflowY || style.overflow;
|
|
591
|
+
return (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && node.scrollHeight > node.clientHeight + 1;
|
|
592
|
+
};
|
|
593
|
+
const scrollables = [];
|
|
594
|
+
const addNode = (node) => {
|
|
595
|
+
if (!node || scrollables.some((item) => item.el === node)) return;
|
|
596
|
+
scrollables.push({
|
|
597
|
+
el: node,
|
|
598
|
+
top: node.scrollTop
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
let current = el.parentElement;
|
|
602
|
+
while (current) {
|
|
603
|
+
if (isScrollable(current)) addNode(current);
|
|
604
|
+
current = current.parentElement;
|
|
605
|
+
}
|
|
606
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
607
|
+
if (scrollingElement) addNode(scrollingElement);
|
|
608
|
+
return scrollables;
|
|
609
|
+
});
|
|
610
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
611
|
+
const step = minStep + Math.floor(Math.random() * (maxStep - minStep));
|
|
612
|
+
const result = await element.evaluate((el, stepPx) => {
|
|
613
|
+
const isScrollable = (node) => {
|
|
614
|
+
if (!node || node === document.body) return false;
|
|
615
|
+
const style = window.getComputedStyle(node);
|
|
616
|
+
const overflowY = style.overflowY || style.overflow;
|
|
617
|
+
return (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && node.scrollHeight > node.clientHeight + 1;
|
|
618
|
+
};
|
|
619
|
+
const rect = el.getBoundingClientRect();
|
|
620
|
+
if (!rect || rect.width === 0 || rect.height === 0) {
|
|
621
|
+
return { moved: false, inView: false };
|
|
622
|
+
}
|
|
623
|
+
const scrollables = [];
|
|
624
|
+
let current = el.parentElement;
|
|
625
|
+
while (current) {
|
|
626
|
+
if (isScrollable(current)) scrollables.push(current);
|
|
627
|
+
current = current.parentElement;
|
|
628
|
+
}
|
|
629
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
630
|
+
if (scrollingElement) scrollables.push(scrollingElement);
|
|
631
|
+
let target2 = null;
|
|
632
|
+
for (const container of scrollables) {
|
|
633
|
+
const crect = container === scrollingElement ? { top: 0, bottom: window.innerHeight } : container.getBoundingClientRect();
|
|
634
|
+
if (rect.top < crect.top + 2) {
|
|
635
|
+
target2 = { container, direction: -1 };
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
if (rect.bottom > crect.bottom - 2) {
|
|
639
|
+
target2 = { container, direction: 1 };
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (!target2) {
|
|
644
|
+
return { moved: false, inView: true };
|
|
645
|
+
}
|
|
646
|
+
const maxScroll = target2.container.scrollHeight - target2.container.clientHeight;
|
|
647
|
+
if (maxScroll <= 0) {
|
|
648
|
+
return { moved: false, inView: false };
|
|
649
|
+
}
|
|
650
|
+
const before = target2.container.scrollTop;
|
|
651
|
+
let next = before + target2.direction * stepPx;
|
|
652
|
+
if (next < 0) next = 0;
|
|
653
|
+
if (next > maxScroll) next = maxScroll;
|
|
654
|
+
if (next === before) {
|
|
655
|
+
return { moved: false, inView: false };
|
|
656
|
+
}
|
|
657
|
+
target2.container.scrollTop = next;
|
|
658
|
+
return { moved: true, inView: false };
|
|
659
|
+
}, step);
|
|
660
|
+
if (result.inView) break;
|
|
661
|
+
if (!result.moved) break;
|
|
662
|
+
await delay2(this.jitterMs(120, 0.4));
|
|
663
|
+
}
|
|
664
|
+
const restore = async () => {
|
|
665
|
+
if (!scrollStateHandle) return;
|
|
666
|
+
try {
|
|
667
|
+
const restoreOnce = async () => page.evaluate((state) => {
|
|
668
|
+
if (!Array.isArray(state) || state.length === 0) return true;
|
|
669
|
+
let done = true;
|
|
670
|
+
for (const item of state) {
|
|
671
|
+
if (!item || !item.el) continue;
|
|
672
|
+
const current = item.el.scrollTop;
|
|
673
|
+
const target2 = item.top;
|
|
674
|
+
if (Math.abs(current - target2) > 1) {
|
|
675
|
+
done = false;
|
|
676
|
+
const delta = target2 - current;
|
|
677
|
+
const step = Math.sign(delta) * Math.max(20, Math.min(120, Math.abs(delta) * 0.3));
|
|
678
|
+
item.el.scrollTop = current + step;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return done;
|
|
682
|
+
}, scrollStateHandle);
|
|
683
|
+
for (let i = 0; i < 10; i++) {
|
|
684
|
+
const done = await restoreOnce();
|
|
685
|
+
if (done) break;
|
|
686
|
+
await delay2(this.jitterMs(80, 0.4));
|
|
687
|
+
}
|
|
688
|
+
} finally {
|
|
689
|
+
await scrollStateHandle.dispose();
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
return { element, didScroll: true, restore };
|
|
693
|
+
},
|
|
536
694
|
/**
|
|
537
695
|
* 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
|
|
538
696
|
*
|
|
@@ -568,49 +726,42 @@ var Humanize = {
|
|
|
568
726
|
} else {
|
|
569
727
|
element = target;
|
|
570
728
|
}
|
|
729
|
+
let restoreScroll = async () => {
|
|
730
|
+
};
|
|
731
|
+
let restored = false;
|
|
732
|
+
const restoreOnce = async () => {
|
|
733
|
+
if (restored) return;
|
|
734
|
+
restored = true;
|
|
735
|
+
try {
|
|
736
|
+
await restoreScroll();
|
|
737
|
+
} catch (restoreError) {
|
|
738
|
+
logger4.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
if (scrollIfNeeded) {
|
|
742
|
+
const scrollResult = await this.humanScroll(page, element);
|
|
743
|
+
restoreScroll = scrollResult.restore || restoreScroll;
|
|
744
|
+
}
|
|
571
745
|
const box = await element.boundingBox();
|
|
572
746
|
if (!box) {
|
|
747
|
+
await restoreOnce();
|
|
573
748
|
if (throwOnMissing) {
|
|
574
749
|
throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
575
750
|
}
|
|
576
751
|
logger4.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
|
|
577
752
|
return false;
|
|
578
753
|
}
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
await delay2(this.jitterMs(300, 0.3));
|
|
587
|
-
const newBox = await element.boundingBox();
|
|
588
|
-
if (newBox) {
|
|
589
|
-
const x = newBox.x + newBox.width / 2 + (Math.random() - 0.5) * newBox.width * 0.3;
|
|
590
|
-
const y = newBox.y + newBox.height / 2 + (Math.random() - 0.5) * newBox.height * 0.3;
|
|
591
|
-
await cursor.actions.move({ x, y });
|
|
592
|
-
await delay2(this.jitterMs(reactionDelay, 0.4));
|
|
593
|
-
await cursor.actions.click();
|
|
594
|
-
} else {
|
|
595
|
-
throw new Error("\u6EDA\u52A8\u540E\u4ECD\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
|
|
596
|
-
}
|
|
597
|
-
if (originalScrollY !== null) {
|
|
598
|
-
await delay2(this.jitterMs(200, 0.3));
|
|
599
|
-
await page.evaluate((scrollY) => {
|
|
600
|
-
window.scrollTo({ top: scrollY, behavior: "smooth" });
|
|
601
|
-
}, originalScrollY);
|
|
602
|
-
logger4.debug(`\u5DF2\u6EDA\u52A8\u56DE\u539F\u4F4D\u7F6E: ${originalScrollY}`);
|
|
603
|
-
}
|
|
604
|
-
} else {
|
|
605
|
-
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
|
|
606
|
-
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
607
|
-
await cursor.actions.move({ x, y });
|
|
608
|
-
await delay2(this.jitterMs(reactionDelay, 0.4));
|
|
609
|
-
await cursor.actions.click();
|
|
610
|
-
}
|
|
754
|
+
const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
|
|
755
|
+
const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
|
|
756
|
+
await cursor.actions.move({ x, y });
|
|
757
|
+
await delay2(this.jitterMs(reactionDelay, 0.4));
|
|
758
|
+
await cursor.actions.click();
|
|
759
|
+
await delay2(this.jitterMs(180, 0.4));
|
|
760
|
+
await restoreOnce();
|
|
611
761
|
logger4.success("humanClick");
|
|
612
762
|
return true;
|
|
613
763
|
} catch (error) {
|
|
764
|
+
logger4.fail("humanClick", error);
|
|
614
765
|
logger4.fail("humanClick", error);
|
|
615
766
|
throw error;
|
|
616
767
|
}
|