@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.js CHANGED
@@ -2120,7 +2120,7 @@ var Humanize = {
2120
2120
  },
2121
2121
  /**
2122
2122
  * 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
2123
- *
2123
+ *
2124
2124
  * @param {import('playwright').Page} page
2125
2125
  * @returns {Promise<void>}
2126
2126
  */
@@ -2136,7 +2136,7 @@ var Humanize = {
2136
2136
  },
2137
2137
  /**
2138
2138
  * 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
2139
- *
2139
+ *
2140
2140
  * @param {import('playwright').Page} page
2141
2141
  * @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
2142
2142
  */
@@ -2178,7 +2178,7 @@ var Humanize = {
2178
2178
  /**
2179
2179
  * 渐进式滚动到元素可见(仅处理 Y 轴滚动)
2180
2180
  * 返回 restore 方法,用于将滚动容器恢复到原位置
2181
- *
2181
+ *
2182
2182
  * @param {import('playwright').Page} page
2183
2183
  * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
2184
2184
  * @param {Object} [options]
@@ -2249,7 +2249,7 @@ var Humanize = {
2249
2249
  return { code: "VISIBLE", isFixed };
2250
2250
  });
2251
2251
  };
2252
- const getScrollableRect = async () => {
2252
+ const getScrollableRect2 = async () => {
2253
2253
  return await element.evaluate((el) => {
2254
2254
  const isScrollable = (node) => {
2255
2255
  const style = window.getComputedStyle(node);
@@ -2292,7 +2292,7 @@ var Humanize = {
2292
2292
  if (status.code === "OBSTRUCTED" && status.obstruction) {
2293
2293
  logger6.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
2294
2294
  }
2295
- const scrollRect = await getScrollableRect();
2295
+ const scrollRect = await getScrollableRect2();
2296
2296
  if (!scrollRect && status.isFixed) {
2297
2297
  logger6.warn("humanScroll | fixed \u5BB9\u5668\u5185\u4E14\u65E0\u53EF\u6EDA\u52A8\u7956\u5148\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
2298
2298
  return { element, didScroll };
@@ -2339,7 +2339,7 @@ var Humanize = {
2339
2339
  },
2340
2340
  /**
2341
2341
  * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
2342
- *
2342
+ *
2343
2343
  * @param {import('playwright').Page} page
2344
2344
  * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
2345
2345
  * @param {Object} [options]
@@ -2400,9 +2400,7 @@ var Humanize = {
2400
2400
  const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
2401
2401
  await cursor.actions.move({ x, y });
2402
2402
  await delay(this.jitterMs(reactionDelay, 0.4));
2403
- await page.mouse.click(x, y, {
2404
- delay: this.jitterMs(45, 0.45)
2405
- });
2403
+ await cursor.actions.click();
2406
2404
  await restoreOnce();
2407
2405
  logger6.success("humanClick");
2408
2406
  return true;
@@ -2447,7 +2445,7 @@ var Humanize = {
2447
2445
  * @param {import('playwright').Page} page
2448
2446
  * @param {string} selector - 输入框选择器
2449
2447
  * @param {string} text - 要输入的文本
2450
- * @param {Object} [options]
2448
+ * @param {Object} [options]
2451
2449
  * @param {number} [options.baseDelay=180] - 基础按键延迟 (ms),实际 ±40% 抖动
2452
2450
  * @param {number} [options.pauseProbability=0.08] - 停顿概率 (0-1)
2453
2451
  * @param {number} [options.pauseBase=800] - 停顿时长基础值 (ms),实际 ±50% 抖动
@@ -2487,6 +2485,43 @@ var Humanize = {
2487
2485
  throw error;
2488
2486
  }
2489
2487
  },
2488
+ /**
2489
+ * 人类化按键 - 模拟用户在当前焦点或指定目标上按下一次键。
2490
+ * @param {import('playwright').Page} page
2491
+ * @param {string|import('playwright').ElementHandle|import('playwright').Locator} targetOrKey - 目标或按键
2492
+ * @param {string|Object} [maybeKey] - 按键或选项
2493
+ * @param {Object} [options]
2494
+ */
2495
+ async humanPress(page, targetOrKey, maybeKey, options = {}) {
2496
+ const hasTarget = typeof maybeKey === "string";
2497
+ const key = hasTarget ? maybeKey : targetOrKey;
2498
+ const pressOptions = hasTarget ? options : maybeKey || options;
2499
+ const {
2500
+ reactionDelay = 180,
2501
+ holdDelay = 45,
2502
+ focusDelay = 180,
2503
+ scrollIfNeeded = true,
2504
+ throwOnMissing = true,
2505
+ keyboardOptions = {}
2506
+ } = pressOptions || {};
2507
+ const targetDesc = hasTarget ? typeof targetOrKey === "string" ? targetOrKey : "ElementHandle" : "current focus";
2508
+ logger6.start("humanPress", `key=${key}, target=${targetDesc}`);
2509
+ try {
2510
+ if (hasTarget) {
2511
+ await this.humanClick(page, targetOrKey, { reactionDelay: focusDelay, scrollIfNeeded, throwOnMissing });
2512
+ }
2513
+ await delay(this.jitterMs(reactionDelay, 0.45));
2514
+ await page.keyboard.press(key, {
2515
+ ...keyboardOptions,
2516
+ delay: this.jitterMs(holdDelay, 0.5)
2517
+ });
2518
+ logger6.success("humanPress");
2519
+ return true;
2520
+ } catch (error) {
2521
+ logger6.fail("humanPress", error);
2522
+ throw error;
2523
+ }
2524
+ },
2490
2525
  /**
2491
2526
  * 人类化清空输入框 - 模拟人类删除文本的行为
2492
2527
  * @param {import('playwright').Page} page
@@ -2575,6 +2610,141 @@ var Humanize = {
2575
2610
  }
2576
2611
  };
2577
2612
 
2613
+ // src/internals/humanize/machine.js
2614
+ var resolveDeviceFromPage = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
2615
+ var isPoint = (value) => value && typeof value === "object" && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
2616
+ var resolveNativeTarget = (page, target) => {
2617
+ if (!target) return null;
2618
+ if (typeof target === "string") {
2619
+ return page.locator(target);
2620
+ }
2621
+ return target;
2622
+ };
2623
+ var normalizeSelectorOptions = (options = {}) => ({
2624
+ targetParent: Boolean(options.targetParent ?? options.parent ?? false),
2625
+ pick: options.pick === "last" || options.last ? "last" : "first",
2626
+ visibleOnly: options.visibleOnly !== false,
2627
+ fallbackDomClick: Boolean(options.fallbackDomClick),
2628
+ forceMouse: Boolean(options.forceMouse)
2629
+ });
2630
+ var MachineHumanize = {
2631
+ async clickPoint(page, pointOrX, maybeY, options = {}) {
2632
+ const clickOptions = isPoint(pointOrX) && maybeY && typeof maybeY === "object" ? maybeY : options;
2633
+ const point = isPoint(pointOrX) ? pointOrX : { x: pointOrX, y: maybeY };
2634
+ const x = Number(point.x);
2635
+ const y = Number(point.y);
2636
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
2637
+ throw new Error(`Invalid click point: ${JSON.stringify(point)}`);
2638
+ }
2639
+ const resolvedDevice = resolveDeviceFromPage(page);
2640
+ if (resolvedDevice === Device.Mobile && !clickOptions.forceMouse && page.touchscreen && typeof page.touchscreen.tap === "function") {
2641
+ await page.touchscreen.tap(x, y);
2642
+ return true;
2643
+ }
2644
+ await page.mouse.click(x, y, clickOptions.mouseOptions || {});
2645
+ return true;
2646
+ },
2647
+ async click(page, target, options = {}) {
2648
+ if (isPoint(target)) {
2649
+ return MachineHumanize.clickPoint(page, target, void 0, options);
2650
+ }
2651
+ const nativeTarget = resolveNativeTarget(page, target);
2652
+ if (!nativeTarget || typeof nativeTarget.click !== "function") {
2653
+ throw new Error("Machine click target does not expose click()");
2654
+ }
2655
+ await nativeTarget.click(options.clickOptions || options);
2656
+ return true;
2657
+ },
2658
+ async focus(page, target, options = {}) {
2659
+ const nativeTarget = resolveNativeTarget(page, target);
2660
+ if (!nativeTarget || typeof nativeTarget.focus !== "function") {
2661
+ throw new Error("Machine focus target does not expose focus()");
2662
+ }
2663
+ await nativeTarget.focus(options);
2664
+ return true;
2665
+ },
2666
+ async fill(page, target, value, options = {}) {
2667
+ const nativeTarget = resolveNativeTarget(page, target);
2668
+ if (!nativeTarget || typeof nativeTarget.fill !== "function") {
2669
+ throw new Error("Machine fill target does not expose fill()");
2670
+ }
2671
+ await nativeTarget.fill(value, options);
2672
+ return true;
2673
+ },
2674
+ async keyboardType(page, text, options = {}) {
2675
+ await page.keyboard.type(text, options);
2676
+ return true;
2677
+ },
2678
+ async type(page, targetOrText, maybeText, options = {}) {
2679
+ if (maybeText == null || typeof maybeText === "object" && !Array.isArray(maybeText)) {
2680
+ return MachineHumanize.keyboardType(page, targetOrText, maybeText || options);
2681
+ }
2682
+ const nativeTarget = resolveNativeTarget(page, targetOrText);
2683
+ if (!nativeTarget || typeof nativeTarget.type !== "function") {
2684
+ throw new Error("Machine type target does not expose type()");
2685
+ }
2686
+ await nativeTarget.type(maybeText, options);
2687
+ return true;
2688
+ },
2689
+ async selectorCenterPoint(page, selector, options = {}) {
2690
+ const normalizedOptions = normalizeSelectorOptions(options);
2691
+ return page.evaluate(({ innerSelector, innerOptions }) => {
2692
+ const nodes = Array.from(document.querySelectorAll(innerSelector));
2693
+ const ordered = innerOptions.pick === "last" ? nodes.reverse() : nodes;
2694
+ let rect = null;
2695
+ for (const node of ordered) {
2696
+ if (!node) continue;
2697
+ const target = innerOptions.targetParent ? node.parentElement || node : node;
2698
+ rect = target.getBoundingClientRect?.() || null;
2699
+ const isVisible = Boolean(rect && rect.width > 0 && rect.height > 0);
2700
+ if (innerOptions.visibleOnly && !isVisible) continue;
2701
+ break;
2702
+ }
2703
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
2704
+ return {
2705
+ x: rect.left + rect.width / 2,
2706
+ y: rect.top + rect.height / 2
2707
+ };
2708
+ }, { innerSelector: selector, innerOptions: normalizedOptions });
2709
+ },
2710
+ async domClick(page, selector, options = {}) {
2711
+ const normalizedOptions = normalizeSelectorOptions(options);
2712
+ return page.evaluate(({ innerSelector, innerOptions }) => {
2713
+ const nodes = Array.from(document.querySelectorAll(innerSelector));
2714
+ const ordered = innerOptions.pick === "last" ? nodes.reverse() : nodes;
2715
+ let target = null;
2716
+ for (const node of ordered) {
2717
+ if (!node) continue;
2718
+ const candidate = innerOptions.targetParent ? node.parentElement || node : node;
2719
+ const rect = candidate.getBoundingClientRect?.() || null;
2720
+ const isVisible = Boolean(rect && rect.width > 0 && rect.height > 0);
2721
+ if (innerOptions.visibleOnly && !isVisible) continue;
2722
+ target = candidate;
2723
+ break;
2724
+ }
2725
+ if (!target || typeof target.click !== "function") return false;
2726
+ target.click();
2727
+ return true;
2728
+ }, { innerSelector: selector, innerOptions: normalizedOptions });
2729
+ },
2730
+ async clickSelectorCenter(page, selector, options = {}) {
2731
+ const normalizedOptions = normalizeSelectorOptions(options);
2732
+ try {
2733
+ const point = await MachineHumanize.selectorCenterPoint(page, selector, normalizedOptions);
2734
+ if (point) {
2735
+ await MachineHumanize.clickPoint(page, point, void 0, normalizedOptions);
2736
+ return true;
2737
+ }
2738
+ } catch (error) {
2739
+ if (!normalizedOptions.fallbackDomClick) throw error;
2740
+ }
2741
+ if (normalizedOptions.fallbackDomClick) {
2742
+ return MachineHumanize.domClick(page, selector, normalizedOptions);
2743
+ }
2744
+ return false;
2745
+ }
2746
+ };
2747
+
2578
2748
  // src/internals/humanize/shared.js
2579
2749
  import delay2 from "delay";
2580
2750
  var jitterMs = (base, jitterPercent = 0.3) => {
@@ -2609,16 +2779,43 @@ var logger7 = createInternalLogger("Humanize.Mobile");
2609
2779
  var initializedPages = /* @__PURE__ */ new WeakSet();
2610
2780
  var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
2611
2781
  var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
2782
+ var describeTarget = (target) => {
2783
+ if (target == null) return "Current Position";
2784
+ return typeof target === "string" ? target : "ElementHandle";
2785
+ };
2786
+ var clipBoxToViewport = (box, viewport) => {
2787
+ if (!box || !viewport) return box;
2788
+ const left = clamp(box.x, 0, viewport.width);
2789
+ const top = clamp(box.y, 0, viewport.height);
2790
+ const right = clamp(box.x + box.width, 0, viewport.width);
2791
+ const bottom = clamp(box.y + box.height, 0, viewport.height);
2792
+ if (right - left > 1 && bottom - top > 1) {
2793
+ return {
2794
+ x: left,
2795
+ y: top,
2796
+ width: right - left,
2797
+ height: bottom - top
2798
+ };
2799
+ }
2800
+ return box;
2801
+ };
2612
2802
  var checkElementVisibility = async (element) => {
2613
2803
  return element.evaluate((el) => {
2804
+ const targetStyle = window.getComputedStyle(el);
2805
+ if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
2806
+ return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
2807
+ }
2614
2808
  const rect = el.getBoundingClientRect();
2615
2809
  if (!rect || rect.width <= 0 || rect.height <= 0) {
2616
- return { code: "ZERO_DIMENSIONS", direction: "down" };
2810
+ return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6", direction: "down" };
2617
2811
  }
2618
2812
  const viewW = window.innerWidth;
2619
2813
  const viewH = window.innerHeight;
2620
- const cx = rect.left + rect.width / 2;
2621
- const cy = rect.top + rect.height / 2;
2814
+ const centerY = rect.top + rect.height / 2;
2815
+ let clipLeft = 0;
2816
+ let clipRight = viewW;
2817
+ let clipTop = 0;
2818
+ let clipBottom = viewH;
2622
2819
  let isFixed = false;
2623
2820
  for (let node = el; node && node !== document.body; node = node.parentElement) {
2624
2821
  const style = window.getComputedStyle(node);
@@ -2627,20 +2824,101 @@ var checkElementVisibility = async (element) => {
2627
2824
  break;
2628
2825
  }
2629
2826
  }
2630
- if ((isFixed ? cy < 0 || cy > viewH : cy < 12 || cy > viewH - 12) || cx < 0 || cx > viewW) {
2827
+ for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
2828
+ const style = window.getComputedStyle(node);
2829
+ if (!style) continue;
2830
+ const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
2831
+ const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
2832
+ if (!clipsX && !clipsY) continue;
2833
+ const nodeRect = node.getBoundingClientRect();
2834
+ if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
2835
+ if (clipsX) {
2836
+ clipLeft = Math.max(clipLeft, nodeRect.left);
2837
+ clipRight = Math.min(clipRight, nodeRect.right);
2838
+ }
2839
+ if (clipsY) {
2840
+ clipTop = Math.max(clipTop, nodeRect.top);
2841
+ clipBottom = Math.min(clipBottom, nodeRect.bottom);
2842
+ }
2843
+ }
2844
+ const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
2845
+ const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
2846
+ const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
2847
+ const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
2848
+ const visibleWidth = visibleRight - visibleLeft;
2849
+ const visibleHeight = visibleBottom - visibleTop;
2850
+ const cx = visibleLeft + visibleWidth / 2;
2851
+ const cy = visibleTop + visibleHeight / 2;
2852
+ if (visibleWidth <= 1 || visibleHeight <= 1) {
2631
2853
  return {
2632
2854
  code: "OUT_OF_VIEWPORT",
2633
- direction: cy < viewH / 2 ? "up" : "down",
2634
- cy,
2855
+ reason: "\u4E0D\u5728\u89C6\u53E3\u5185",
2856
+ direction: centerY < clipTop ? "up" : "down",
2857
+ cy: centerY,
2635
2858
  viewH,
2636
2859
  isFixed
2637
2860
  };
2638
2861
  }
2639
- const pointElement = document.elementFromPoint(cx, cy);
2640
- if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
2862
+ const interactiveSelector = [
2863
+ "button",
2864
+ '[role="button"]',
2865
+ "a[href]",
2866
+ "label",
2867
+ "input",
2868
+ "textarea",
2869
+ "select",
2870
+ "summary",
2871
+ '[contenteditable="true"]',
2872
+ '[tabindex]:not([tabindex="-1"])'
2873
+ ].join(",");
2874
+ const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
2875
+ const sameInteractiveTarget = (pointElement) => {
2876
+ if (!pointElement) return false;
2877
+ if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
2878
+ return true;
2879
+ }
2880
+ if (!targetInteractive || typeof pointElement.closest !== "function") {
2881
+ return false;
2882
+ }
2883
+ return pointElement.closest(interactiveSelector) === targetInteractive;
2884
+ };
2885
+ const describeElement = (node) => {
2886
+ if (!node) return null;
2887
+ const className = typeof node.className === "string" ? node.className : node.className && typeof node.className.baseVal === "string" ? node.className.baseVal : "";
2888
+ const rect2 = node.getBoundingClientRect?.();
2889
+ const style = window.getComputedStyle(node);
2890
+ return {
2891
+ tag: node.tagName,
2892
+ id: node.id || "",
2893
+ className,
2894
+ isFixed: Boolean(style && (style.position === "fixed" || style.position === "sticky")),
2895
+ top: rect2 ? rect2.top : null,
2896
+ bottom: rect2 ? rect2.bottom : null,
2897
+ left: rect2 ? rect2.left : null,
2898
+ right: rect2 ? rect2.right : null
2899
+ };
2900
+ };
2901
+ const samplePoints = [
2902
+ { x: cx, y: cy },
2903
+ { x: visibleLeft + Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
2904
+ { x: visibleRight - Math.min(8, Math.max(1, visibleWidth * 0.25)), y: cy },
2905
+ { x: cx, y: visibleTop + Math.min(8, Math.max(1, visibleHeight * 0.25)) },
2906
+ { x: cx, y: visibleBottom - Math.min(8, Math.max(1, visibleHeight * 0.25)) }
2907
+ ];
2908
+ let obstruction = null;
2909
+ for (const point of samplePoints) {
2910
+ const pointElement = document.elementFromPoint(point.x, point.y);
2911
+ if (sameInteractiveTarget(pointElement)) {
2912
+ return { code: "VISIBLE", isFixed };
2913
+ }
2914
+ obstruction = obstruction || describeElement(pointElement);
2915
+ }
2916
+ if (obstruction) {
2641
2917
  return {
2642
2918
  code: "OBSTRUCTED",
2919
+ reason: "\u88AB\u906E\u6321",
2643
2920
  direction: cy > viewH / 2 ? "down" : "up",
2921
+ obstruction,
2644
2922
  cy,
2645
2923
  viewH,
2646
2924
  isFixed
@@ -2649,17 +2927,228 @@ var checkElementVisibility = async (element) => {
2649
2927
  return { code: "VISIBLE", isFixed };
2650
2928
  });
2651
2929
  };
2930
+ var resolveSafeTapPoint = async (element) => {
2931
+ return element.evaluate((el) => {
2932
+ const rect = el.getBoundingClientRect();
2933
+ if (!rect || rect.width <= 0 || rect.height <= 0) {
2934
+ return null;
2935
+ }
2936
+ const viewW = window.innerWidth;
2937
+ const viewH = window.innerHeight;
2938
+ let clipLeft = 0;
2939
+ let clipRight = viewW;
2940
+ let clipTop = 0;
2941
+ let clipBottom = viewH;
2942
+ for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
2943
+ const style = window.getComputedStyle(node);
2944
+ if (!style) continue;
2945
+ const clipsX = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowX);
2946
+ const clipsY = ["auto", "scroll", "overlay", "hidden", "clip"].includes(style.overflowY);
2947
+ if (!clipsX && !clipsY) continue;
2948
+ const nodeRect = node.getBoundingClientRect();
2949
+ if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
2950
+ if (clipsX) {
2951
+ clipLeft = Math.max(clipLeft, nodeRect.left);
2952
+ clipRight = Math.min(clipRight, nodeRect.right);
2953
+ }
2954
+ if (clipsY) {
2955
+ clipTop = Math.max(clipTop, nodeRect.top);
2956
+ clipBottom = Math.min(clipBottom, nodeRect.bottom);
2957
+ }
2958
+ }
2959
+ const visibleLeft = Math.max(clipLeft, Math.min(clipRight, rect.left));
2960
+ const visibleRight = Math.max(clipLeft, Math.min(clipRight, rect.right));
2961
+ const visibleTop = Math.max(clipTop, Math.min(clipBottom, rect.top));
2962
+ const visibleBottom = Math.max(clipTop, Math.min(clipBottom, rect.bottom));
2963
+ const visibleWidth = visibleRight - visibleLeft;
2964
+ const visibleHeight = visibleBottom - visibleTop;
2965
+ if (visibleWidth <= 1 || visibleHeight <= 1) {
2966
+ return null;
2967
+ }
2968
+ const interactiveSelector = [
2969
+ "button",
2970
+ '[role="button"]',
2971
+ "a[href]",
2972
+ "label",
2973
+ "input",
2974
+ "textarea",
2975
+ "select",
2976
+ "summary",
2977
+ '[contenteditable="true"]',
2978
+ '[tabindex]:not([tabindex="-1"])'
2979
+ ].join(",");
2980
+ const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
2981
+ const sameInteractiveTarget = (pointElement) => {
2982
+ if (!pointElement) return false;
2983
+ if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
2984
+ return true;
2985
+ }
2986
+ if (!targetInteractive || typeof pointElement.closest !== "function") {
2987
+ return false;
2988
+ }
2989
+ return pointElement.closest(interactiveSelector) === targetInteractive;
2990
+ };
2991
+ const cx = visibleLeft + visibleWidth / 2;
2992
+ const cy = visibleTop + visibleHeight / 2;
2993
+ const smallX = Math.min(12, Math.max(2, visibleWidth * 0.18));
2994
+ const smallY = Math.min(12, Math.max(2, visibleHeight * 0.18));
2995
+ const points = [
2996
+ { x: cx, y: cy },
2997
+ { x: visibleLeft + smallX, y: visibleTop + smallY },
2998
+ { x: visibleRight - smallX, y: visibleTop + smallY },
2999
+ { x: visibleLeft + smallX, y: visibleBottom - smallY },
3000
+ { x: visibleRight - smallX, y: visibleBottom - smallY },
3001
+ { x: cx, y: visibleTop + Math.min(10, Math.max(2, visibleHeight * 0.2)) },
3002
+ { x: cx, y: visibleBottom - Math.min(10, Math.max(2, visibleHeight * 0.2)) },
3003
+ { x: visibleLeft + Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy },
3004
+ { x: visibleRight - Math.min(10, Math.max(2, visibleWidth * 0.2)), y: cy }
3005
+ ];
3006
+ const safePoints = points.filter((point) => {
3007
+ const pointElement = document.elementFromPoint(point.x, point.y);
3008
+ return sameInteractiveTarget(pointElement);
3009
+ });
3010
+ const candidates = safePoints.length ? safePoints : points;
3011
+ const chosen = candidates[Math.floor(Math.random() * candidates.length)];
3012
+ if (!chosen) return null;
3013
+ return {
3014
+ x: chosen.x,
3015
+ y: chosen.y
3016
+ };
3017
+ });
3018
+ };
3019
+ var getScrollableRect = async (element) => {
3020
+ return element.evaluate((el) => {
3021
+ const isScrollable = (node) => {
3022
+ const style = window.getComputedStyle(node);
3023
+ if (!style) return false;
3024
+ const overflowY = style.overflowY;
3025
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3026
+ return node.scrollHeight > node.clientHeight + 1;
3027
+ };
3028
+ let current = el;
3029
+ while (current && current !== document.body) {
3030
+ if (isScrollable(current)) {
3031
+ const rect = current.getBoundingClientRect();
3032
+ if (rect && rect.width > 0 && rect.height > 0) {
3033
+ return {
3034
+ x: rect.x,
3035
+ y: rect.y,
3036
+ width: rect.width,
3037
+ height: rect.height
3038
+ };
3039
+ }
3040
+ }
3041
+ current = current.parentElement;
3042
+ }
3043
+ return null;
3044
+ });
3045
+ };
3046
+ var scrollScrollableAncestor = async (element, deltaY) => {
3047
+ return element.evaluate((el, amount) => {
3048
+ const isScrollable = (node) => {
3049
+ const style = window.getComputedStyle(node);
3050
+ if (!style) return false;
3051
+ const overflowY = style.overflowY;
3052
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3053
+ return node.scrollHeight > node.clientHeight + 1;
3054
+ };
3055
+ let current = el;
3056
+ while (current && current !== document.body) {
3057
+ if (isScrollable(current)) {
3058
+ const beforeTop2 = current.scrollTop;
3059
+ current.scrollTop = beforeTop2 + amount;
3060
+ return {
3061
+ scroller: true,
3062
+ moved: current.scrollTop !== beforeTop2,
3063
+ scrollTop: current.scrollTop
3064
+ };
3065
+ }
3066
+ current = current.parentElement;
3067
+ }
3068
+ const beforeTop = window.scrollY;
3069
+ window.scrollBy(0, amount);
3070
+ return {
3071
+ scroller: null,
3072
+ moved: window.scrollY !== beforeTop,
3073
+ scrollTop: window.scrollY
3074
+ };
3075
+ }, deltaY);
3076
+ };
3077
+ var scrollAwayFromObstruction = async (element, status) => {
3078
+ return element.evaluate((el, innerStatus) => {
3079
+ const isScrollable = (node) => {
3080
+ const style = window.getComputedStyle(node);
3081
+ if (!style) return false;
3082
+ const overflowY = style.overflowY;
3083
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3084
+ return node.scrollHeight > node.clientHeight + 1;
3085
+ };
3086
+ let scroller = el;
3087
+ while (scroller && scroller !== document.body) {
3088
+ if (isScrollable(scroller)) break;
3089
+ scroller = scroller.parentElement;
3090
+ }
3091
+ if (!scroller || scroller === document.body) {
3092
+ return { moved: false, scrollTop: 0, deltaY: 0 };
3093
+ }
3094
+ const rect = el.getBoundingClientRect();
3095
+ const scrollerRect = scroller.getBoundingClientRect();
3096
+ const obstruction = innerStatus?.obstruction || {};
3097
+ const obstructionTop = Number(obstruction.top);
3098
+ const obstructionBottom = Number(obstruction.bottom);
3099
+ const obstructionMiddle = Number.isFinite(obstructionTop) && Number.isFinite(obstructionBottom) ? (obstructionTop + obstructionBottom) / 2 : scrollerRect.top + scrollerRect.height / 2;
3100
+ const scrollerMiddle = scrollerRect.top + scrollerRect.height / 2;
3101
+ const padding = 18;
3102
+ let deltaY = 0;
3103
+ if (Number.isFinite(obstructionTop) && rect.bottom > obstructionTop && obstructionTop >= scrollerRect.top && obstructionTop <= scrollerRect.bottom && obstructionMiddle >= scrollerMiddle) {
3104
+ deltaY = rect.bottom - obstructionTop + padding;
3105
+ } else if (Number.isFinite(obstructionBottom) && rect.top < obstructionBottom && obstructionBottom >= scrollerRect.top && obstructionBottom <= scrollerRect.bottom && obstructionMiddle < scrollerMiddle) {
3106
+ deltaY = rect.top - obstructionBottom - padding;
3107
+ } else {
3108
+ const fallbackDistance = Math.max(48, Math.min(180, scrollerRect.height * 0.45));
3109
+ deltaY = obstructionMiddle >= scrollerMiddle ? fallbackDistance : -fallbackDistance;
3110
+ }
3111
+ const beforeTop = scroller.scrollTop;
3112
+ scroller.scrollTop = beforeTop + deltaY;
3113
+ return {
3114
+ moved: Math.abs(scroller.scrollTop - beforeTop) > 2,
3115
+ scrollTop: scroller.scrollTop,
3116
+ deltaY
3117
+ };
3118
+ }, status);
3119
+ };
2652
3120
  var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
