@skrillex1224/playwright-toolkit 2.1.243 → 2.1.245

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3439,6 +3439,11 @@ var DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS = 900;
3439
3439
  var INTERACTIVE_SELECTOR = [
3440
3440
  "button",
3441
3441
  '[role="button"]',
3442
+ '[role="link"]',
3443
+ '[role="menuitem"]',
3444
+ '[role="tab"]',
3445
+ '[role="switch"]',
3446
+ '[role="checkbox"]',
3442
3447
  "a[href]",
3443
3448
  "label",
3444
3449
  "input",
@@ -3453,6 +3458,34 @@ var EDITABLE_SELECTOR = [
3453
3458
  "textarea",
3454
3459
  '[contenteditable="true"]'
3455
3460
  ].join(",");
3461
+ var INTERACTIVE_HINT_PATTERN = [
3462
+ "btn",
3463
+ "button",
3464
+ "send",
3465
+ "submit",
3466
+ "confirm",
3467
+ "cancel",
3468
+ "retry",
3469
+ "reload",
3470
+ "search",
3471
+ "copy",
3472
+ "share",
3473
+ "close",
3474
+ "more",
3475
+ "\u53D1\u9001",
3476
+ "\u63D0\u4EA4",
3477
+ "\u786E\u5B9A",
3478
+ "\u786E\u8BA4",
3479
+ "\u53D6\u6D88",
3480
+ "\u91CD\u8BD5",
3481
+ "\u641C\u7D22",
3482
+ "\u590D\u5236",
3483
+ "\u5206\u4EAB",
3484
+ "\u5173\u95ED",
3485
+ "\u66F4\u591A",
3486
+ "\u5C55\u5F00",
3487
+ "\u6536\u8D77"
3488
+ ].join("|");
3456
3489
  var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
3457
3490
  var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
3458
3491
  var describeTarget = (target) => {
@@ -3492,7 +3525,47 @@ var withTimeout = async (operation, timeoutMs, label) => {
3492
3525
  }
3493
3526
  };
3494
3527
  var checkElementVisibility = async (element) => {
3495
- return element.evaluate((el, interactiveSelector) => {
3528
+ return element.evaluate((el, { interactiveSelector, interactiveHintPattern }) => {
3529
+ const interactiveHintRe = new RegExp(interactiveHintPattern, "i");
3530
+ const nodeClassName = (node) => {
3531
+ if (!node) return "";
3532
+ if (typeof node.className === "string") return node.className;
3533
+ if (node.className && typeof node.className.baseVal === "string") return node.className.baseVal;
3534
+ return "";
3535
+ };
3536
+ const interactiveScore = (node) => {
3537
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return 0;
3538
+ let score = 0;
3539
+ if (typeof node.matches === "function" && node.matches(interactiveSelector)) score += 8;
3540
+ const hints = [
3541
+ node.id || "",
3542
+ nodeClassName(node),
3543
+ node.getAttribute?.("aria-label") || "",
3544
+ node.getAttribute?.("title") || "",
3545
+ node.getAttribute?.("data-testid") || "",
3546
+ node.getAttribute?.("data-test") || "",
3547
+ node.getAttribute?.("data-click") || "",
3548
+ node.getAttribute?.("data-action") || "",
3549
+ node.getAttribute?.("onclick") || "",
3550
+ String(node.textContent || "").trim().slice(0, 24)
3551
+ ].join(" ");
3552
+ if (interactiveHintRe.test(hints)) score += 4;
3553
+ const style = window.getComputedStyle(node);
3554
+ if (style?.cursor === "pointer") score += 2;
3555
+ return score;
3556
+ };
3557
+ const closestInteractive = (node) => {
3558
+ let best = null;
3559
+ let bestScore = 0;
3560
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3561
+ const score = interactiveScore(current);
3562
+ if (score > bestScore || score > 0 && score === bestScore) {
3563
+ best = current;
3564
+ bestScore = score;
3565
+ }
3566
+ }
3567
+ return best;
3568
+ };
3496
3569
  const targetStyle = window.getComputedStyle(el);
3497
3570
  if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
3498
3571
  return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
@@ -3554,16 +3627,13 @@ var checkElementVisibility = async (element) => {
3554
3627
  positioning
3555
3628
  };
3556
3629
  }
3557
- const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
3630
+ const targetInteractive = closestInteractive(el);
3558
3631
  const sameInteractiveTarget = (pointElement) => {
3559
3632
  if (!pointElement) return false;
3560
3633
  if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
3561
3634
  return true;
3562
3635
  }
3563
- if (!targetInteractive || typeof pointElement.closest !== "function") {
3564
- return false;
3565
- }
3566
- return pointElement.closest(interactiveSelector) === targetInteractive;
3636
+ return Boolean(targetInteractive && closestInteractive(pointElement) === targetInteractive);
3567
3637
  };
3568
3638
  const describeElement = (node) => {
3569
3639
  if (!node) return null;
@@ -3610,10 +3680,53 @@ var checkElementVisibility = async (element) => {
3610
3680
  };
3611
3681
  }
3612
3682
  return { code: "VISIBLE", isFixed, positioning };
3613
- }, INTERACTIVE_SELECTOR);
3683
+ }, {
3684
+ interactiveSelector: INTERACTIVE_SELECTOR,
3685
+ interactiveHintPattern: INTERACTIVE_HINT_PATTERN
3686
+ });
3614
3687
  };
