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