2653
3121
  const viewport = resolveViewport(page);
2654
- const distance = clamp(Math.abs(deltaY), 80, Math.max(120, viewport.height * 0.72));
3122
+ const rawRect = options.rect || null;
3123
+ const rect = rawRect ? {
3124
+ x: clamp(rawRect.x, 0, viewport.width),
3125
+ y: clamp(rawRect.y, 0, viewport.height),
3126
+ width: clamp(rawRect.width, 0, viewport.width),
3127
+ height: clamp(rawRect.height, 0, viewport.height)
3128
+ } : null;
3129
+ const area = rect && rect.width > 24 && rect.height > 48 ? {
3130
+ left: rect.x,
3131
+ right: Math.min(viewport.width, rect.x + rect.width),
3132
+ top: rect.y,
3133
+ bottom: Math.min(viewport.height, rect.y + rect.height)
3134
+ } : {
3135
+ left: 0,
3136
+ right: viewport.width,
3137
+ top: 0,
3138
+ bottom: viewport.height
3139
+ };
3140
+ const areaWidth = Math.max(1, area.right - area.left);
3141
+ const areaHeight = Math.max(1, area.bottom - area.top);
3142
+ const maxDistance = rect ? Math.max(60, areaHeight * 0.72) : Math.max(120, viewport.height * 0.72);
3143
+ const distance = clamp(Math.abs(deltaY), Math.min(80, maxDistance), maxDistance);
2655
3144
  const direction = deltaY >= 0 ? 1 : -1;
