@skrillex1224/playwright-toolkit 2.1.225 → 2.1.227

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
@@ -2148,7 +2148,7 @@ var Humanize = {
2148
2148
  },
2149
2149
  /**
2150
2150
  * 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
2151
- *
2151
+ *
2152
2152
  * @param {import('playwright').Page} page
2153
2153
  * @returns {Promise<void>}
2154
2154
  */
@@ -2164,7 +2164,7 @@ var Humanize = {
2164
2164
  },
2165
2165
  /**
2166
2166
  * 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
2167
- *
2167
+ *
2168
2168
  * @param {import('playwright').Page} page
2169
2169
  * @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
2170
2170
  */
@@ -2206,7 +2206,7 @@ var Humanize = {
2206
2206
  /**
2207
2207
  * 渐进式滚动到元素可见(仅处理 Y 轴滚动)
2208
2208
  * 返回 restore 方法,用于将滚动容器恢复到原位置
2209
- *
2209
+ *
2210
2210
  * @param {import('playwright').Page} page
2211
2211
  * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
2212
2212
  * @param {Object} [options]
@@ -2277,7 +2277,7 @@ var Humanize = {
2277
2277
  return { code: "VISIBLE", isFixed };
2278
2278
  });
2279
2279
  };
2280
- const getScrollableRect = async () => {
2280
+ const getScrollableRect2 = async () => {
2281
2281
  return await element.evaluate((el) => {
2282
2282
  const isScrollable = (node) => {
2283
2283
  const style = window.getComputedStyle(node);
@@ -2320,7 +2320,7 @@ var Humanize = {
2320
2320
  if (status.code === "OBSTRUCTED" && status.obstruction) {
2321
2321
  logger6.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
2322
2322
  }
2323
- const scrollRect = await getScrollableRect();
2323
+ const scrollRect = await getScrollableRect2();
2324
2324
  if (!scrollRect && status.isFixed) {
2325
2325
  logger6.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
2326
2326
  return { element, didScroll };
@@ -2367,7 +2367,7 @@ var Humanize = {
2367
2367
  },
2368
2368
  /**
2369
2369
  * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
2370
- *
2370
+ *
2371
2371
  * @param {import('playwright').Page} page
2372
2372
  * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
2373
2373
  * @param {Object} [options]
@@ -2428,9 +2428,7 @@ var Humanize = {
2428
2428
  const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
2429
2429
  await cursor.actions.move({ x, y });
2430
2430
  await (0, import_delay.default)(this.jitterMs(reactionDelay, 0.4));
2431
- await page.mouse.click(x, y, {
2432
- delay: this.jitterMs(45, 0.45)
2433
- });
2431
+ await cursor.actions.click();
2434
2432
  await restoreOnce();
2435
2433
  logger6.success("humanClick");
2436
2434
  return true;
@@ -2475,7 +2473,7 @@ var Humanize = {
2475
2473
  * @param {import('playwright').Page} page
2476
2474
  * @param {string} selector - 输入框选择器
2477
2475
  * @param {string} text - 要输入的文本
2478
- * @param {Object} [options]
2476
+ * @param {Object} [options]
2479
2477
  * @param {number} [options.baseDelay=180] - 基础按键延迟 (ms),实际 ±40% 抖动
2480
2478
  * @param {number} [options.pauseProbability=0.08] - 停顿概率 (0-1)
2481
2479
  * @param {number} [options.pauseBase=800] - 停顿时长基础值 (ms),实际 ±50% 抖动
@@ -2515,6 +2513,43 @@ var Humanize = {
2515
2513
  throw error;
2516
2514
  }
2517
2515
  },
2516
+ /**
2517
+ * 人类化按键 - 模拟用户在当前焦点或指定目标上按下一次键。
2518
+ * @param {import('playwright').Page} page
2519
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} targetOrKey - 目标或按键
2520
+ * @param {string|Object} [maybeKey] - 按键或选项
2521
+ * @param {Object} [options]
2522
+ */
2523
+ async humanPress(page, targetOrKey, maybeKey, options = {}) {
2524
+ const hasTarget = typeof maybeKey === "string";
2525
+ const key = hasTarget ? maybeKey : targetOrKey;
2526
+ const pressOptions = hasTarget ? options : maybeKey || options;
2527
+ const {
2528
+ reactionDelay = 180,
2529
+ holdDelay = 45,
2530
+ focusDelay = 180,
2531
+ scrollIfNeeded = true,
2532
+ throwOnMissing = true,
2533
+ keyboardOptions = {}
2534
+ } = pressOptions || {};
2535
+ const targetDesc = hasTarget ? typeof targetOrKey === "string" ? targetOrKey : "ElementHandle" : "current focus";
2536
+ logger6.start("humanPress", `key=${key}, target=${targetDesc}`);
2537
+ try {
2538
+ if (hasTarget) {
2539
+ await this.humanClick(page, targetOrKey, { reactionDelay: focusDelay, scrollIfNeeded, throwOnMissing });
2540
+ }
2541
+ await (0, import_delay.default)(this.jitterMs(reactionDelay, 0.45));
2542
+ await page.keyboard.press(key, {
2543
+ ...keyboardOptions,
2544
+ delay: this.jitterMs(holdDelay, 0.5)
2545
+ });
2546
+ logger6.success("humanPress");
2547
+ return true;
2548
+ } catch (error) {
2549
+ logger6.fail("humanPress", error);
2550
+ throw error;
2551
+ }
2552
+ },
2518
2553
  /**
2519
2554
  * 人类化清空输入框 - 模拟人类删除文本的行为
2520
2555
  * @param {import('playwright').Page} page
@@ -2603,6 +2638,141 @@ var Humanize = {
2603
2638
  }
2604
2639
  };
2605
2640
 
2641
+ // src/internals/humanize/machine.js
2642
+ var resolveDeviceFromPage = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
2643
+ var isPoint = (value) => value && typeof value === "object" && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
2644
+ var resolveNativeTarget = (page, target) => {
2645
+ if (!target) return null;
2646
+ if (typeof target === "string") {
2647
+ return page.locator(target);
2648
+ }
2649
+ return target;
2650
+ };
2651
+ var normalizeSelectorOptions = (options = {}) => ({
2652
+ targetParent: Boolean(options.targetParent ?? options.parent ?? false),
2653
+ pick: options.pick === "last" || options.last ? "last" : "first",
2654
+ visibleOnly: options.visibleOnly !== false,
2655
+ fallbackDomClick: Boolean(options.fallbackDomClick),
2656
+ forceMouse: Boolean(options.forceMouse)
2657
+ });
2658
+ var MachineHumanize = {
2659
+ async clickPoint(page, pointOrX, maybeY, options = {}) {
2660
+ const clickOptions = isPoint(pointOrX) && maybeY && typeof maybeY === "object" ? maybeY : options;
2661
+ const point = isPoint(pointOrX) ? pointOrX : { x: pointOrX, y: maybeY };
2662
+ const x = Number(point.x);
2663
+ const y = Number(point.y);
2664
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
2665
+ throw new Error(`Invalid click point: ${JSON.stringify(point)}`);
2666
+ }
2667
+ const resolvedDevice = resolveDeviceFromPage(page);
2668
+ if (resolvedDevice === Device.Mobile && !clickOptions.forceMouse && page.touchscreen && typeof page.touchscreen.tap === "function") {
2669
+ await page.touchscreen.tap(x, y);
2670
+ return true;
2671
+ }
2672
+ await page.mouse.click(x, y, clickOptions.mouseOptions || {});
2673
+ return true;
2674
+ },
2675
+ async click(page, target, options = {}) {
2676
+ if (isPoint(target)) {
2677
+ return MachineHumanize.clickPoint(page, target, void 0, options);
2678
+ }
2679
+ const nativeTarget = resolveNativeTarget(page, target);
2680
+ if (!nativeTarget || typeof nativeTarget.click !== "function") {
2681
+ throw new Error("Machine click target does not expose click()");
2682
+ }
2683
+ await nativeTarget.click(options.clickOptions || options);
2684
+ return true;
2685
+ },
2686
+ async focus(page, target, options = {}) {
2687
+ const nativeTarget = resolveNativeTarget(page, target);
2688
+ if (!nativeTarget || typeof nativeTarget.focus !== "function") {
2689
+ throw new Error("Machine focus target does not expose focus()");
2690
+ }
2691
+ await nativeTarget.focus(options);
2692
+ return true;
2693
+ },
2694
+ async fill(page, target, value, options = {}) {
2695
+ const nativeTarget = resolveNativeTarget(page, target);
2696
+ if (!nativeTarget || typeof nativeTarget.fill !== "function") {
2697
+ throw new Error("Machine fill target does not expose fill()");
2698
+ }
2699
+ await nativeTarget.fill(value, options);
2700
+ return true;
2701
+ },
2702
+ async keyboardType(page, text, options = {}) {
2703
+ await page.keyboard.type(text, options);
2704
+ return true;
2705
+ },
2706
+ async type(page, targetOrText, maybeText, options = {}) {
2707
+ if (maybeText == null || typeof maybeText === "object" && !Array.isArray(maybeText)) {
2708
+ return MachineHumanize.keyboardType(page, targetOrText, maybeText || options);
2709
+ }
2710
+ const nativeTarget = resolveNativeTarget(page, targetOrText);
2711
+ if (!nativeTarget || typeof nativeTarget.type !== "function") {
2712
+ throw new Error("Machine type target does not expose type()");
2713
+ }
2714
+ await nativeTarget.type(maybeText, options);
2715
+ return true;
2716
+ },
2717
+ async selectorCenterPoint(page, selector, options = {}) {
2718
+ const normalizedOptions = normalizeSelectorOptions(options);
2719
+ return page.evaluate(({ innerSelector, innerOptions }) => {
2720
+ const nodes = Array.from(document.querySelectorAll(innerSelector));
2721
+ const ordered = innerOptions.pick === "last" ? nodes.reverse() : nodes;
2722
+ let rect = null;
2723
+ for (const node of ordered) {
2724
+ if (!node) continue;
2725
+ const target = innerOptions.targetParent ? node.parentElement || node : node;
2726
+ rect = target.getBoundingClientRect?.() || null;
2727
+ const isVisible = Boolean(rect && rect.width > 0 && rect.height > 0);
2728
+ if (innerOptions.visibleOnly && !isVisible) continue;
2729
+ break;
2730
+ }
2731
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
2732
+ return {
2733
+ x: rect.left + rect.width / 2,
2734
+ y: rect.top + rect.height / 2
2735
+ };
2736
+ }, { innerSelector: selector, innerOptions: normalizedOptions });
2737
+ },
2738
+ async domClick(page, selector, options = {}) {
2739
+ const normalizedOptions = normalizeSelectorOptions(options);
2740
+ return page.evaluate(({ innerSelector, innerOptions }) => {
2741
+ const nodes = Array.from(document.querySelectorAll(innerSelector));
2742
+ const ordered = innerOptions.pick === "last" ? nodes.reverse() : nodes;
2743
+ let target = null;
2744
+ for (const node of ordered) {
2745
+ if (!node) continue;
2746
+ const candidate = innerOptions.targetParent ? node.parentElement || node : node;
2747
+ const rect = candidate.getBoundingClientRect?.() || null;
2748
+ const isVisible = Boolean(rect && rect.width > 0 && rect.height > 0);
2749
+ if (innerOptions.visibleOnly && !isVisible) continue;
2750
+ target = candidate;
2751
+ break;
2752
+ }
2753
+ if (!target || typeof target.click !== "function") return false;
2754
+ target.click();
2755
+ return true;
2756
+ }, { innerSelector: selector, innerOptions: normalizedOptions });
2757
+ },
2758
+ async clickSelectorCenter(page, selector, options = {}) {
2759
+ const normalizedOptions = normalizeSelectorOptions(options);
2760
+ try {
2761
+ const point = await MachineHumanize.selectorCenterPoint(page, selector, normalizedOptions);
2762
+ if (point) {
2763
+ await MachineHumanize.clickPoint(page, point, void 0, normalizedOptions);
2764
+ return true;
2765
+ }
2766
+ } catch (error) {
2767
+ if (!normalizedOptions.fallbackDomClick) throw error;
2768
+ }
2769
+ if (normalizedOptions.fallbackDomClick) {
2770
+ return MachineHumanize.domClick(page, selector, normalizedOptions);
2771
+ }
2772
+ return false;
2773
+ }
2774
+ };
2775
+
2606
2776
  // src/internals/humanize/shared.js
2607
2777
  var import_delay2 = __toESM(require("delay"), 1);
2608
2778
  var jitterMs = (base, jitterPercent = 0.3) => {
@@ -2637,16 +2807,43 @@ var logger7 = createInternalLogger("Humanize.Mobile");
2637
2807
  var initializedPages = /* @__PURE__ */ new WeakSet();
2638
2808
  var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
2639
2809
  var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
2810
+ var describeTarget = (target) => {
2811
+ if (target == null) return "Current Position";
2812
+ return typeof target === "string" ? target : "ElementHandle";
2813
+ };
2814
+ var clipBoxToViewport = (box, viewport) => {
2815
+ if (!box || !viewport) return box;
2816
+ const left = clamp(box.x, 0, viewport.width);
2817
+ const top = clamp(box.y, 0, viewport.height);
2818
+ const right = clamp(box.x + box.width, 0, viewport.width);
2819
+ const bottom = clamp(box.y + box.height, 0, viewport.height);
2820
+ if (right - left > 1 && bottom - top > 1) {
2821
+ return {
2822
+ x: left,
2823
+ y: top,
2824
+ width: right - left,
2825
+ height: bottom - top
2826
+ };
2827
+ }
2828
+ return box;
2829
+ };
2640
2830
  var checkElementVisibility = async (element) => {
2641
2831
  return element.evaluate((el) => {
2832
+ const targetStyle = window.getComputedStyle(el);
2833
+ if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
2834
+ return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
2835
+ }
2642
2836
  const rect = el.getBoundingClientRect();
2643
2837
  if (!rect || rect.width <= 0 || rect.height <= 0) {
2644
- return { code: "ZERO_DIMENSIONS", direction: "down" };
2838
+ return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6", direction: "down" };
2645
2839
  }
2646
2840
  const viewW = window.innerWidth;
2647
2841
  const viewH = window.innerHeight;
2648
- const cx = rect.left + rect.width / 2;
2649
- const cy = rect.top + rect.height / 2;
2842
+ const centerY = rect.top + rect.height / 2;
2843
+ let clipLeft = 0;
2844
+ let clipRight = viewW;
2845
+ let clipTop = 0;
2846
+ let clipBottom = viewH;
2650
2847
  let isFixed = false;
2651
2848
  for (let node = el; node && node !== document.body; node = node.parentElement) {
2652
2849
  const style = window.getComputedStyle(node);
@@ -2655,20 +2852,101 @@ var checkElementVisibility = async (element) => {
2655
2852
  break;
2656
2853
  }
2657
2854
  }
2658
- if ((isFixed ? cy < 0 || cy > viewH : cy < 12 || cy > viewH - 12) || cx < 0 || cx > viewW) {
2855
+ for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
2856
+ const style = window.getComputedStyle(node);
2857
+ if (!style) continue;
2858
+ const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
2859
+ const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
2860
+ if (!clipsX && !clipsY) continue;
2861
+ const nodeRect = node.getBoundingClientRect();
2862
+ if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
2863
+ if (clipsX) {
2864
+ clipLeft = Math.max(clipLeft, nodeRect.left);
2865
+ clipRight = Math.min(clipRight, nodeRect.right);
2866
+ }
2867
+ if (clipsY) {
2868
+ clipTop = Math.max(clipTop, nodeRect.top);
2869
+ clipBottom = Math.min(clipBottom, nodeRect.bottom);
2870
+ }
2871
+ }
2872
+ const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
2873
+ const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
2874
+ const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
2875
+ const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
2876
+ const visibleWidth = visibleRight - visibleLeft;
2877
+ const visibleHeight = visibleBottom - visibleTop;
2878
+ const cx = visibleLeft + visibleWidth / 2;
2879
+ const cy = visibleTop + visibleHeight / 2;
2880
+ if (visibleWidth <= 1 || visibleHeight <= 1) {
2659
2881
  return {
2660
2882
  code: "OUT_OF_VIEWPORT",
2661
- direction: cy < viewH / 2 ? "up" : "down",
2662
- cy,
2883
+ reason: "\u4E0D\u5728\u89C6\u53E3\u5185",
2884
+ direction: centerY < clipTop ? "up" : "down",
2885
+ cy: centerY,
2663
2886
  viewH,
2664
2887
  isFixed
2665
2888
  };
2666
2889
  }
2667
- const pointElement = document.elementFromPoint(cx, cy);
2668
- if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
2890
+ const interactiveSelector = [
2891
+ "button",
2892
+ '[role="button"]',
2893
+ "a[href]",
2894
+ "label",
2895
+ "input",
2896
+ "textarea",
2897
+ "select",
2898
+ "summary",
2899
+ '[contenteditable="true"]',
2900
+ '[tabindex]:not([tabindex="-1"])'
2901
+ ].join(",");
2902
+ const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
2903
+ const sameInteractiveTarget = (pointElement) => {
2904
+ if (!pointElement) return false;
2905
+ if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
2906
+ return true;
2907
+ }
2908
+ if (!targetInteractive || typeof pointElement.closest !== "function") {
2909
+ return false;
2910
+ }
2911
+ return pointElement.closest(interactiveSelector) === targetInteractive;
2912
+ };
2913
+ const describeElement = (node) => {
2914
+ if (!node) return null;
2915
+ const className = typeof node.className === "string" ? node.className : node.className && typeof node.className.baseVal === "string" ? node.className.baseVal : "";
2916
+ const rect2 = node.getBoundingClientRect?.();
2917
+ const style = window.getComputedStyle(node);
2918
+ return {
2919
+ tag: node.tagName,
2920
+ id: node.id || "",
2921
+ className,
2922
+ isFixed: Boolean(style && (style.position === "fixed" || style.position === "sticky")),
2923
+ top: rect2 ? rect2.top : null,
2924
+ bottom: rect2 ? rect2.bottom : null,
2925
+ left: rect2 ? rect2.left : null,
2926
+ right: rect2 ? rect2.right : null
2927
+ };
2928
+ };
2929
+ const samplePoints = [
2930
+ { x: cx, y: cy },
2931
+ { x: visibleLeft + Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
2932
+ { x: visibleRight - Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
2933
+ { x: cx, y: visibleTop + Math.min(8, Math.max(1, visibleHeight * 0.25)) },
2934
+ { x: cx, y: visibleBottom - Math.min(8, Math.max(1, visibleHeight * 0.25)) }
2935
+ ];
2936
+ let obstruction = null;
2937
+ for (const point of samplePoints) {
2938
+ const pointElement = document.elementFromPoint(point.x, point.y);
2939
+ if (sameInteractiveTarget(pointElement)) {
2940
+ return { code: "VISIBLE", isFixed };
2941
+ }
2942
+ obstruction = obstruction || describeElement(pointElement);
2943
+ }
2944
+ if (obstruction) {
2669
2945
  return {
2670
2946
  code: "OBSTRUCTED",
2947
+ reason: "\u88AB\u906E\u6321",
2671
2948
  direction: cy > viewH / 2 ? "down" : "up",
2949
+ obstruction,
2672
2950
  cy,
2673
2951
  viewH,
2674
2952
  isFixed
@@ -2677,17 +2955,228 @@ var checkElementVisibility = async (element) => {
2677
2955
  return { code: "VISIBLE", isFixed };
2678
2956
  });
2679
2957
  };
2958
+ var resolveSafeTapPoint = async (element) => {
2959
+ return element.evaluate((el) => {
2960
+ const rect = el.getBoundingClientRect();
2961
+ if (!rect || rect.width <= 0 || rect.height <= 0) {
2962
+ return null;
2963
+ }
2964
+ const viewW = window.innerWidth;
2965
+ const viewH = window.innerHeight;
2966
+ let clipLeft = 0;
2967
+ let clipRight = viewW;
2968
+ let clipTop = 0;
2969
+ let clipBottom = viewH;
2970
+ for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
2971
+ const style = window.getComputedStyle(node);
2972
+ if (!style) continue;
2973
+ const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
2974
+ const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
2975
+ if (!clipsX && !clipsY) continue;
2976
+ const nodeRect = node.getBoundingClientRect();
2977
+ if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
2978
+ if (clipsX) {
2979
+ clipLeft = Math.max(clipLeft, nodeRect.left);
2980
+ clipRight = Math.min(clipRight, nodeRect.right);
2981
+ }
2982
+ if (clipsY) {
2983
+ clipTop = Math.max(clipTop, nodeRect.top);
2984
+ clipBottom = Math.min(clipBottom, nodeRect.bottom);
2985
+ }
2986
+ }
2987
+ const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
2988
+ const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
2989
+ const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
2990
+ const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
2991
+ const visibleWidth = visibleRight - visibleLeft;
2992
+ const visibleHeight = visibleBottom - visibleTop;
2993
+ if (visibleWidth <= 1 || visibleHeight <= 1) {
2994
+ return null;
2995
+ }
2996
+ const interactiveSelector = [
2997
+ "button",
2998
+ '[role="button"]',
2999
+ "a[href]",
3000
+ "label",
3001
+ "input",
3002
+ "textarea",
3003
+ "select",
3004
+ "summary",
3005
+ '[contenteditable="true"]',
3006
+ '[tabindex]:not([tabindex="-1"])'
3007
+ ].join(",");
3008
+ const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
3009
+ const sameInteractiveTarget = (pointElement) => {
3010
+ if (!pointElement) return false;
3011
+ if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
3012
+ return true;
3013
+ }
3014
+ if (!targetInteractive || typeof pointElement.closest !== "function") {
3015
+ return false;
3016
+ }
3017
+ return pointElement.closest(interactiveSelector) === targetInteractive;
3018
+ };
3019
+ const cx = visibleLeft + visibleWidth / 2;
3020
+ const cy = visibleTop + visibleHeight / 2;
3021
+ const smallX = Math.min(12, Math.max(2, visibleWidth * 0.18));
3022
+ const smallY = Math.min(12, Math.max(2, visibleHeight * 0.18));
3023
+ const points = [
3024
+ { x: cx, y: cy },
3025
+ { x: visibleLeft + smallX, y: visibleTop + smallY },
3026
+ { x: visibleRight - smallX, y: visibleTop + smallY },
3027
+ { x: visibleLeft + smallX, y: visibleBottom - smallY },
3028
+ { x: visibleRight - smallX, y: visibleBottom - smallY },
3029
+ { x: cx, y: visibleTop + Math.min(10, Math.max(2, visibleHeight * 0.2)) },
3030
+ { x: cx, y: visibleBottom - Math.min(10, Math.max(2, visibleHeight * 0.2)) },
3031
+ { x: visibleLeft + Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy },
3032
+ { x: visibleRight - Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy }
3033
+ ];
3034
+ const safePoints = points.filter((point) => {
3035
+ const pointElement = document.elementFromPoint(point.x, point.y);
3036
+ return sameInteractiveTarget(pointElement);
3037
+ });
3038
+ const candidates = safePoints.length ? safePoints : points;
3039
+ const chosen = candidates[Math.floor(Math.random() * candidates.length)];
3040
+ if (!chosen) return null;
3041
+ return {
3042
+ x: chosen.x,
3043
+ y: chosen.y
3044
+ };
3045
+ });
3046
+ };
3047
+ var getScrollableRect = async (element) => {
3048
+ return element.evaluate((el) => {
3049
+ const isScrollable = (node) => {
3050
+ const style = window.getComputedStyle(node);
3051
+ if (!style) return false;
3052
+ const overflowY = style.overflowY;
3053
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3054
+ return node.scrollHeight > node.clientHeight + 1;
3055
+ };
3056
+ let current = el;
3057
+ while (current && current !== document.body) {
3058
+ if (isScrollable(current)) {
3059
+ const rect = current.getBoundingClientRect();
3060
+ if (rect && rect.width > 0 && rect.height > 0) {
3061
+ return {
3062
+ x: rect.x,
3063
+ y: rect.y,
3064
+ width: rect.width,
3065
+ height: rect.height
3066
+ };
3067
+ }
3068
+ }
3069
+ current = current.parentElement;
3070
+ }
3071
+ return null;
3072
+ });
3073
+ };
3074
+ var scrollScrollableAncestor = async (element, deltaY) => {
3075
+ return element.evaluate((el, amount) => {
3076
+ const isScrollable = (node) => {
3077
+ const style = window.getComputedStyle(node);
3078
+ if (!style) return false;
3079
+ const overflowY = style.overflowY;
3080
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3081
+ return node.scrollHeight > node.clientHeight + 1;
3082
+ };
3083
+ let current = el;
3084
+ while (current && current !== document.body) {
3085
+ if (isScrollable(current)) {
3086
+ const beforeTop2 = current.scrollTop;
3087
+ current.scrollTop = beforeTop2 + amount;
3088
+ return {
3089
+ scroller: true,
3090
+ moved: current.scrollTop !== beforeTop2,
3091
+ scrollTop: current.scrollTop
3092
+ };
3093
+ }
3094
+ current = current.parentElement;
3095
+ }
3096
+ const beforeTop = window.scrollY;
3097
+ window.scrollBy(0, amount);
3098
+ return {
3099
+ scroller: null,
3100
+ moved: window.scrollY !== beforeTop,
3101
+ scrollTop: window.scrollY
3102
+ };
3103
+ }, deltaY);
3104
+ };
3105
+ var scrollAwayFromObstruction = async (element, status) => {
3106
+ return element.evaluate((el, innerStatus) => {
3107
+ const isScrollable = (node) => {
3108
+ const style = window.getComputedStyle(node);
3109
+ if (!style) return false;
3110
+ const overflowY = style.overflowY;
3111
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3112
+ return node.scrollHeight > node.clientHeight + 1;
3113
+ };
3114
+ let scroller = el;
3115
+ while (scroller && scroller !== document.body) {
3116
+ if (isScrollable(scroller)) break;
3117
+ scroller = scroller.parentElement;
3118
+ }
3119
+ if (!scroller || scroller === document.body) {
3120
+ return { moved: false, scrollTop: 0, deltaY: 0 };
3121
+ }
3122
+ const rect = el.getBoundingClientRect();
3123
+ const scrollerRect = scroller.getBoundingClientRect();
3124
+ const obstruction = innerStatus?.obstruction || {};
3125
+ const obstructionTop = Number(obstruction.top);
3126
+ const obstructionBottom = Number(obstruction.bottom);
3127
+ const obstructionMiddle = Number.isFinite(obstructionTop) && Number.isFinite(obstructionBottom) ? (obstructionTop + obstructionBottom) / 2 : scrollerRect.top + scrollerRect.height / 2;
3128
+ const scrollerMiddle = scrollerRect.top + scrollerRect.height / 2;
3129
+ const padding = 18;
3130
+ let deltaY = 0;
3131
+ if (Number.isFinite(obstructionTop) && rect.bottom > obstructionTop && obstructionTop >= scrollerRect.top && obstructionTop <= scrollerRect.bottom && obstructionMiddle >= scrollerMiddle) {
3132
+ deltaY = rect.bottom - obstructionTop + padding;
3133
+ } else if (Number.isFinite(obstructionBottom) && rect.top < obstructionBottom && obstructionBottom >= scrollerRect.top && obstructionBottom <= scrollerRect.bottom && obstructionMiddle < scrollerMiddle) {
3134
+ deltaY = rect.top - obstructionBottom - padding;
3135
+ } else {
3136
+ const fallbackDistance = Math.max(48, Math.min(180, scrollerRect.height * 0.45));
3137
+ deltaY = obstructionMiddle >= scrollerMiddle ? fallbackDistance : -fallbackDistance;
3138
+ }
3139
+ const beforeTop = scroller.scrollTop;
3140
+ scroller.scrollTop = beforeTop + deltaY;
3141
+ return {
3142
+ moved: Math.abs(scroller.scrollTop - beforeTop) > 2,
3143
+ scrollTop: scroller.scrollTop,
3144
+ deltaY
3145
+ };
3146
+ }, status);
3147
+ };
2680
3148
  var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
2681
3149
  const viewport = resolveViewport(page);
2682
- const distance = clamp(Math.abs(deltaY), 80, Math.max(120, viewport.height * 0.72));
3150
+ const rawRect = options.rect || null;
3151
+ const rect = rawRect ? {
3152
+ x: clamp(rawRect.x, 0, viewport.width),
3153
+ y: clamp(rawRect.y, 0, viewport.height),
3154
+ width: clamp(rawRect.width, 0, viewport.width),
3155
+ height: clamp(rawRect.height, 0, viewport.height)
3156
+ } : null;
3157
+ const area = rect && rect.width > 24 && rect.height > 48 ? {
3158
+ left: rect.x,
3159
+ right: Math.min(viewport.width, rect.x + rect.width),
3160
+ top: rect.y,
3161
+ bottom: Math.min(viewport.height, rect.y + rect.height)
3162
+ } : {
3163
+ left: 0,
3164
+ right: viewport.width,
3165
+ top: 0,
3166
+ bottom: viewport.height
3167
+ };
3168
+ const areaWidth = Math.max(1, area.right - area.left);
3169
+ const areaHeight = Math.max(1, area.bottom - area.top);
3170
+ const maxDistance = rect ? Math.max(60, areaHeight * 0.72) : Math.max(120, viewport.height * 0.72);
3171
+ const distance = clamp(Math.abs(deltaY), Math.min(80, maxDistance), maxDistance);
2683
3172
  const direction = deltaY >= 0 ? 1 : -1;
2684
3173
  const startX = clamp(
2685
- viewport.width * (0.45 + Math.random() * 0.1),
3174
+ area.left + areaWidth * (0.45 + Math.random() * 0.1),
2686
3175
  24,
2687
3176
  viewport.width - 24
2688
3177
  );
2689
- const startY = direction > 0 ? clamp(viewport.height * (0.72 + Math.random() * 0.1), 80, viewport.height - 32) : clamp(viewport.height * (0.28 + Math.random() * 0.1), 32, viewport.height - 80);
2690
- const endY = clamp(startY - direction * distance, 24, viewport.height - 24);
3178
+ const startY = direction > 0 ? clamp(area.top + areaHeight * (0.72 + Math.random() * 0.1), area.top + 24, area.bottom - 16) : clamp(area.top + areaHeight * (0.28 + Math.random() * 0.1), area.top + 16, area.bottom - 24);
3179
+ const endY = clamp(startY - direction * distance, Math.max(16, area.top + 12), Math.min(viewport.height - 16, area.bottom - 12));
2691
3180
  const steps = Math.max(4, Math.round(Number(options.steps || 6)));
2692
3181
  const durationMs = jitterMs(options.durationMs || 320, 0.35);
2693
3182
  let client = null;
@@ -2790,8 +3279,11 @@ var MobileHumanize = {
2790
3279
  maxDurationMs = maxSteps * 280 + 1200,
2791
3280
  throwOnMissing = false
2792
3281
  } = options;
3282
+ const targetDesc = describeTarget(target);
3283
+ logger7.start("humanScroll", `target=${targetDesc}`);
2793
3284
  const element = await resolveElement(page, target, { throwOnMissing });
2794
3285
  if (!element) {
3286
+ logger7.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
2795
3287
  return { element: null, didScroll: false, restore: null };
2796
3288
  }
2797
3289
  const startTime = Date.now();
@@ -2799,17 +3291,133 @@ var MobileHumanize = {
2799
3291
  for (let i = 0; i < maxSteps; i += 1) {
2800
3292
  const status = await checkElementVisibility(element);
2801
3293
  if (status.code === "VISIBLE") {
3294
+ if (status.isFixed) {
3295
+ logger7.info("humanScroll | fixed/sticky \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
3296
+ } else {
3297
+ logger7.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
3298
+ }
3299
+ logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
3300
+ return { element, didScroll, restore: null };
3301
+ }
3302
+ if (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE") {
3303
+ logger7.warn(`humanScroll | \u5143\u7D20\u4E0D\u53EF\u6EDA\u52A8\u81F3\u53EF\u70B9\u51FB\u72B6\u6001: ${status.reason || status.code}`);
2802
3304
  return { element, didScroll, restore: null };
2803
3305
  }
3306
+ const scrollRect = await getScrollableRect(element);
3307
+ if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
3308
+ logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
3309
+ return { element, didScroll, restore: null };
3310
+ }
3311
+ if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
3312
+ const moved = await scrollAwayFromObstruction(element, status);
3313
+ if (moved.moved) {
3314
+ logger7.debug(`humanScroll | sticky/fixed \u906E\u6321\u8865\u507F\u6EDA\u52A8 top=${Math.round(moved.scrollTop || 0)}`);
3315
+ await waitJitter(90, 0.3);
3316
+ didScroll = true;
3317
+ continue;
3318
+ }
3319
+ }
2804
3320
  if (Date.now() - startTime > maxDurationMs) {
2805
- logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms)`);
3321
+ logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms, status=${status.code}, direction=${status.direction || "unknown"}, fixed=${Boolean(status.isFixed)})`);
2806
3322
  return { element, didScroll, restore: null };
2807
3323
  }
2808
- const distance = minStep + Math.random() * Math.max(1, maxStep - minStep);
2809
- const deltaY = status.direction === "up" ? -distance : distance;
2810
- await dispatchTouchSwipe(page, deltaY);
3324
+ const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
3325
+ const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
3326
+ logger7.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
3327
+ const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
3328
+ let deltaY = status.direction === "up" ? -distance : distance;
3329
+ if (status.code === "OBSTRUCTED") {
3330
+ if (status.obstruction?.isFixed && status.obstruction.top != null) {
3331
+ const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
3332
+ const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
3333
+ deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
3334
+ } else {
3335
+ const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
3336
+ deltaY = status.cy > halfY ? distance : -distance;
3337
+ }
3338
+ }
3339
+ const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
3340
+ const beforeState = scrollRect ? await element.evaluate((el) => {
3341
+ const isScrollable = (node) => {
3342
+ const style = window.getComputedStyle(node);
3343
+ if (!style) return false;
3344
+ const overflowY = style.overflowY;
3345
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3346
+ return node.scrollHeight > node.clientHeight + 1;
3347
+ };
3348
+ let current = el;
3349
+ while (current && current !== document.body) {
3350
+ if (isScrollable(current)) {
3351
+ return {
3352
+ kind: "element",
3353
+ top: current.scrollTop,
3354
+ left: current.scrollLeft
3355
+ };
3356
+ }
3357
+ current = current.parentElement;
3358
+ }
3359
+ return { kind: "window", top: window.scrollY, left: window.scrollX };
3360
+ }) : null;
3361
+ await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
3362
+ if (scrollRect && beforeWindowState) {
3363
+ const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
3364
+ if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
3365
+ await page.evaluate((state) => window.scrollTo(state.x, state.y), beforeWindowState);
3366
+ logger7.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
3367
+ }
3368
+ }
3369
+ if (scrollRect && beforeState) {
3370
+ const afterState = await element.evaluate((el) => {
3371
+ const isScrollable = (node) => {
3372
+ const style = window.getComputedStyle(node);
3373
+ if (!style) return false;
3374
+ const overflowY = style.overflowY;
3375
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3376
+ return node.scrollHeight > node.clientHeight + 1;
3377
+ };
3378
+ let current = el;
3379
+ while (current && current !== document.body) {
3380
+ if (isScrollable(current)) {
3381
+ return {
3382
+ kind: "element",
3383
+ top: current.scrollTop,
3384
+ left: current.scrollLeft
3385
+ };
3386
+ }
3387
+ current = current.parentElement;
3388
+ }
3389
+ return { kind: "window", top: window.scrollY, left: window.scrollX };
3390
+ });
3391
+ const topDelta = Number(afterState.top || 0) - Number(beforeState.top || 0);
3392
+ const leftDelta = Number(afterState.left || 0) - Number(beforeState.left || 0);
3393
+ const expectedDelta = Number(deltaY || 0);
3394
+ const moved = beforeState.kind !== afterState.kind || Math.abs(topDelta) > 2 || Math.abs(leftDelta) > 2;
3395
+ if (!moved) {
3396
+ const fallback = await scrollScrollableAncestor(element, deltaY);
3397
+ logger7.debug(`humanScroll | \u5BB9\u5668\u89E6\u6478\u65E0\u6548\uFF0C\u76F4\u63A5\u6EDA\u52A8 fallback=${fallback.scroller ? "ancestor" : "window"} top=${Math.round(fallback.scrollTop || 0)}`);
3398
+ } else if (beforeState.kind === afterState.kind && Math.abs(expectedDelta) > 24 && Math.sign(topDelta || expectedDelta) === Math.sign(expectedDelta) && Math.abs(topDelta) < Math.min(Math.abs(expectedDelta) * 0.45, 96)) {
3399
+ const residualDelta = expectedDelta - topDelta;
3400
+ if (Math.sign(residualDelta) === Math.sign(expectedDelta) && Math.abs(residualDelta) > 24) {
3401
+ const fallback = await scrollScrollableAncestor(element, residualDelta);
3402
+ logger7.debug(`humanScroll | \u5BB9\u5668\u89E6\u6478\u8DDD\u79BB\u4E0D\u8DB3\uFF0C\u8865\u507F\u6EDA\u52A8 fallback=${fallback.scroller ? "ancestor" : "window"} top=${Math.round(fallback.scrollTop || 0)}`);
3403
+ }
3404
+ }
3405
+ }
2811
3406
  didScroll = true;