3615
3688
  var resolveSafeTapPoint = async (element) => {
3616
- return element.evaluate((el, interactiveSelector) => {
3689
+ return element.evaluate((el, { interactiveSelector, interactiveHintPattern }) => {
3690
+ const interactiveHintRe = new RegExp(interactiveHintPattern, "i");
3691
+ const nodeClassName = (node) => {
3692
+ if (!node) return "";
3693
+ if (typeof node.className === "string") return node.className;
3694
+ if (node.className && typeof node.className.baseVal === "string") return node.className.baseVal;
3695
+ return "";
3696
+ };
3697
+ const interactiveScore = (node) => {
3698
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return 0;
3699
+ let score = 0;
3700
+ if (typeof node.matches === "function" && node.matches(interactiveSelector)) score += 8;
3701
+ const hints = [
3702
+ node.id || "",
3703
+ nodeClassName(node),
3704
+ node.getAttribute?.("aria-label") || "",
3705
+ node.getAttribute?.("title") || "",
3706
+ node.getAttribute?.("data-testid") || "",
3707
+ node.getAttribute?.("data-test") || "",
3708
+ node.getAttribute?.("data-click") || "",
3709
+ node.getAttribute?.("data-action") || "",
3710
+ node.getAttribute?.("onclick") || "",
3711
+ String(node.textContent || "").trim().slice(0, 24)
3712
+ ].join(" ");
3713
+ if (interactiveHintRe.test(hints)) score += 4;
3714
+ const style = window.getComputedStyle(node);
3715
+ if (style?.cursor === "pointer") score += 2;
3716
+ return score;
3717
+ };
3718
+ const closestInteractive = (node) => {
3719
+ let best = null;
3720
+ let bestScore = 0;
3721
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3722
+ const score = interactiveScore(current);
3723
+ if (score > bestScore || score > 0 && score === bestScore) {
3724
+ best = current;
3725
+ bestScore = score;
3726
+ }
3727
+ }
3728
+ return best;
3729
+ };
3617
3730
  const rect = el.getBoundingClientRect();
3618
3731
  if (!rect || rect.width <= 0 || rect.height <= 0) {
3619
3732
  return null;
@@ -3650,16 +3763,13 @@ var resolveSafeTapPoint = async (element) => {
3650
3763
  if (visibleWidth <= 1 || visibleHeight <= 1) {
3651
3764
  return null;
3652
3765
  }
3653
- const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
3766
+ const targetInteractive = closestInteractive(el);
3654
3767
  const sameInteractiveTarget = (pointElement) => {
3655
3768
  if (!pointElement) return false;
3656
3769
  if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
3657
3770
  return true;
3658
3771
  }
3659
- if (!targetInteractive || typeof pointElement.closest !== "function") {
3660
- return false;
3661
- }
3662
- return pointElement.closest(interactiveSelector) === targetInteractive;
3772
+ return Boolean(targetInteractive && closestInteractive(pointElement) === targetInteractive);
3663
3773
  };
3664
3774
  const cx = visibleLeft + visibleWidth / 2;
3665
3775
  const cy = visibleTop + visibleHeight / 2;
@@ -3687,14 +3797,53 @@ var resolveSafeTapPoint = async (element) => {
3687
3797
  x: chosen.x,
3688
3798
  y: chosen.y
3689
3799
  };
3690
- }, INTERACTIVE_SELECTOR);
3800
+ }, {
3801
+ interactiveSelector: INTERACTIVE_SELECTOR,
3802
+ interactiveHintPattern: INTERACTIVE_HINT_PATTERN
3803
+ });
3691
3804
  };