2656
3145
  const startX = clamp(
2657
- viewport.width * (0.45 + Math.random() * 0.1),
3146
+ area.left + areaWidth * (0.45 + Math.random() * 0.1),
2658
3147
  24,
2659
3148
  viewport.width - 24
2660
3149
  );
2661
- 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);
2662
- const endY = clamp(startY - direction * distance, 24, viewport.height - 24);
3150
+ 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);
3151
+ const endY = clamp(startY - direction * distance, Math.max(16, area.top + 12), Math.min(viewport.height - 16, area.bottom - 12));
2663
3152
  const steps = Math.max(4, Math.round(Number(options.steps || 6)));
2664
3153
  const durationMs = jitterMs(options.durationMs || 320, 0.35);
2665
3154
  let client = null;
@@ -2762,8 +3251,11 @@ var MobileHumanize = {
2762
3251
  maxDurationMs = maxSteps * 280 + 1200,
2763
3252
  throwOnMissing = false
2764
3253
  } = options;
3254
+ const targetDesc = describeTarget(target);
3255
+ logger7.start("humanScroll", `target=${targetDesc}`);
2765
3256
  const element = await resolveElement(page, target, { throwOnMissing });
2766
3257
  if (!element) {
3258
+ logger7.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${targetDesc}`);
2767
3259
  return { element: null, didScroll: false, restore: null };
2768
3260
  }
2769
3261
  const startTime = Date.now();
@@ -2771,17 +3263,133 @@ var MobileHumanize = {
2771
3263
  for (let i = 0; i < maxSteps; i += 1) {
2772
3264
  const status = await checkElementVisibility(element);
2773
3265
  if (status.code === "VISIBLE") {
3266
+ if (status.isFixed) {
3267
+ logger7.info("humanScroll | fixed/sticky \u5BB9\u5668\u5185\uFF0C\u8DF3\u8FC7\u6EDA\u52A8");
3268
+ } else {
3269
+ logger7.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
3270
+ }
3271
+ logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
3272
+ return { element, didScroll, restore: null };
3273
+ }
3274
+ if (status.code === "ZERO_DIMENSIONS" || status.code === "NOT_INTERACTABLE") {
3275
+ logger7.warn(`humanScroll | \u5143\u7D20\u4E0D\u53EF\u6EDA\u52A8\u81F3\u53EF\u70B9\u51FB\u72B6\u6001: ${status.reason || status.code}`);
2774
3276
  return { element, didScroll, restore: null };
2775
3277
  }
3278
+ const scrollRect = await getScrollableRect(element);
3279
+ if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
3280
+ logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
3281
+ return { element, didScroll, restore: null };
3282
+ }
3283
+ if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
3284
+ const moved = await scrollAwayFromObstruction(element, status);
3285
+ if (moved.moved) {
3286
+ logger7.debug(`humanScroll | sticky/fixed \u906E\u6321\u8865\u507F\u6EDA\u52A8 top=${Math.round(moved.scrollTop || 0)}`);
3287
+ await waitJitter(90, 0.3);
3288
+ didScroll = true;
3289
+ continue;
3290
+ }
3291
+ }
2776
3292
  if (Date.now() - startTime > maxDurationMs) {
2777
- logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms)`);
3293
+ logger7.warn(`humanScroll | mobile timeout (${maxDurationMs}ms, status=${status.code}, direction=${status.direction || "unknown"}, fixed=${Boolean(status.isFixed)})`);
2778
3294
  return { element, didScroll, restore: null };
