@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 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 viewport = page.viewportSize() || { width: 1920, height: 1080 };
609
- const isInViewport = box.x >= 0 && box.y >= 0 && box.x + box.width <= viewport.width && box.y + box.height <= viewport.height;
610
- let originalScrollY = null;
611
- if (!isInViewport && scrollIfNeeded) {
612
- logger4.debug(`\u5143\u7D20\u4E0D\u5728\u89C6\u53E3\u5185\uFF0C\u6EDA\u52A8\u5230\u89C6\u53E3...`);
613
- originalScrollY = await page.evaluate(() => window.scrollY);
614
- await element.scrollIntoViewIfNeeded();
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
  }