3692
3805
  var activateElementFallback = async (element, point = null, options = {}) => {
3693
- return element.evaluate((el, { innerPoint, innerOptions, interactiveSelector, editableSelector }) => {
3806
+ return element.evaluate((el, { innerPoint, innerOptions, interactiveSelector, editableSelector, interactiveHintPattern }) => {
3807
+ const interactiveHintRe = new RegExp(interactiveHintPattern, "i");
3808
+ const nodeClassName = (node) => {
3809
+ if (!node) return "";
3810
+ if (typeof node.className === "string") return node.className;
3811
+ if (node.className && typeof node.className.baseVal === "string") return node.className.baseVal;
3812
+ return "";
3813
+ };
3814
+ const interactiveScore = (node) => {
3815
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return 0;
3816
+ let score = 0;
3817
+ if (typeof node.matches === "function" && node.matches(interactiveSelector)) score += 8;
3818
+ const hints = [
3819
+ node.id || "",
3820
+ nodeClassName(node),
3821
+ node.getAttribute?.("aria-label") || "",
3822
+ node.getAttribute?.("title") || "",
3823
+ node.getAttribute?.("data-testid") || "",
3824
+ node.getAttribute?.("data-test") || "",
3825
+ node.getAttribute?.("data-click") || "",
3826
+ node.getAttribute?.("data-action") || "",
3827
+ node.getAttribute?.("onclick") || "",
3828
+ String(node.textContent || "").trim().slice(0, 24)
3829
+ ].join(" ");
3830
+ if (interactiveHintRe.test(hints)) score += 4;
3831
+ const style = window.getComputedStyle(node);
3832
+ if (style?.cursor === "pointer") score += 2;
3833
+ return score;
3834
+ };
3694
3835
  const pointElement = innerPoint ? document.elementFromPoint(innerPoint.x, innerPoint.y) : null;
3695
3836
  const nearestInteractive = (node) => {
3696
- if (!node || typeof node.closest !== "function") return null;
3697
- return node.closest(interactiveSelector);
3837
+ let best = null;
3838
+ let bestScore = 0;
3839
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3840
+ const score = interactiveScore(current);
3841
+ if (score > bestScore || score > 0 && score === bestScore) {
3842
+ best = current;
3843
+ bestScore = score;
3844
+ }
3845
+ }
3846
+ return best;
3698
3847
  };
3699
3848
  const targetInteractive = nearestInteractive(el);
3700
3849
  const pointInteractive = nearestInteractive(pointElement);
@@ -3726,7 +3875,8 @@ var activateElementFallback = async (element, point = null, options = {}) => {
3726
3875
  innerPoint: point,
3727
3876
  innerOptions: options || {},
3728
3877
  interactiveSelector: INTERACTIVE_SELECTOR,
3729
- editableSelector: EDITABLE_SELECTOR
3878
+ editableSelector: EDITABLE_SELECTOR,
3879
+ interactiveHintPattern: INTERACTIVE_HINT_PATTERN
3730
3880
  });
3731
3881
  };
3732
3882
  var getScrollableRect = async (element) => {
@@ -3830,6 +3980,47 @@ var scrollAwayFromObstruction = async (element, status) => {
3830
3980
  };
3831
3981
  }, status);
3832
3982
  };