2779
3295
  }
2780
- const distance = minStep + Math.random() * Math.max(1, maxStep - minStep);
2781
- const deltaY = status.direction === "up" ? -distance : distance;
2782
- await dispatchTouchSwipe(page, deltaY);
3296
+ const stepMin = scrollRect ? Math.min(minStep, Math.max(60, scrollRect.height * 0.4)) : minStep;
3297
+ const stepMax = scrollRect ? Math.min(maxStep, Math.max(stepMin + 40, scrollRect.height * 0.8)) : maxStep;
3298
+ logger7.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason || status.code} ${status.direction ? `(${status.direction})` : ""}`);
3299
+ const distance = stepMin + Math.random() * Math.max(1, stepMax - stepMin);
3300
+ let deltaY = status.direction === "up" ? -distance : distance;
3301
+ if (status.code === "OBSTRUCTED") {
3302
+ if (status.obstruction?.isFixed && status.obstruction.top != null) {
3303
+ const obstructionMiddle = (Number(status.obstruction.top || 0) + Number(status.obstruction.bottom || status.obstruction.top || 0)) / 2;
3304
+ const visibleMiddle = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
3305
+ deltaY = obstructionMiddle < visibleMiddle ? -distance : distance;
3306
+ } else {
3307
+ const halfY = scrollRect ? scrollRect.y + scrollRect.height / 2 : status.viewH / 2;
3308
+ deltaY = status.cy > halfY ? distance : -distance;
3309
+ }
3310
+ }
3311
+ const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
3312
+ const beforeState = scrollRect ? await element.evaluate((el) => {
3313
+ const isScrollable = (node) => {
3314
+ const style = window.getComputedStyle(node);
3315
+ if (!style) return false;
3316
+ const overflowY = style.overflowY;
3317
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3318
+ return node.scrollHeight > node.clientHeight + 1;
3319
+ };
3320
+ let current = el;
3321
+ while (current && current !== document.body) {
3322
+ if (isScrollable(current)) {
3323
+ return {
3324
+ kind: "element",
3325
+ top: current.scrollTop,
3326
+ left: current.scrollLeft
3327
+ };
3328
+ }
3329
+ current = current.parentElement;
3330
+ }
3331
+ return { kind: "window", top: window.scrollY, left: window.scrollX };
3332
+ }) : null;
3333
+ await dispatchTouchSwipe(page, deltaY, { rect: scrollRect });
3334
+ if (scrollRect && beforeWindowState) {
3335
+ const afterWindowState = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
3336
+ if (Math.abs(afterWindowState.x - beforeWindowState.x) > 2 || Math.abs(afterWindowState.y - beforeWindowState.y) > 2) {
3337
+ await page.evaluate((state) => window.scrollTo(state.x, state.y), beforeWindowState);
3338
+ logger7.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
3339
+ }
3340
+ }
3341
+ if (scrollRect && beforeState) {
3342
+ const afterState = await element.evaluate((el) => {
3343
+ const isScrollable = (node) => {
3344
+ const style = window.getComputedStyle(node);
3345
+ if (!style) return false;
3346
+ const overflowY = style.overflowY;
3347
+ if (!["auto", "scroll", "overlay"].includes(overflowY)) return false;
3348
+ return node.scrollHeight > node.clientHeight + 1;
3349
+ };
3350
+ let current = el;
3351
+ while (current && current !== document.body) {
3352
+ if (isScrollable(current)) {
3353
+ return {
3354
+ kind: "element",
3355
+ top: current.scrollTop,
3356
+ left: current.scrollLeft
3357
+ };
3358
+ }
3359
+ current = current.parentElement;
3360
+ }
3361
+ return { kind: "window", top: window.scrollY, left: window.scrollX };
3362
+ });
3363
+ const topDelta = Number(afterState.top || 0) - Number(beforeState.top || 0);
3364
+ const leftDelta = Number(afterState.left || 0) - Number(beforeState.left || 0);
3365
+ const expectedDelta = Number(deltaY || 0);
3366
+ const moved = beforeState.kind !== afterState.kind || Math.abs(topDelta) > 2 || Math.abs(leftDelta) > 2;
3367
+ if (!moved) {
3368
+ const fallback = await scrollScrollableAncestor(element, deltaY);
3369
+ 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)}`);
3370
+ } 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)) {
3371
+ const residualDelta = expectedDelta - topDelta;
3372
+ if (Math.sign(residualDelta) === Math.sign(expectedDelta) && Math.abs(residualDelta) > 24) {
3373
+ const fallback = await scrollScrollableAncestor(element, residualDelta);
3374
+ 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)}`);
3375
+ }
3376
+ }
3377
+ }
2783
3378
  didScroll = true;
2784
3379
  }
3380
+ try {
3381
+ await element.scrollIntoViewIfNeeded?.();
3382
+ await waitJitter(80, 0.3);
3383
+ const finalStatus = await checkElementVisibility(element);
3384
+ if (finalStatus.code === "VISIBLE") {
3385
+ logger7.info("humanScroll | \u539F\u751F scrollIntoViewIfNeeded \u515C\u5E95\u6210\u529F");
3386
+ logger7.success("humanScroll", didScroll ? "\u5DF2\u6EDA\u52A8" : "\u65E0\u9700\u6EDA\u52A8");
3387
+ return { element, didScroll: true, restore: null };
3388
+ }
3389
+ } catch (fallbackError) {
3390
+ logger7.debug(`humanScroll | native fallback failed: ${fallbackError?.message || fallbackError}`);
3391
+ }
3392
+ logger7.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
2785
3393
  return { element, didScroll, restore: null };
2786
3394
  },
2787
3395
  async humanClick(page, target, options = {}) {
@@ -2790,29 +3398,51 @@ var MobileHumanize = {
2790
3398
  throwOnMissing = true,
2791
3399
  scrollIfNeeded = true
2792
3400
  } = options;
2793
- if (target == null) {
2794
- const viewport = resolveViewport(page);
3401
+ const targetDesc = describeTarget(target);
3402
+ logger7.start("humanClick", `target=${targetDesc}`);
3403
+ try {
3404
+ if (target == null) {
3405
+ const viewport = resolveViewport(page);
3406
+ await waitJitter(reactionDelay, 0.45);
3407
+ await tapPoint(page, {
3408
+ x: viewport.width * (0.45 + Math.random() * 0.1),
3409
+ y: viewport.height * (0.48 + Math.random() * 0.12)
3410
+ });
3411
+ logger7.success("humanClick", "Tapped current position");
3412
+ return true;
3413
+ }
3414
+ const element = await resolveElement(page, target, { throwOnMissing });
3415
+ if (!element) {
3416
+ logger7.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
3417
+ return false;
3418
+ }
3419
+ if (scrollIfNeeded) {
3420
+ await MobileHumanize.humanScroll(page, element, { throwOnMissing });
3421
+ }
3422
+ const status = await checkElementVisibility(element).catch(() => null);
3423
+ if (status && status.code !== "VISIBLE") {
3424
+ const message = `\u5143\u7D20\u4E0D\u53EF\u70B9\u51FB: ${status.reason || status.code}`;
3425
+ if (throwOnMissing) throw new Error(message);
3426
+ logger7.warn(`humanClick: ${message}\uFF0C\u8DF3\u8FC7\u70B9\u51FB`);
3427
+ return false;
3428
+ }
3429
+ const box = await element.boundingBox();
3430
+ if (!box) {
3431
+ if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
3432
+ logger7.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
3433
+ return false;
3434
+ }
2795
3435
  await waitJitter(reactionDelay, 0.45);
2796
- await tapPoint(page, {
2797
- x: viewport.width * (0.45 + Math.random() * 0.1),
2798
- y: viewport.height * (0.48 + Math.random() * 0.12)
2799
- });
3436
+ const safePoint = await resolveSafeTapPoint(element).catch(() => null);
3437
+ const tapTarget = safePoint || randomPointInBox(clipBoxToViewport(box, resolveViewport(page)), 0.2);
3438
+ await tapPoint(page, tapTarget);
3439
+ await waitJitter(120, 0.35);
3440
+ logger7.success("humanClick");
2800
3441
  return true;
3442
+ } catch (error) {
3443
+ logger7.fail("humanClick", error);
3444
+ throw error;
2801
3445
  }
2802
- const element = await resolveElement(page, target, { throwOnMissing });
2803
- if (!element) return false;
2804
- if (scrollIfNeeded) {
2805
- await MobileHumanize.humanScroll(page, element, { throwOnMissing });
2806
- }
2807
- const box = await element.boundingBox();
2808
- if (!box) {
2809
- if (throwOnMissing) throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
2810
- return false;
2811
- }
2812
- await waitJitter(reactionDelay, 0.45);
2813
- await tapPoint(page, randomPointInBox(box, 0.2));
2814
- await waitJitter(120, 0.35);
2815
- return true;
2816
3446
  },
2817
3447
  async randomSleep(baseMs, jitterPercent = 0.3) {
2818
3448
  await waitJitter(baseMs, jitterPercent);
@@ -2850,6 +3480,40 @@ var MobileHumanize = {
2850
3480
  }
2851
3481
  }
2852
3482
  },
3483
+ async humanPress(page, targetOrKey, maybeKey, options = {}) {
3484
+ const hasTarget = typeof maybeKey === "string";
3485
+ const key = hasTarget ? maybeKey : targetOrKey;
3486
+ const pressOptions = hasTarget ? options : maybeKey || options;
3487
+ const {
3488
+ reactionDelay = 170,
3489
+ holdDelay = 42,
3490
+ focusDelay = 180,
3491
+ scrollIfNeeded = true,
3492
+ throwOnMissing = true,
3493
+ keyboardOptions = {}
3494
+ } = pressOptions || {};
3495
+ const targetDesc = hasTarget ? describeTarget(targetOrKey) : "current focus";
3496
+ logger7.start("humanPress", `key=${key}, target=${targetDesc}`);
3497
+ try {
3498
+ if (hasTarget) {
3499
+ await MobileHumanize.humanClick(page, targetOrKey, {
3500
+ reactionDelay: focusDelay,
3501
+ scrollIfNeeded,
3502
+ throwOnMissing
3503
+ });
3504
+ }
3505
+ await waitJitter(reactionDelay, 0.45);
3506
+ await page.keyboard.press(key, {
3507
+ ...keyboardOptions,
3508
+ delay: jitterMs(holdDelay, 0.5)
3509
+ });
3510
+ logger7.success("humanPress");
3511
+ return true;
3512
+ } catch (error) {
3513
+ logger7.fail("humanPress", error);
3514
+ throw error;
3515
+ }
3516
+ },
2853
3517
  async humanClear(page, selector) {
2854
3518
  const locator = page.locator(selector);
2855
3519
  await MobileHumanize.humanClick(page, locator, { scrollIfNeeded: true });
@@ -2900,15 +3564,18 @@ var MobileHumanize = {
2900
3564
  };
2901
3565
 
2902
3566
  // src/humanize.js
2903
- var resolveDeviceFromPage = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
3567
+ var resolveDeviceFromPage2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
2904
3568
  var resolveDelegate = (page) => {
2905
- return resolveDeviceFromPage(page) === Device.Mobile ? MobileHumanize : Humanize;
3569
+ return resolveDeviceFromPage2(page) === Device.Mobile ? MobileHumanize : Humanize;
2906
3570
  };
2907
3571
  var callDelegate = (method, page, args) => {
2908
3572
  const delegate = resolveDelegate(page);
2909
3573
  return delegate[method](page, ...args);
2910
3574
  };
2911
3575
  var Humanize2 = {
3576
+ // M = Machine: native/mechanical operations for compatibility paths.
3577
+ // These APIs intentionally do not humanize timing, motion, or intent.
3578
+ M: MachineHumanize,
2912
3579
  jitterMs(base, jitterPercent = 0.3) {
2913
3580
  return Humanize.jitterMs(base, jitterPercent);
2914
3581
  },
@@ -2937,6 +3604,12 @@ var Humanize2 = {
2937
3604
  humanType(page, selector, text, options = {}) {
2938
3605
  return callDelegate("humanType", page, [selector, text, options]);
2939
3606
  },
3607
+ humanPress(page, targetOrKey, maybeKey, options = {}) {
3608
+ if (typeof maybeKey === "string") {
3609
+ return callDelegate("humanPress", page, [targetOrKey, maybeKey, options]);
3610
+ }
3611
+ return callDelegate("humanPress", page, [targetOrKey, maybeKey || options]);
3612
+ },
2940
3613
  humanClear(page, selector) {
2941
3614
  return callDelegate("humanClear", page, [selector]);
2942
3615
  },