@skrillex1224/playwright-toolkit 2.1.239 → 2.1.241

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
@@ -56,7 +56,7 @@ var createActorInfo = (info) => {
56
56
  const normalizeShare2 = (value) => {
57
57
  const raw = value && typeof value === "object" ? value : {};
58
58
  const modeRaw = String(raw.mode || "dom").trim().toLowerCase();
59
- const mode = modeRaw === "response" ? "response" : "dom";
59
+ const mode = ["dom", "response", "custom"].includes(modeRaw) ? modeRaw : "dom";
60
60
  const prefix = String(raw.prefix || "").trim();
61
61
  const rawXurl = Array.isArray(raw.xurl) ? raw.xurl : [];
62
62
  const normalizeMatcherList = (input) => {
@@ -214,7 +214,7 @@ var ActorInfo = {
214
214
  path: "/",
215
215
  device: Device.Mobile,
216
216
  share: {
217
- mode: "dom",
217
+ mode: "custom",
218
218
  prefix: "",
219
219
  xurl: []
220
220
  }
@@ -723,14 +723,16 @@ var expandScrollableContent = async (page, options = {}) => {
723
723
  expandDocumentElements: options.expandDocumentElements === true
724
724
  });
725
725
  };
726
- var pinBottomAffixedElements = async (page, options = {}) => {
726
+ var adjustAffixedElementsForExpandedScreenshot = async (page, options = {}) => {
727
727
  return await page.evaluate(({ className, targetHeight }) => {
728
+ const viewportWidth = window.innerWidth || document.documentElement?.clientWidth || document.body?.clientWidth || 0;
728
729
  const viewportHeight = window.innerHeight || document.documentElement?.clientHeight || document.body?.clientHeight || 0;
730
+ const scrollY = window.scrollY || window.pageYOffset || 0;
729
731
  const safeTargetHeight = Math.ceil(Number(targetHeight) || 0);
730
- const deltaY = safeTargetHeight - viewportHeight;
731
- if (deltaY <= 1) {
732
+ if (safeTargetHeight <= viewportHeight + 1) {
732
733
  return 0;
733
734
  }
735
+ const hasOwn = (source, key) => Object.prototype.hasOwnProperty.call(source, key);
734
736
  const isVisible = (el, style, rect) => {
735
737
  if (!el || !style || !rect) return false;
736
738
  if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") {
@@ -739,7 +741,7 @@ var pinBottomAffixedElements = async (page, options = {}) => {
739
741
  if (Number(style.opacity) === 0) return false;
740
742
  return rect.width > 0 && rect.height > 0;
741
743
  };
742
- const bottomThreshold = Math.max(24, Math.min(96, viewportHeight * 0.12));
744
+ const edgeThreshold = Math.max(24, Math.min(96, viewportHeight * 0.12));
743
745
  const maxAffixedHeight = Math.max(56, viewportHeight * 0.65);
744
746
  const candidates = [];
745
747
  document.querySelectorAll("*").forEach((el) => {
@@ -749,28 +751,49 @@ var pinBottomAffixedElements = async (page, options = {}) => {
749
751
  const rect = el.getBoundingClientRect();
750
752
  if (!isVisible(el, style, rect)) return;
751
753
  if (rect.height > maxAffixedHeight) return;
752
- if (rect.bottom < viewportHeight - bottomThreshold) return;
754
+ if (rect.width < viewportWidth * 0.25 && rect.height < 80) return;
755
+ const top = Number.parseFloat(style.top);
753
756
  const bottom = Number.parseFloat(style.bottom);
757
+ const hasTopRule = Number.isFinite(top) && top <= Math.max(160, viewportHeight * 0.25);
754
758
  const hasBottomRule = Number.isFinite(bottom) && bottom <= Math.max(160, viewportHeight * 0.25);
755
- const isNearViewportBottom = rect.bottom >= viewportHeight - bottomThreshold;
756
- if (!hasBottomRule && !isNearViewportBottom) return;
757
- candidates.push(el);
759
+ const isNearViewportTop = rect.top <= edgeThreshold;
760
+ const isNearViewportBottom = rect.bottom >= viewportHeight - edgeThreshold;
761
+ const edge = (hasBottomRule || isNearViewportBottom) && !(hasTopRule || isNearViewportTop) ? "bottom" : "top";
762
+ if (position === "fixed" && edge === "top" && !hasTopRule && !isNearViewportTop) return;
763
+ if (position === "fixed" && edge === "bottom" && !hasBottomRule && !isNearViewportBottom) return;
764
+ if (position === "sticky" && !hasTopRule && !hasBottomRule && !isNearViewportTop && !isNearViewportBottom) return;
765
+ candidates.push({ el, position, edge });
758
766
  });
759
- const candidateSet = new Set(candidates);
760
- const topLevelCandidates = candidates.filter((el) => {
767
+ const candidateSet = new Set(candidates.map(({ el }) => el));
768
+ const topLevelCandidates = candidates.filter(({ el }) => {
761
769
  for (let parent = el.parentElement; parent; parent = parent.parentElement) {
762
770
  if (candidateSet.has(parent)) return false;
763
771
  }
764
772
  return true;
765
773
  });
766
- topLevelCandidates.forEach((el) => {
767
- if (!Object.prototype.hasOwnProperty.call(el.dataset, "pkBottomPinned")) {
768
- el.dataset.pkBottomPinned = "1";
774
+ topLevelCandidates.forEach(({ el, position, edge }) => {
775
+ if (!hasOwn(el.dataset, "pkAffixedAdjusted")) {
776
+ el.dataset.pkAffixedAdjusted = "1";
777
+ el.dataset.pkOrigPosition = el.style.getPropertyValue("position") || "";
778
+ el.dataset.pkOrigPositionPriority = el.style.getPropertyPriority("position") || "";
779
+ el.dataset.pkOrigTop = el.style.getPropertyValue("top") || "";
780
+ el.dataset.pkOrigTopPriority = el.style.getPropertyPriority("top") || "";
781
+ el.dataset.pkOrigBottom = el.style.getPropertyValue("bottom") || "";
782
+ el.dataset.pkOrigBottomPriority = el.style.getPropertyPriority("bottom") || "";
769
783
  el.dataset.pkOrigTranslate = el.style.getPropertyValue("translate") || "";
770
784
  el.dataset.pkOrigTranslatePriority = el.style.getPropertyPriority("translate") || "";
771
785
  }
772
786
  el.classList.add(className);
773
- el.style.setProperty("translate", `0 ${Math.round(deltaY)}px`, "important");
787
+ if (position === "fixed") {
788
+ const deltaY = edge === "bottom" ? safeTargetHeight - viewportHeight - scrollY : -scrollY;
789
+ if (Math.abs(deltaY) > 1) {
790
+ el.style.setProperty("translate", `0 ${Math.round(deltaY)}px`, "important");
791
+ }
792
+ return;
793
+ }
794
+ el.style.setProperty("position", "relative", "important");
795
+ el.style.setProperty("top", "auto", "important");
796
+ el.style.setProperty("bottom", "auto", "important");
774
797
  });
775
798
  return topLevelCandidates.length;
776
799
  }, {
@@ -778,7 +801,7 @@ var pinBottomAffixedElements = async (page, options = {}) => {
778
801
  targetHeight: options.targetHeight
779
802
  });
780
803
  };
781
- var restoreBottomAffixedElements = async (page) => {
804
+ var restoreAffixedElementsForExpandedScreenshot = async (page) => {
782
805
  await page.evaluate((className) => {
783
806
  const hasOwn = (source, key) => Object.prototype.hasOwnProperty.call(source, key);
784
807
  const expansionKeys = [
@@ -787,13 +810,28 @@ var restoreBottomAffixedElements = async (page) => {
787
810
  "pkOrigMinHeight",
788
811
  "pkOrigMaxHeight"
789
812
  ];
790
- document.querySelectorAll('[data-pk-bottom-pinned="1"]').forEach((el) => {
813
+ document.querySelectorAll('[data-pk-affixed-adjusted="1"]').forEach((el) => {
814
+ if (hasOwn(el.dataset, "pkOrigPosition")) {
815
+ el.style.setProperty("position", el.dataset.pkOrigPosition || "", el.dataset.pkOrigPositionPriority || "");
816
+ delete el.dataset.pkOrigPosition;
817
+ delete el.dataset.pkOrigPositionPriority;
818
+ }
819
+ if (hasOwn(el.dataset, "pkOrigTop")) {
820
+ el.style.setProperty("top", el.dataset.pkOrigTop || "", el.dataset.pkOrigTopPriority || "");
821
+ delete el.dataset.pkOrigTop;
822
+ delete el.dataset.pkOrigTopPriority;
823
+ }
824
+ if (hasOwn(el.dataset, "pkOrigBottom")) {
825
+ el.style.setProperty("bottom", el.dataset.pkOrigBottom || "", el.dataset.pkOrigBottomPriority || "");
826
+ delete el.dataset.pkOrigBottom;
827
+ delete el.dataset.pkOrigBottomPriority;
828
+ }
791
829
  if (hasOwn(el.dataset, "pkOrigTranslate")) {
792
830
  el.style.setProperty("translate", el.dataset.pkOrigTranslate || "", el.dataset.pkOrigTranslatePriority || "");
793
831
  delete el.dataset.pkOrigTranslate;
794
832
  delete el.dataset.pkOrigTranslatePriority;
795
833
  }
796
- delete el.dataset.pkBottomPinned;
834
+ delete el.dataset.pkAffixedAdjusted;
797
835
  const stillExpanded = expansionKeys.some((key) => hasOwn(el.dataset, key));
798
836
  if (!stillExpanded) {
799
837
  el.classList.remove(className);
@@ -822,8 +860,8 @@ var prepareExpandedFullPageScreenshot = async (page, options = {}) => {
822
860
  });
823
861
  viewportResized = true;
824
862
  } else {
825
- await pinBottomAffixedElements(page, { targetHeight }).catch((error) => {
826
- logger.warning(`\u79FB\u52A8\u7AEF\u5438\u5E95\u5143\u7D20\u91CD\u5B9A\u4F4D\u5931\u8D25: ${error?.message || error}`);
863
+ await adjustAffixedElementsForExpandedScreenshot(page, { targetHeight }).catch((error) => {
864
+ logger.warning(`\u79FB\u52A8\u7AEF\u5438\u9644\u5143\u7D20\u91CD\u5B9A\u4F4D\u5931\u8D25: ${error?.message || error}`);
827
865
  });
828
866
  }
829
867
  if (settleMs > 0) {
@@ -929,8 +967,8 @@ var captureExpandedFullPageScreenshot = async (page, options = {}) => {
929
967
  maxClipHeight: state.targetHeight
930
968
  });
931
969
  } finally {
932
- await restoreBottomAffixedElements(page).catch((error) => {
933
- logger.warning(`\u79FB\u52A8\u7AEF\u5438\u5E95\u5143\u7D20\u6062\u590D\u5931\u8D25: ${error?.message || error}`);
970
+ await restoreAffixedElementsForExpandedScreenshot(page).catch((error) => {
971
+ logger.warning(`\u79FB\u52A8\u7AEF\u5438\u9644\u5143\u7D20\u6062\u590D\u5931\u8D25: ${error?.message || error}`);
934
972
  });
935
973
  if (options.restore) {
936
974
  await restoreExpandedFullPageScreenshot(page, state);
@@ -3370,6 +3408,26 @@ var waitJitter = (base, jitterPercent = 0.3) => delay3(jitterMs(base, jitterPerc
3370
3408
  // src/internals/humanize/mobile.js
3371
3409
  var logger7 = createInternalLogger("Humanize.Mobile");
3372
3410
  var initializedPages = /* @__PURE__ */ new WeakSet();
3411
+ var DEFAULT_TAP_TIMEOUT_MS = 2500;
3412
+ var DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS = 1200;
3413
+ var DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS = 900;
3414
+ var INTERACTIVE_SELECTOR = [
3415
+ "button",
3416
+ '[role="button"]',
3417
+ "a[href]",
3418
+ "label",
3419
+ "input",
3420
+ "textarea",
3421
+ "select",
3422
+ "summary",
3423
+ '[contenteditable="true"]',
3424
+ '[tabindex]:not([tabindex="-1"])'
3425
+ ].join(",");
3426
+ var EDITABLE_SELECTOR = [
3427
+ "input",
3428
+ "textarea",
3429
+ '[contenteditable="true"]'
3430
+ ].join(",");
3373
3431
  var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
3374
3432
  var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
3375
3433
  var describeTarget = (target) => {
@@ -3392,8 +3450,24 @@ var clipBoxToViewport = (box, viewport) => {
3392
3450
  }
3393
3451
  return box;
3394
3452
  };
3453
+ var withTimeout = async (operation, timeoutMs, label) => {
3454
+ const safeTimeoutMs = Math.max(50, Number(timeoutMs || 0));
3455
+ let timeoutId = null;
3456
+ try {
3457
+ return await Promise.race([
3458
+ Promise.resolve().then(operation),
3459
+ new Promise((_, reject) => {
3460
+ timeoutId = setTimeout(() => {
3461
+ reject(new Error(`${label} timeout after ${safeTimeoutMs}ms`));
3462
+ }, safeTimeoutMs);
3463
+ })
3464
+ ]);
3465
+ } finally {
3466
+ if (timeoutId) clearTimeout(timeoutId);
3467
+ }
3468
+ };
3395
3469
  var checkElementVisibility = async (element) => {
3396
- return element.evaluate((el) => {
3470
+ return element.evaluate((el, interactiveSelector) => {
3397
3471
  const targetStyle = window.getComputedStyle(el);
3398
3472
  if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
3399
3473
  return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
@@ -3410,10 +3484,12 @@ var checkElementVisibility = async (element) => {
3410
3484
  let clipTop = 0;
3411
3485
  let clipBottom = viewH;
3412
3486
  let isFixed = false;
3487
+ let positioning = null;
3413
3488
  for (let node = el; node && node !== document.body; node = node.parentElement) {
3414
3489
  const style = window.getComputedStyle(node);
3415
3490
  if (style && (style.position === "fixed" || style.position === "sticky")) {
3416
3491
  isFixed = true;
3492
+ positioning = style.position;
3417
3493
  break;
3418
3494
  }
3419
3495
  }
@@ -3449,21 +3525,10 @@ var checkElementVisibility = async (element) => {
3449
3525
  direction: centerY < clipTop ? "up" : "down",
3450
3526
  cy: centerY,
3451
3527
  viewH,
3452
- isFixed
3528
+ isFixed,
3529
+ positioning
3453
3530
  };
3454
3531
  }
3455
- const interactiveSelector = [
3456
- "button",
3457
- '[role="button"]',
3458
- "a[href]",
3459
- "label",
3460
- "input",
3461
- "textarea",
3462
- "select",
3463
- "summary",
3464
- '[contenteditable="true"]',
3465
- '[tabindex]:not([tabindex="-1"])'
3466
- ].join(",");
3467
3532
  const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
3468
3533
  const sameInteractiveTarget = (pointElement) => {
3469
3534
  if (!pointElement) return false;
@@ -3485,6 +3550,7 @@ var checkElementVisibility = async (element) => {
3485
3550
  id: node.id || "",
3486
3551
  className,
3487
3552
  isFixed: Boolean(style && (style.position === "fixed" || style.position === "sticky")),
3553
+ positioning: style?.position || "",
3488
3554
  top: rect2 ? rect2.top : null,
3489
3555
  bottom: rect2 ? rect2.bottom : null,
3490
3556
  left: rect2 ? rect2.left : null,
@@ -3502,7 +3568,7 @@ var checkElementVisibility = async (element) => {
3502
3568
  for (const point of samplePoints) {
3503
3569
  const pointElement = document.elementFromPoint(point.x, point.y);
3504
3570
  if (sameInteractiveTarget(pointElement)) {
3505
- return { code: "VISIBLE", isFixed };
3571
+ return { code: "VISIBLE", isFixed, positioning };
3506
3572
  }
3507
3573
  obstruction = obstruction || describeElement(pointElement);
3508
3574
  }
@@ -3514,14 +3580,15 @@ var checkElementVisibility = async (element) => {
3514
3580
  obstruction,
3515
3581
  cy,
3516
3582
  viewH,
3517
- isFixed
3583
+ isFixed,
3584
+ positioning
3518
3585
  };
3519
3586
  }
3520
- return { code: "VISIBLE", isFixed };
3521
- });
3587
+ return { code: "VISIBLE", isFixed, positioning };
3588
+ }, INTERACTIVE_SELECTOR);
3522
3589
  };
3523
3590
  var resolveSafeTapPoint = async (element) => {
3524
- return element.evaluate((el) => {
3591
+ return element.evaluate((el, interactiveSelector) => {
3525
3592
  const rect = el.getBoundingClientRect();
3526
3593
  if (!rect || rect.width <= 0 || rect.height <= 0) {
3527
3594
  return null;
@@ -3558,18 +3625,6 @@ var resolveSafeTapPoint = async (element) => {
3558
3625
  if (visibleWidth <= 1 || visibleHeight <= 1) {
3559
3626
  return null;
3560
3627
  }
3561
- const interactiveSelector = [
3562
- "button",
3563
- '[role="button"]',
3564
- "a[href]",
3565
- "label",
3566
- "input",
3567
- "textarea",
3568
- "select",
3569
- "summary",
3570
- '[contenteditable="true"]',
3571
- '[tabindex]:not([tabindex="-1"])'
3572
- ].join(",");
3573
3628
  const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
3574
3629
  const sameInteractiveTarget = (pointElement) => {
3575
3630
  if (!pointElement) return false;
@@ -3607,6 +3662,46 @@ var resolveSafeTapPoint = async (element) => {
3607
3662
  x: chosen.x,
3608
3663
  y: chosen.y
3609
3664
  };
3665
+ }, INTERACTIVE_SELECTOR);
3666
+ };
3667
+ var activateElementFallback = async (element, point = null, options = {}) => {
3668
+ return element.evaluate((el, { innerPoint, innerOptions, interactiveSelector, editableSelector }) => {
3669
+ const pointElement = innerPoint ? document.elementFromPoint(innerPoint.x, innerPoint.y) : null;
3670
+ const nearestInteractive = (node) => {
3671
+ if (!node || typeof node.closest !== "function") return null;
3672
+ return node.closest(interactiveSelector);
3673
+ };
3674
+ const targetInteractive = nearestInteractive(el);
3675
+ const pointInteractive = nearestInteractive(pointElement);
3676
+ let target = null;
3677
+ if (pointInteractive && (pointInteractive === targetInteractive || pointInteractive === el || pointInteractive.contains(el) || el.contains(pointInteractive))) {
3678
+ target = pointInteractive;
3679
+ }
3680
+ target = target || targetInteractive || el;
3681
+ const editable = typeof target.closest === "function" ? target.closest(editableSelector) : null;
3682
+ if (editable && typeof editable.focus === "function") {
3683
+ editable.focus({ preventScroll: true });
3684
+ if (innerOptions.editableOnly) {
3685
+ return { activated: true, method: "focus", tag: editable.tagName || "" };
3686
+ }
3687
+ }
3688
+ if (typeof target.focus === "function") {
3689
+ target.focus({ preventScroll: true });
3690
+ }
3691
+ if (!innerOptions.editableOnly && typeof target.click === "function") {
3692
+ target.click();
3693
+ return { activated: true, method: "dom-click", tag: target.tagName || "" };
3694
+ }
3695
+ return {
3696
+ activated: Boolean(editable),
3697
+ method: editable ? "focus" : "none",
3698
+ tag: target?.tagName || ""
3699
+ };
3700
+ }, {
3701
+ innerPoint: point,
3702
+ innerOptions: options || {},
3703
+ interactiveSelector: INTERACTIVE_SELECTOR,
3704
+ editableSelector: EDITABLE_SELECTOR
3610
3705
  });
3611
3706
  };
3612
3707
  var getScrollableRect = async (element) => {
@@ -3811,12 +3906,31 @@ var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
3811
3906
  }
3812
3907
  }
3813
3908
  };
3814
- var tapPoint = async (page, point) => {
3909
+ var tapPoint = async (page, point, options = {}) => {
3910
+ const {
3911
+ timeoutMs = DEFAULT_TAP_TIMEOUT_MS,
3912
+ mouseFallbackTimeoutMs = DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS,
3913
+ allowMouseFallback = true
3914
+ } = options;
3815
3915
  if (page.touchscreen && typeof page.touchscreen.tap === "function") {
3816
- await page.touchscreen.tap(point.x, point.y);
3817
- return;
3916
+ try {
3917
+ await withTimeout(
3918
+ () => page.touchscreen.tap(point.x, point.y),
3919
+ timeoutMs,
3920
+ "touchscreen.tap"
3921
+ );
3922
+ return { method: "touchscreen" };
3923
+ } catch (error) {
3924
+ logger7.warn(`tapPoint | touchscreen.tap \u5931\u8D25\u6216\u8D85\u65F6\uFF0C\u5C1D\u8BD5\u9F20\u6807\u515C\u5E95: ${error?.message || error}`);
3925
+ if (!allowMouseFallback) throw error;
3926
+ }
3818
3927
  }
3819
- await page.mouse.click(point.x, point.y);
3928
+ await withTimeout(
3929
+ () => page.mouse.click(point.x, point.y),
3930
+ mouseFallbackTimeoutMs,
3931
+ "mouse.click fallback"
3932
+ );
3933
+ return { method: "mouse" };
3820
3934
  };
3821
3935
  var MobileHumanize = {
3822
3936
  jitterMs,
@@ -3869,6 +3983,10 @@ var MobileHumanize = {
3869
3983
  return { element, didScroll, restore: null };
3870
3984
  }
3871
3985
  const scrollRect = await getScrollableRect(element);
3986
+ if (!scrollRect && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
3987
+ logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u4E0D\u5728\u89C6\u53E3\u5185\uFF0C\u9875\u9762\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (direction=${status.direction || "unknown"})`);
3988
+ return { element, didScroll, restore: null };
3989
+ }
3872
3990
  if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
3873
3991
  logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
3874
3992
  return { element, didScroll, restore: null };
@@ -3989,7 +4107,12 @@ var MobileHumanize = {
3989
4107
  const {
3990
4108
  reactionDelay = 220,
3991
4109
  throwOnMissing = true,
3992
- scrollIfNeeded = true
4110
+ scrollIfNeeded = true,
4111
+ tapTimeoutMs = DEFAULT_TAP_TIMEOUT_MS,
4112
+ mouseFallbackTimeoutMs = DEFAULT_MOUSE_TAP_FALLBACK_TIMEOUT_MS,
4113
+ activateFallbackTimeoutMs = DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS,
4114
+ fallbackDomClick = true,
4115
+ fallbackDomClickOnTapError = true
3993
4116
  } = options;
3994
4117
  const targetDesc = describeTarget(target);
3995
4118
  logger7.start("humanClick", `target=${targetDesc}`);
@@ -4000,6 +4123,9 @@ var MobileHumanize = {
4000
4123
  await tapPoint(page, {
4001
4124
  x: viewport.width * (0.45 + Math.random() * 0.1),
4002
4125
  y: viewport.height * (0.48 + Math.random() * 0.12)
4126
+ }, {
4127
+ timeoutMs: tapTimeoutMs,
4128
+ mouseFallbackTimeoutMs
4003
4129
  });
4004
4130
  logger7.success("humanClick", "Tapped current position");
4005
4131
  return true;
@@ -4014,6 +4140,19 @@ var MobileHumanize = {
4014
4140
  }
4015
4141
  const status = await checkElementVisibility(element).catch(() => null);
4016
4142
  if (status && status.code !== "VISIBLE") {
4143
+ if (fallbackDomClick && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
4144
+ const fallback = await withTimeout(
4145
+ () => activateElementFallback(element, null, {
4146
+ editableOnly: true
4147
+ }),
4148
+ activateFallbackTimeoutMs,
4149
+ "focus fallback"
4150
+ ).catch(() => null);
4151
+ if (fallback?.activated) {
4152
+ logger7.warn(`humanClick: fixed/sticky \u76EE\u6807\u4E0D\u5728\u89C6\u53E3\u5185\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
4153
+ return true;
4154
+ }
4155
+ }
4017
4156
  const message = `\u5143\u7D20\u4E0D\u53EF\u70B9\u51FB: ${status.reason || status.code}`;
4018
4157
  if (throwOnMissing) throw new Error(message);
4019
4158
  logger7.warn(`humanClick: ${message}\uFF0C\u8DF3\u8FC7\u70B9\u51FB`);
@@ -4028,7 +4167,23 @@ var MobileHumanize = {
4028
4167
  await waitJitter(reactionDelay, 0.45);
4029
4168
  const safePoint = await resolveSafeTapPoint(element).catch(() => null);
4030
4169
  const tapTarget = safePoint || randomPointInBox(clipBoxToViewport(box, resolveViewport(page)), 0.2);
4031
- await tapPoint(page, tapTarget);
4170
+ try {
4171
+ await tapPoint(page, tapTarget, {
4172
+ timeoutMs: tapTimeoutMs,
4173
+ mouseFallbackTimeoutMs
4174
+ });
4175
+ } catch (tapError) {
4176
+ if (!fallbackDomClickOnTapError) throw tapError;
4177
+ const fallback = await withTimeout(
4178
+ () => activateElementFallback(element, tapTarget, {
4179
+ editableOnly: false
4180
+ }),
4181
+ activateFallbackTimeoutMs,
4182
+ "activation fallback"
4183
+ ).catch(() => null);
4184
+ if (!fallback?.activated) throw tapError;
4185
+ logger7.warn(`humanClick: tap \u5931\u8D25\u540E\u5DF2\u7528 ${fallback.method} \u515C\u5E95: ${tapError?.message || tapError}`);
4186
+ }
4032
4187
  await waitJitter(120, 0.35);
4033
4188
  logger7.success("humanClick");
4034
4189
  return true;
@@ -6846,7 +7001,7 @@ var getHostname = (url) => {
6846
7001
  return "\u672C\u5730\u9875\u9762";
6847
7002
  }
6848
7003
  };
6849
- var withTimeout = async (promise, timeoutMs) => {
7004
+ var withTimeout2 = async (promise, timeoutMs) => {
6850
7005
  const safeTimeoutMs = Math.max(0, Number(timeoutMs) || 0);
6851
7006
  if (!safeTimeoutMs) {
6852
7007
  return promise;
@@ -6931,7 +7086,7 @@ var resolveWithCustomResolver = async (page, baseMeta, options = {}) => {
6931
7086
  const serverAddr = response && typeof response.serverAddr === "function" ? await response.serverAddr().catch(() => null) : null;
6932
7087
  const controller = typeof AbortController === "function" ? new AbortController() : null;
6933
7088
  try {
6934
- const resolved = await withTimeout(
7089
+ const resolved = await withTimeout2(
6935
7090
  Promise.resolve(resolver({
6936
7091
  page,
6937
7092
  response,
@@ -7231,13 +7386,13 @@ var resolveWithIpLookup = async (page, options = {}) => {
7231
7386
  const contentType = normalizeText(await pickHeaderValue(response, ["content-type"]));
7232
7387
  let rawText = "";
7233
7388
  if (response && typeof response.text === "function") {
7234
- rawText = String(await withTimeout(
7389
+ rawText = String(await withTimeout2(
7235
7390
  response.text().catch(() => ""),
7236
7391
  timeoutMs
7237
7392
  ) || "");
7238
7393
  }
7239
7394
  if (!rawText) {
7240
- rawText = String(await withTimeout(
7395
+ rawText = String(await withTimeout2(
7241
7396
  probePage.evaluate(() => {
7242
7397
  return document.body?.innerText || document.documentElement?.innerText || "";
7243
7398
  }).catch(() => ""),
@@ -8340,7 +8495,7 @@ var normalizeXurl = (value) => {
8340
8495
  var normalizeShare = (share) => {
8341
8496
  const source = share && typeof share === "object" ? share : {};
8342
8497
  const modeRaw = String(source.mode || "dom").trim().toLowerCase();
8343
- const mode = modeRaw === "response" ? "response" : "dom";
8498
+ const mode = ["dom", "response", "custom"].includes(modeRaw) ? modeRaw : "dom";
8344
8499
  return {
8345
8500
  mode,
8346
8501
  prefix: normalizePrefix(source.prefix),
@@ -8456,16 +8611,17 @@ var Share = {
8456
8611
  * 捕获分享链接(单模式)
8457
8612
  * - mode='dom': 仅 DOM 监听
8458
8613
  * - mode='response': 仅接口监听
8614
+ * - mode='custom': 使用 performActions 返回值作为链接
8459
8615
  *
8460
8616
  * @param {import('playwright').Page} page
8461
8617
  * @param {Object} [options]
8462
- * @param {{ mode?: 'dom' | 'response', prefix: string, xurl?: Array<string | string[]> }} options.share
8618
+ * @param {{ mode?: 'dom' | 'response' | 'custom', prefix: string, xurl?: Array<string | string[]> }} options.share
8463
8619
  * @param {number} [options.timeoutMs=50000]
8464
8620
  * @param {number} [options.payloadSnapshotMaxLen=500]
8465
8621
  * @param {string | string[]} [options.domSelectors='html']
8466
8622
  * @param {'added' | 'changed' | 'all'} [options.domMode='all']
8467
- * @param {() => Promise<void>} [options.performActions]
8468
- * @returns {Promise<{ link: string | null; payloadText: string; payloadSnapshot: string; source: 'dom' | 'response' | 'none' }>}
8623
+ * @param {() => void | string | { link?: string | null; payloadText?: string } | Promise<void | string | { link?: string | null; payloadText?: string }>} [options.performActions]
8624
+ * @returns {Promise<{ link: string | null; payloadText: string; payloadSnapshot: string; source: 'dom' | 'response' | 'custom' | 'none' }>}
8469
8625
  */
8470
8626
  async captureLink(page, options = {}) {
8471
8627
  const share = normalizeShare(options.share);
@@ -8478,7 +8634,7 @@ var Share = {
8478
8634
  if (share.prefix === void 0) {
8479
8635
  throw new Error("Share.captureLink requires options.share.prefix");
8480
8636
  }
8481
- if (share.mode !== "dom" && share.mode !== "response") {
8637
+ if (share.mode !== "dom" && share.mode !== "response" && share.mode !== "custom") {
8482
8638
  throw new Error(`Share.captureLink unsupported share.mode: ${share.mode}`);
8483
8639
  }
8484
8640
  const apiMatchers = share.mode === "response" ? share.xurl[0] || [] : [];
@@ -8500,7 +8656,8 @@ var Share = {
8500
8656
  };
8501
8657
  const candidates = {
8502
8658
  dom: null,
8503
- response: null
8659
+ response: null,
8660
+ custom: null
8504
8661
  };
8505
8662
  const setCandidate = (source, link, payloadText = "") => {
8506
8663
  const safe = String(link || "").trim();
@@ -8600,37 +8757,47 @@ var Share = {
8600
8757
  logger15.info(`\u5F53\u524D\u4E3A\u63A5\u53E3\u6A21\u5F0F\uFF0C\u6302\u8F7D response \u76D1\u542C: apiMatchers=${toJsonInline(apiMatchers, 160)}`);
8601
8758
  page.on("response", onResponse);
8602
8759
  }
8760
+ if (share.mode === "custom") {
8761
+ logger15.info("\u5F53\u524D\u4E3A custom \u6A21\u5F0F\uFF0C\u5C06\u4F7F\u7528 performActions \u8FD4\u56DE\u503C");
8762
+ }
8603
8763
  const deadline = Date.now() + timeoutMs;
8604
8764
  const getRemainingMs = () => Math.max(0, deadline - Date.now());
8605
8765
  try {
8606
8766
  const actionTimeout = getRemainingMs();
8607
8767
  logger15.start("captureLink.performActions", `\u6267\u884C\u52A8\u4F5C\u9884\u7B97=${actionTimeout}ms`);
8768
+ let actionValue;
8608
8769
  if (actionTimeout > 0) {
8609
8770
  let timer = null;
8610
- let actionError = null;
8611
- const actionPromise = Promise.resolve().then(() => performActions()).then(() => "__ACTION_DONE__").catch((error) => {
8612
- actionError = error;
8613
- return "__ACTION_ERROR__";
8614
- });
8771
+ const actionPromise = Promise.resolve().then(() => performActions()).then((result) => ({ type: "done", result })).catch((error) => ({ type: "error", error }));
8615
8772
  const timeoutPromise = new Promise((resolve) => {
8616
- timer = setTimeout(() => resolve("__ACTION_TIMEOUT__"), actionTimeout);
8773
+ timer = setTimeout(() => resolve({ type: "timeout" }), actionTimeout);
8617
8774
  });
8618
8775
  const actionResult = await Promise.race([actionPromise, timeoutPromise]);
8619
8776
  if (timer) clearTimeout(timer);
8620
- if (actionResult === "__ACTION_ERROR__") {
8621
- logger15.fail("captureLink.performActions", actionError);
8622
- throw actionError;
8777
+ if (actionResult.type === "error") {
8778
+ logger15.fail("captureLink.performActions", actionResult.error);
8779
+ throw actionResult.error;
8623
8780
  }
8624
- if (actionResult === "__ACTION_TIMEOUT__") {
8781
+ if (actionResult.type === "timeout") {
8625
8782
  stats.actionTimedOut = true;
8626
8783
  logger15.warning(`performActions \u5DF2\u8D85\u65F6 (${actionTimeout}ms)\uFF0C\u52A8\u4F5C\u53EF\u80FD\u4ECD\u5728\u5F02\u6B65\u6267\u884C`);
8627
8784
  } else {
8785
+ actionValue = actionResult.result;
8628
8786
  logger15.success("captureLink.performActions", "\u6267\u884C\u52A8\u4F5C\u5B8C\u6210");
8629
8787
  }
8630
8788
  }
8789
+ if (share.mode === "custom") {
8790
+ const customLink = typeof actionValue === "string" ? actionValue : actionValue?.link || actionValue?.payloadText;
8791
+ const customPayloadText = typeof actionValue === "string" ? actionValue : actionValue?.payloadText;
8792
+ if (setCandidate("custom", customLink, customPayloadText)) {
8793
+ logger15.success("captureLink.customResult", `custom \u547D\u4E2D\u5206\u4EAB\u94FE\u63A5: link=${candidates.custom.link}`);
8794
+ } else {
8795
+ logger15.warning("performActions \u6267\u884C\u5B8C\u6210\u4F46\u672A\u8FD4\u56DE\u6709\u6548\u5206\u4EAB\u94FE\u63A5");
8796
+ }
8797
+ }
8631
8798
  let nextProgressLogTs = Date.now() + 3e3;
8632
8799
  while (true) {
8633
- const selected = share.mode === "dom" ? candidates.dom : candidates.response;
8800
+ const selected = candidates[share.mode];
8634
8801
  if (selected?.link) {
8635
8802
  logger15.success("captureLink", `\u6355\u83B7\u6210\u529F: source=${share.mode}, link=${selected.link}`);
8636
8803
  return {
@@ -8640,6 +8807,7 @@ var Share = {
8640
8807
  source: share.mode
8641
8808
  };
8642
8809
  }
8810
+ if (share.mode === "custom") break;
8643
8811
  const remaining = getRemainingMs();
8644
8812
  if (remaining <= 0) break;
8645
8813
  const now = Date.now();