2812
3407
  }
3408
+ try {
3409
+ await element.scrollIntoViewIfNeeded?.();
3410
+ await waitJitter(80, 0.3);
3411
+ const finalStatus = await checkElementVisibility(element);
3412
+ if (finalStatus.code === "VISIBLE") {
3413
+ logger7.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
3414
+ logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
3415
+ return { element, didScroll: true, restore: null };
3416
+ }
3417
+ } catch (fallbackError) {
3418
+ logger7.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
3419
+ }
3420
+ logger7.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
2813
3421
  return { element, didScroll, restore: null };
2814
3422
  },
2815
3423
  async humanClick(page, target, options = {}) {
@@ -2818,29 +3426,51 @@ var MobileHumanize = {
2818
3426
  throwOnMissing = true,
2819
3427
  scrollIfNeeded = true
2820
3428
  } = options;
2821
- if (target == null) {
2822
- const viewport = resolveViewport(page);
3429
+ const targetDesc = describeTarget(target);
3430
+ logger7.start("humanClick", `target=${targetDesc}`);
3431
+ try {
3432
+ if (target == null) {
3433
+ const viewport = resolveViewport(page);
3434
+ await waitJitter(reactionDelay, 0.45);
3435
+ await tapPoint(page, {
3436
+ x: viewport.width * (0.45 + Math.random() * 0.1),
3437
+ y: viewport.height * (0.48 + Math.random() * 0.12)
3438
+ });
3439
+ logger7.success("humanClick", "Tapped current position");
3440
+ return true;
3441
+ }
3442
+ const element = await resolveElement(page, target, { throwOnMissing });
3443
+ if (!element) {
3444
+ logger7.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
3445
+ return false;
3446
+ }
3447
+ if (scrollIfNeeded) {
3448
+ await MobileHumanize.humanScroll(page, element, { throwOnMissing });
3449
+ }
3450
+ const status = await checkElementVisibility(element).catch(() => null);
3451
+ if (status && status.code !== "VISIBLE") {
3452
+ const message = `\u5143\u7D20\u4E0D\u53EF\u70B9\u51FB: ${status.reason || status.code}`;
3453
+ if (throwOnMissing) throw new Error(message);
3454
+ logger7.warn(`humanClick: ${message}\uFF0C\u8DF3\u8FC7\u70B9\u51FB`);
3455
+ return false;
3456
+ }
3457
+ const box = await element.boundingBox();
3458
+ if (!box) {
3459
+ if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
3460
+ logger7.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
3461
+ return false;
3462
+ }
2823
3463
  await waitJitter(reactionDelay, 0.45);
2824
- await tapPoint(page, {
2825
- x: viewport.width * (0.45 + Math.random() * 0.1),
2826
- y: viewport.height * (0.48 + Math.random() * 0.12)
2827
- });
3464
+ const safePoint = await resolveSafeTapPoint(element).catch(() => null);
3465
+ const tapTarget = safePoint || randomPointInBox(clipBoxToViewport(box, resolveViewport(page)), 0.2);
3466
+ await tapPoint(page, tapTarget);
3467
+ await waitJitter(120, 0.35);
3468
+ logger7.success("humanClick");
2828
3469
  return true;
3470
+ } catch (error) {
3471
+ logger7.fail("humanClick", error);
3472
+ throw error;
2829
3473
  }
2830
- const element = await resolveElement(page, target, { throwOnMissing });
2831
- if (!element) return false;
2832
- if (scrollIfNeeded) {
2833
- await MobileHumanize.humanScroll(page, element, { throwOnMissing });
2834
- }
2835
- const box = await element.boundingBox();
2836
- if (!box) {
2837
- if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
2838
- return false;
2839
- }
2840
- await waitJitter(reactionDelay, 0.45);
2841
- await tapPoint(page, randomPointInBox(box, 0.2));
2842
- await waitJitter(120, 0.35);
2843
- return true;
2844
3474
  },
2845
3475
  async randomSleep(baseMs, jitterPercent = 0.3) {
2846
3476
  await waitJitter(baseMs, jitterPercent);
@@ -2878,6 +3508,40 @@ var MobileHumanize = {
2878
3508
  }
2879
3509
  }
2880
3510
  },
