@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 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 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
- }
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 locator.click();
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];