3983
+ var getElementViewportSnapshot = async (element) => {
3984
+ return element.evaluate((el) => {
3985
+ const rect = el.getBoundingClientRect();
3986
+ return {
3987
+ top: rect.top,
3988
+ bottom: rect.bottom,
3989
+ left: rect.left,
3990
+ right: rect.right,
3991
+ width: rect.width,
3992
+ height: rect.height,
3993
+ scrollX: window.scrollX,
3994
+ scrollY: window.scrollY
3995
+ };
3996
+ });
3997
+ };
3998
+ var isTargetImmobileAfterScroll = (before, after) => {
3999
+ if (!before || !after) return false;
4000
+ const rectDeltaY = Number(after.top || 0) - Number(before.top || 0);
4001
+ const rectDeltaX = Number(after.left || 0) - Number(before.left || 0);
4002
+ const scrollDeltaY = Number(after.scrollY || 0) - Number(before.scrollY || 0);
4003
+ const scrollDeltaX = Number(after.scrollX || 0) - Number(before.scrollX || 0);
4004
+ const rectMoved = Math.abs(rectDeltaY) > 3 || Math.abs(rectDeltaX) > 3;
4005
+ const pageMoved = Math.abs(scrollDeltaY) > 3 || Math.abs(scrollDeltaX) > 3;
4006
+ if (!rectMoved && !pageMoved) return true;
4007
+ if (pageMoved && !rectMoved) return true;
4008
+ if (Math.abs(scrollDeltaY) > 12 && Math.abs(rectDeltaY) < Math.min(12, Math.abs(scrollDeltaY) * 0.2)) {
4009
+ return true;
4010
+ }
4011
+ return false;
4012
+ };
4013
+ var restoreWindowFromSnapshot = async (page, before, after) => {
4014
+ if (!before || !after) return;
4015
+ if (Math.abs(Number(after.scrollX || 0) - Number(before.scrollX || 0)) <= 2 && Math.abs(Number(after.scrollY || 0) - Number(before.scrollY || 0)) <= 2) {
4016
+ return;
4017
+ }
4018
+ await page.evaluate(
4019
+ (state) => window.scrollTo(state.x, state.y),
4020
+ { x: Number(before.scrollX || 0), y: Number(before.scrollY || 0) }
4021
+ ).catch(() => {
4022
+ });
4023
+ };
3833
4024
  var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
3834
4025
  const viewport = resolveViewport(page);
3835
4026
  const rawRect = options.rect || null;
@@ -4010,11 +4201,11 @@ var MobileHumanize = {
4010
4201
  const scrollRect = await getScrollableRect(element);
4011
4202
  if (!scrollRect && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
4012
4203
  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"})`);
4013
- return { element, didScroll, restore: null };
4204
+ return { element, didScroll, restore: null, unscrollable: true };
4014
4205
  }
4015
4206
  if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
4016
4207
  logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
4017
- return { element, didScroll, restore: null };
4208
+ return { element, didScroll, restore: null, unscrollable: true };
4018
4209
  }
4019
4210
  if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
4020
4211
  const moved = await scrollAwayFromObstruction(element, status);
@@ -4045,6 +4236,7 @@ var MobileHumanize = {
4045
4236
  }
4046
4237
  }
4047
4238
  const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