3511
+ async humanPress(page, targetOrKey, maybeKey, options = {}) {
3512
+ const hasTarget = typeof maybeKey === "string";
3513
+ const key = hasTarget ? maybeKey : targetOrKey;
3514
+ const pressOptions = hasTarget ? options : maybeKey || options;
3515
+ const {
3516
+ reactionDelay = 170,
3517
+ holdDelay = 42,
3518
+ focusDelay = 180,
3519
+ scrollIfNeeded = true,
3520
+ throwOnMissing = true,
3521
+ keyboardOptions = {}
3522
+ } = pressOptions || {};
3523
+ const targetDesc = hasTarget ? describeTarget(targetOrKey) : "current focus";
3524
+ logger7.start("humanPress", `key=${key}, target=${targetDesc}`);
3525
+ try {
3526
+ if (hasTarget) {
3527
+ await MobileHumanize.humanClick(page, targetOrKey, {
3528
+ reactionDelay: focusDelay,
3529
+ scrollIfNeeded,
3530
+ throwOnMissing
3531
+ });
3532
+ }
3533
+ await waitJitter(reactionDelay, 0.45);
3534
+ await page.keyboard.press(key, {
3535
+ ...keyboardOptions,
3536
+ delay: jitterMs(holdDelay, 0.5)
3537
+ });
3538
+ logger7.success("humanPress");
3539
+ return true;
3540
+ } catch (error) {
3541
+ logger7.fail("humanPress", error);
3542
+ throw error;
3543
+ }
3544
+ },
2881
3545
  async humanClear(page, selector) {
2882
3546
  const locator = page.locator(selector);
2883
3547
  await MobileHumanize.humanClick(page, locator, { scrollIfNeeded: true });
@@ -2928,15 +3592,18 @@ var MobileHumanize = {
2928
3592
  };
2929
3593
 
2930
3594
  // src/humanize.js
2931
- var resolveDeviceFromPage = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
3595
+ var resolveDeviceFromPage2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
2932
3596
  var resolveDelegate = (page) => {
2933
- return resolveDeviceFromPage(page) === Device.Mobile ? MobileHumanize : Humanize;
3597
+ return resolveDeviceFromPage2(page) === Device.Mobile ? MobileHumanize : Humanize;
2934
3598
  };
2935
3599
  var callDelegate = (method, page, args) => {
2936
3600
  const delegate = resolveDelegate(page);
2937
3601
  return delegate[method](page, ...args);
2938
3602
  };
2939
3603
  var Humanize2 = {
3604
+ // M = Machine: native/mechanical operations for compatibility paths.
3605
+ // These APIs intentionally do not humanize timing, motion, or intent.
3606
+ M: MachineHumanize,
2940
3607
  jitterMs(base, jitterPercent = 0.3) {
2941
3608
  return Humanize.jitterMs(base, jitterPercent);
2942
3609
  },
@@ -2965,6 +3632,12 @@ var Humanize2 = {
2965
3632
  humanType(page, selector, text, options = {}) {
2966
3633
  return callDelegate("humanType", page, [selector, text, options]);
2967
3634
  },
3635
+ humanPress(page, targetOrKey, maybeKey, options = {}) {
3636
+ if (typeof maybeKey === "string") {
3637
+ return callDelegate("humanPress", page, [targetOrKey, maybeKey, options]);
3638
+ }
3639
+ return callDelegate("humanPress", page, [targetOrKey, maybeKey || options]);
3640
+ },
2968
3641
  humanClear(page, selector) {
2969
3642
  return callDelegate("humanClear", page, [selector]);
2970
3643
  },