@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/README.md +2 -0
- package/dist/index.cjs +721 -48
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +721 -48
- package/dist/index.js.map +4 -4
- package/index.d.ts +43 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
2649
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2662
|
-
|
|
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
|
|
2668
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
2690
|
-
const endY = clamp(startY - direction * distance,
|
|
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
|
|
2809
|
-
const
|
|
2810
|
-
|
|
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
|
-
|
|
2822
|
-
|
|
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
|
|
2825
|
-
|
|
2826
|
-
|
|
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
|
|
3595
|
+
var resolveDeviceFromPage2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
|
|
2932
3596
|
var resolveDelegate = (page) => {
|
|
2933
|
-
return
|
|
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
|
},
|