4239
+ const beforeElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4048
4240
  const beforeState = scrollRect ? await element.evaluate((el) => {
4049
4241
  const isScrollable = (node) => {
4050
4242
  const style = window.getComputedStyle(node);
@@ -4074,6 +4266,21 @@ var MobileHumanize = {
4074
4266
  logger7.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
4075
4267
  }
4076
4268
  }
4269
+ let afterElementSnapshot = null;
4270
+ const readAfterElementSnapshot = async () => {
4271
+ if (!afterElementSnapshot) {
4272
+ afterElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4273
+ }
4274
+ return afterElementSnapshot;
4275
+ };
4276
+ if (!scrollRect && beforeElementSnapshot) {
4277
+ const afterSnapshot = await readAfterElementSnapshot();
4278
+ if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
4279
+ await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
4280
+ logger7.warn(`humanScroll | \u76EE\u6807\u4E0D\u968F\u9875\u9762\u6EDA\u52A8\u79FB\u52A8\uFF0C\u9875\u9762\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (status=${status.code}, direction=${status.direction || "unknown"})`);
4281
+ return { element, didScroll, restore: null, unscrollable: true };
4282
+ }
4283
+ }
4077
4284
  if (scrollRect && beforeState) {
4078
4285
  const afterState = await element.evaluate((el) => {
4079
4286
  const isScrollable = (node) => {
@@ -4111,6 +4318,14 @@ var MobileHumanize = {
4111
4318
  }
4112
4319
  }
4113
4320
  }
4321
+ if (scrollRect && beforeElementSnapshot) {
4322
+ const afterSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4323
+ if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
4324
+ await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
4325
+ logger7.warn(`humanScroll | \u76EE\u6807\u4E0D\u968F\u6EDA\u52A8\u5BB9\u5668\u79FB\u52A8\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u6539\u53D8\u5176\u4F4D\u7F6E (status=${status.code}, direction=${status.direction || "unknown"})`);
4326
+ return { element, didScroll, restore: null, unscrollable: true };
4327
+ }
4328
+ }
4114
4329
  didScroll = true;
4115
4330
  }
4116
4331
  try {
@@ -4160,21 +4375,28 @@ var MobileHumanize = {
4160
4375
  logger7.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
4161
4376
  return false;
4162
4377
  }
4163
- if (scrollIfNeeded) {
4164
- await MobileHumanize.humanScroll(page, element, { throwOnMissing });
4165
- }
4378
+ const scrollResult = scrollIfNeeded ? await MobileHumanize.humanScroll(page, element, { throwOnMissing }) : null;
4166
4379
  const status = await checkElementVisibility(element).catch(() => null);
4167
4380
  if (status && status.code !== "VISIBLE") {
4168
- if (fallbackDomClick && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
4169
- const fallback = await withTimeout(
4381
+ if (fallbackDomClick && (status.code === "OUT_OF_VIEWPORT" || status.code === "OBSTRUCTED") && (status.isFixed || scrollResult?.unscrollable)) {
4382
+ let fallback = await withTimeout(
4170
4383
  () => activateElementFallback(element, null, {
4171
4384
  editableOnly: true
4172
4385
  }),
4173
4386
  activateFallbackTimeoutMs,
4174
4387
  "focus fallback"
4175
4388
  ).catch(() => null);
4389
+ if (!fallback?.activated) {
4390
+ fallback = await withTimeout(
4391
+ () => activateElementFallback(element, null, {
4392
+ editableOnly: false
4393
+ }),
4394
+ activateFallbackTimeoutMs,
4395
+ "activation fallback"
4396
+ ).catch(() => null);
4397
+ }
4176
4398
  if (fallback?.activated) {
4177
- logger7.warn(`humanClick: fixed/sticky \u76EE\u6807\u4E0D\u5728\u89C6\u53E3\u5185\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
4399
+ logger7.warn(`humanClick: \u4E0D\u53EF\u6EDA\u52A8\u76EE\u6807\u4E0D\u53EF\u7269\u7406\u70B9\u51FB\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
4178
4400
  return true;
4179
4401
  }
4180
4402
  }
@@ -4291,6 +4513,20 @@ var MobileHumanize = {
4291
4513
  const locator = page.locator(selector);
4292
4514
  await MobileHumanize.humanClick(page, locator, { scrollIfNeeded: true });
4293
4515
  await waitJitter(160, 0.4);
4516
+ const readValue = async () => {
4517
+ try {
4518
+ return await locator.inputValue({ timeout: 600 });
4519
+ } catch {
4520
+ return await locator.evaluate((el) => "value" in el ? String(el.value || "") : String(el.textContent || "")).catch(() => "");
4521
+ }
4522
+ };
4523
+ const currentValue = await readValue();
4524
+ if (!currentValue) return;
4525
+ await page.keyboard.press("ControlOrMeta+A");
4526
+ await waitJitter(90, 0.35);
4527
+ await page.keyboard.press("Backspace");
4528
+ await waitJitter(120, 0.35);
4529
+ if (!await readValue()) return;
4294
4530
  await locator.evaluate((el) => {
4295
4531
  if ("value" in el) {
4296
4532
  el.value = "";