@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.js CHANGED
@@ -3411,6 +3411,11 @@ var DEFAULT_ACTIVATE_FALLBACK_TIMEOUT_MS = 900;
3411
3411
  var INTERACTIVE_SELECTOR = [
3412
3412
  "button",
3413
3413
  '[role="button"]',
3414
+ '[role="link"]',
3415
+ '[role="menuitem"]',
3416
+ '[role="tab"]',
3417
+ '[role="switch"]',
3418
+ '[role="checkbox"]',
3414
3419
  "a[href]",
3415
3420
  "label",
3416
3421
  "input",
@@ -3425,6 +3430,34 @@ var EDITABLE_SELECTOR = [
3425
3430
  "textarea",
3426
3431
  '[contenteditable="true"]'
3427
3432
  ].join(",");
3433
+ var INTERACTIVE_HINT_PATTERN = [
3434
+ "btn",
3435
+ "button",
3436
+ "send",
3437
+ "submit",
3438
+ "confirm",
3439
+ "cancel",
3440
+ "retry",
3441
+ "reload",
3442
+ "search",
3443
+ "copy",
3444
+ "share",
3445
+ "close",
3446
+ "more",
3447
+ "\u53D1\u9001",
3448
+ "\u63D0\u4EA4",
3449
+ "\u786E\u5B9A",
3450
+ "\u786E\u8BA4",
3451
+ "\u53D6\u6D88",
3452
+ "\u91CD\u8BD5",
3453
+ "\u641C\u7D22",
3454
+ "\u590D\u5236",
3455
+ "\u5206\u4EAB",
3456
+ "\u5173\u95ED",
3457
+ "\u66F4\u591A",
3458
+ "\u5C55\u5F00",
3459
+ "\u6536\u8D77"
3460
+ ].join("|");
3428
3461
  var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
3429
3462
  var resolveViewport = (page) => page?.viewportSize?.() || { width: 390, height: 844 };
3430
3463
  var describeTarget = (target) => {
@@ -3464,7 +3497,47 @@ var withTimeout = async (operation, timeoutMs, label) => {
3464
3497
  }
3465
3498
  };
3466
3499
  var checkElementVisibility = async (element) => {
3467
- return element.evaluate((el, interactiveSelector) => {
3500
+ return element.evaluate((el, { interactiveSelector, interactiveHintPattern }) => {
3501
+ const interactiveHintRe = new RegExp(interactiveHintPattern, "i");
3502
+ const nodeClassName = (node) => {
3503
+ if (!node) return "";
3504
+ if (typeof node.className === "string") return node.className;
3505
+ if (node.className && typeof node.className.baseVal === "string") return node.className.baseVal;
3506
+ return "";
3507
+ };
3508
+ const interactiveScore = (node) => {
3509
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return 0;
3510
+ let score = 0;
3511
+ if (typeof node.matches === "function" && node.matches(interactiveSelector)) score += 8;
3512
+ const hints = [
3513
+ node.id || "",
3514
+ nodeClassName(node),
3515
+ node.getAttribute?.("aria-label") || "",
3516
+ node.getAttribute?.("title") || "",
3517
+ node.getAttribute?.("data-testid") || "",
3518
+ node.getAttribute?.("data-test") || "",
3519
+ node.getAttribute?.("data-click") || "",
3520
+ node.getAttribute?.("data-action") || "",
3521
+ node.getAttribute?.("onclick") || "",
3522
+ String(node.textContent || "").trim().slice(0, 24)
3523
+ ].join(" ");
3524
+ if (interactiveHintRe.test(hints)) score += 4;
3525
+ const style = window.getComputedStyle(node);
3526
+ if (style?.cursor === "pointer") score += 2;
3527
+ return score;
3528
+ };
3529
+ const closestInteractive = (node) => {
3530
+ let best = null;
3531
+ let bestScore = 0;
3532
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3533
+ const score = interactiveScore(current);
3534
+ if (score > bestScore || score > 0 && score === bestScore) {
3535
+ best = current;
3536
+ bestScore = score;
3537
+ }
3538
+ }
3539
+ return best;
3540
+ };
3468
3541
  const targetStyle = window.getComputedStyle(el);
3469
3542
  if (!targetStyle || targetStyle.display === "none" || targetStyle.visibility === "hidden" || targetStyle.visibility === "collapse") {
3470
3543
  return { code: "NOT_INTERACTABLE", reason: "\u5143\u7D20\u4E0D\u53EF\u89C1", direction: "down" };
@@ -3526,16 +3599,13 @@ var checkElementVisibility = async (element) => {
3526
3599
  positioning
3527
3600
  };
3528
3601
  }
3529
- const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
3602
+ const targetInteractive = closestInteractive(el);
3530
3603
  const sameInteractiveTarget = (pointElement) => {
3531
3604
  if (!pointElement) return false;
3532
3605
  if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
3533
3606
  return true;
3534
3607
  }
3535
- if (!targetInteractive || typeof pointElement.closest !== "function") {
3536
- return false;
3537
- }
3538
- return pointElement.closest(interactiveSelector) === targetInteractive;
3608
+ return Boolean(targetInteractive && closestInteractive(pointElement) === targetInteractive);
3539
3609
  };
3540
3610
  const describeElement = (node) => {
3541
3611
  if (!node) return null;
@@ -3582,10 +3652,53 @@ var checkElementVisibility = async (element) => {
3582
3652
  };
3583
3653
  }
3584
3654
  return { code: "VISIBLE", isFixed, positioning };
3585
- }, INTERACTIVE_SELECTOR);
3655
+ }, {
3656
+ interactiveSelector: INTERACTIVE_SELECTOR,
3657
+ interactiveHintPattern: INTERACTIVE_HINT_PATTERN
3658
+ });
3586
3659
  };
3587
3660
  var resolveSafeTapPoint = async (element) => {
3588
- return element.evaluate((el, interactiveSelector) => {
3661
+ return element.evaluate((el, { interactiveSelector, interactiveHintPattern }) => {
3662
+ const interactiveHintRe = new RegExp(interactiveHintPattern, "i");
3663
+ const nodeClassName = (node) => {
3664
+ if (!node) return "";
3665
+ if (typeof node.className === "string") return node.className;
3666
+ if (node.className && typeof node.className.baseVal === "string") return node.className.baseVal;
3667
+ return "";
3668
+ };
3669
+ const interactiveScore = (node) => {
3670
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return 0;
3671
+ let score = 0;
3672
+ if (typeof node.matches === "function" && node.matches(interactiveSelector)) score += 8;
3673
+ const hints = [
3674
+ node.id || "",
3675
+ nodeClassName(node),
3676
+ node.getAttribute?.("aria-label") || "",
3677
+ node.getAttribute?.("title") || "",
3678
+ node.getAttribute?.("data-testid") || "",
3679
+ node.getAttribute?.("data-test") || "",
3680
+ node.getAttribute?.("data-click") || "",
3681
+ node.getAttribute?.("data-action") || "",
3682
+ node.getAttribute?.("onclick") || "",
3683
+ String(node.textContent || "").trim().slice(0, 24)
3684
+ ].join(" ");
3685
+ if (interactiveHintRe.test(hints)) score += 4;
3686
+ const style = window.getComputedStyle(node);
3687
+ if (style?.cursor === "pointer") score += 2;
3688
+ return score;
3689
+ };
3690
+ const closestInteractive = (node) => {
3691
+ let best = null;
3692
+ let bestScore = 0;
3693
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3694
+ const score = interactiveScore(current);
3695
+ if (score > bestScore || score > 0 && score === bestScore) {
3696
+ best = current;
3697
+ bestScore = score;
3698
+ }
3699
+ }
3700
+ return best;
3701
+ };
3589
3702
  const rect = el.getBoundingClientRect();
3590
3703
  if (!rect || rect.width <= 0 || rect.height <= 0) {
3591
3704
  return null;
@@ -3622,16 +3735,13 @@ var resolveSafeTapPoint = async (element) => {
3622
3735
  if (visibleWidth <= 1 || visibleHeight <= 1) {
3623
3736
  return null;
3624
3737
  }
3625
- const targetInteractive = typeof el.closest === "function" ? el.closest(interactiveSelector) : null;
3738
+ const targetInteractive = closestInteractive(el);
3626
3739
  const sameInteractiveTarget = (pointElement) => {
3627
3740
  if (!pointElement) return false;
3628
3741
  if (pointElement === el || el.contains(pointElement) || pointElement.contains(el)) {
3629
3742
  return true;
3630
3743
  }
3631
- if (!targetInteractive || typeof pointElement.closest !== "function") {
3632
- return false;
3633
- }
3634
- return pointElement.closest(interactiveSelector) === targetInteractive;
3744
+ return Boolean(targetInteractive && closestInteractive(pointElement) === targetInteractive);
3635
3745
  };
3636
3746
  const cx = visibleLeft + visibleWidth / 2;
3637
3747
  const cy = visibleTop + visibleHeight / 2;
@@ -3659,14 +3769,53 @@ var resolveSafeTapPoint = async (element) => {
3659
3769
  x: chosen.x,
3660
3770
  y: chosen.y
3661
3771
  };
3662
- }, INTERACTIVE_SELECTOR);
3772
+ }, {
3773
+ interactiveSelector: INTERACTIVE_SELECTOR,
3774
+ interactiveHintPattern: INTERACTIVE_HINT_PATTERN
3775
+ });
3663
3776
  };
3664
3777
  var activateElementFallback = async (element, point = null, options = {}) => {
3665
- return element.evaluate((el, { innerPoint, innerOptions, interactiveSelector, editableSelector }) => {
3778
+ return element.evaluate((el, { innerPoint, innerOptions, interactiveSelector, editableSelector, interactiveHintPattern }) => {
3779
+ const interactiveHintRe = new RegExp(interactiveHintPattern, "i");
3780
+ const nodeClassName = (node) => {
3781
+ if (!node) return "";
3782
+ if (typeof node.className === "string") return node.className;
3783
+ if (node.className && typeof node.className.baseVal === "string") return node.className.baseVal;
3784
+ return "";
3785
+ };
3786
+ const interactiveScore = (node) => {
3787
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return 0;
3788
+ let score = 0;
3789
+ if (typeof node.matches === "function" && node.matches(interactiveSelector)) score += 8;
3790
+ const hints = [
3791
+ node.id || "",
3792
+ nodeClassName(node),
3793
+ node.getAttribute?.("aria-label") || "",
3794
+ node.getAttribute?.("title") || "",
3795
+ node.getAttribute?.("data-testid") || "",
3796
+ node.getAttribute?.("data-test") || "",
3797
+ node.getAttribute?.("data-click") || "",
3798
+ node.getAttribute?.("data-action") || "",
3799
+ node.getAttribute?.("onclick") || "",
3800
+ String(node.textContent || "").trim().slice(0, 24)
3801
+ ].join(" ");
3802
+ if (interactiveHintRe.test(hints)) score += 4;
3803
+ const style = window.getComputedStyle(node);
3804
+ if (style?.cursor === "pointer") score += 2;
3805
+ return score;
3806
+ };
3666
3807
  const pointElement = innerPoint ? document.elementFromPoint(innerPoint.x, innerPoint.y) : null;
3667
3808
  const nearestInteractive = (node) => {
3668
- if (!node || typeof node.closest !== "function") return null;
3669
- return node.closest(interactiveSelector);
3809
+ let best = null;
3810
+ let bestScore = 0;
3811
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
3812
+ const score = interactiveScore(current);
3813
+ if (score > bestScore || score > 0 && score === bestScore) {
3814
+ best = current;
3815
+ bestScore = score;
3816
+ }
3817
+ }
3818
+ return best;
3670
3819
  };
3671
3820
  const targetInteractive = nearestInteractive(el);
3672
3821
  const pointInteractive = nearestInteractive(pointElement);
@@ -3698,7 +3847,8 @@ var activateElementFallback = async (element, point = null, options = {}) => {
3698
3847
  innerPoint: point,
3699
3848
  innerOptions: options || {},
3700
3849
  interactiveSelector: INTERACTIVE_SELECTOR,
3701
- editableSelector: EDITABLE_SELECTOR
3850
+ editableSelector: EDITABLE_SELECTOR,
3851
+ interactiveHintPattern: INTERACTIVE_HINT_PATTERN
3702
3852
  });
3703
3853
  };
3704
3854
  var getScrollableRect = async (element) => {
@@ -3802,6 +3952,47 @@ var scrollAwayFromObstruction = async (element, status) => {
3802
3952
  };
3803
3953
  }, status);
3804
3954
  };
3955
+ var getElementViewportSnapshot = async (element) => {
3956
+ return element.evaluate((el) => {
3957
+ const rect = el.getBoundingClientRect();
3958
+ return {
3959
+ top: rect.top,
3960
+ bottom: rect.bottom,
3961
+ left: rect.left,
3962
+ right: rect.right,
3963
+ width: rect.width,
3964
+ height: rect.height,
3965
+ scrollX: window.scrollX,
3966
+ scrollY: window.scrollY
3967
+ };
3968
+ });
3969
+ };
3970
+ var isTargetImmobileAfterScroll = (before, after) => {
3971
+ if (!before || !after) return false;
3972
+ const rectDeltaY = Number(after.top || 0) - Number(before.top || 0);
3973
+ const rectDeltaX = Number(after.left || 0) - Number(before.left || 0);
3974
+ const scrollDeltaY = Number(after.scrollY || 0) - Number(before.scrollY || 0);
3975
+ const scrollDeltaX = Number(after.scrollX || 0) - Number(before.scrollX || 0);
3976
+ const rectMoved = Math.abs(rectDeltaY) > 3 || Math.abs(rectDeltaX) > 3;
3977
+ const pageMoved = Math.abs(scrollDeltaY) > 3 || Math.abs(scrollDeltaX) > 3;
3978
+ if (!rectMoved && !pageMoved) return true;
3979
+ if (pageMoved && !rectMoved) return true;
3980
+ if (Math.abs(scrollDeltaY) > 12 && Math.abs(rectDeltaY) < Math.min(12, Math.abs(scrollDeltaY) * 0.2)) {
3981
+ return true;
3982
+ }
3983
+ return false;
3984
+ };
3985
+ var restoreWindowFromSnapshot = async (page, before, after) => {
3986
+ if (!before || !after) return;
3987
+ if (Math.abs(Number(after.scrollX || 0) - Number(before.scrollX || 0)) <= 2 && Math.abs(Number(after.scrollY || 0) - Number(before.scrollY || 0)) <= 2) {
3988
+ return;
3989
+ }
3990
+ await page.evaluate(
3991
+ (state) => window.scrollTo(state.x, state.y),
3992
+ { x: Number(before.scrollX || 0), y: Number(before.scrollY || 0) }
3993
+ ).catch(() => {
3994
+ });
3995
+ };
3805
3996
  var dispatchTouchSwipe = async (page, deltaY, options = {}) => {
3806
3997
  const viewport = resolveViewport(page);
3807
3998
  const rawRect = options.rect || null;
@@ -3982,11 +4173,11 @@ var MobileHumanize = {
3982
4173
  const scrollRect = await getScrollableRect(element);
3983
4174
  if (!scrollRect && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
3984
4175
  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"})`);
3985
- return { element, didScroll, restore: null };
4176
+ return { element, didScroll, restore: null, unscrollable: true };
3986
4177
  }
3987
4178
  if (!scrollRect && status.isFixed && status.code === "OBSTRUCTED") {
3988
4179
  logger7.warn(`humanScroll | fixed/sticky \u76EE\u6807\u88AB\u906E\u6321\uFF0C\u6EDA\u52A8\u65E0\u6CD5\u89E3\u9664 (${status.obstruction?.tag || "unknown"})`);
3989
- return { element, didScroll, restore: null };
4180
+ return { element, didScroll, restore: null, unscrollable: true };
3990
4181
  }
3991
4182
  if (scrollRect && status.code === "OBSTRUCTED" && status.obstruction?.isFixed) {
3992
4183
  const moved = await scrollAwayFromObstruction(element, status);
@@ -4017,6 +4208,7 @@ var MobileHumanize = {
4017
4208
  }
4018
4209
  }
4019
4210
  const beforeWindowState = scrollRect ? await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY })) : null;
4211
+ const beforeElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4020
4212
  const beforeState = scrollRect ? await element.evaluate((el) => {
4021
4213
  const isScrollable = (node) => {
4022
4214
  const style = window.getComputedStyle(node);
@@ -4046,6 +4238,21 @@ var MobileHumanize = {
4046
4238
  logger7.debug(`humanScroll | \u7A97\u53E3\u6EDA\u52A8\u56DE\u6536 from=${Math.round(afterWindowState.y)} to=${Math.round(beforeWindowState.y)}`);
4047
4239
  }
4048
4240
  }
4241
+ let afterElementSnapshot = null;
4242
+ const readAfterElementSnapshot = async () => {
4243
+ if (!afterElementSnapshot) {
4244
+ afterElementSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4245
+ }
4246
+ return afterElementSnapshot;
4247
+ };
4248
+ if (!scrollRect && beforeElementSnapshot) {
4249
+ const afterSnapshot = await readAfterElementSnapshot();
4250
+ if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
4251
+ await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
4252
+ 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"})`);
4253
+ return { element, didScroll, restore: null, unscrollable: true };
4254
+ }
4255
+ }
4049
4256
  if (scrollRect && beforeState) {
4050
4257
  const afterState = await element.evaluate((el) => {
4051
4258
  const isScrollable = (node) => {
@@ -4083,6 +4290,14 @@ var MobileHumanize = {
4083
4290
  }
4084
4291
  }
4085
4292
  }
4293
+ if (scrollRect && beforeElementSnapshot) {
4294
+ const afterSnapshot = await getElementViewportSnapshot(element).catch(() => null);
4295
+ if (isTargetImmobileAfterScroll(beforeElementSnapshot, afterSnapshot)) {
4296
+ await restoreWindowFromSnapshot(page, beforeElementSnapshot, afterSnapshot);
4297
+ 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"})`);
4298
+ return { element, didScroll, restore: null, unscrollable: true };
4299
+ }
4300
+ }
4086
4301
  didScroll = true;
4087
4302
  }
4088
4303
  try {
@@ -4132,21 +4347,28 @@ var MobileHumanize = {
4132
4347
  logger7.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${targetDesc}`);
4133
4348
  return false;
4134
4349
  }
4135
- if (scrollIfNeeded) {
4136
- await MobileHumanize.humanScroll(page, element, { throwOnMissing });
4137
- }
4350
+ const scrollResult = scrollIfNeeded ? await MobileHumanize.humanScroll(page, element, { throwOnMissing }) : null;
4138
4351
  const status = await checkElementVisibility(element).catch(() => null);
4139
4352
  if (status && status.code !== "VISIBLE") {
4140
- if (fallbackDomClick && status.isFixed && status.code === "OUT_OF_VIEWPORT") {
4141
- const fallback = await withTimeout(
4353
+ if (fallbackDomClick && (status.code === "OUT_OF_VIEWPORT" || status.code === "OBSTRUCTED") && (status.isFixed || scrollResult?.unscrollable)) {
4354
+ let fallback = await withTimeout(
4142
4355
  () => activateElementFallback(element, null, {
4143
4356
  editableOnly: true
4144
4357
  }),
4145
4358
  activateFallbackTimeoutMs,
4146
4359
  "focus fallback"
4147
4360
  ).catch(() => null);
4361
+ if (!fallback?.activated) {
4362
+ fallback = await withTimeout(
4363
+ () => activateElementFallback(element, null, {
4364
+ editableOnly: false
4365
+ }),
4366
+ activateFallbackTimeoutMs,
4367
+ "activation fallback"
4368
+ ).catch(() => null);
4369
+ }
4148
4370
  if (fallback?.activated) {
4149
- logger7.warn(`humanClick: fixed/sticky \u76EE\u6807\u4E0D\u5728\u89C6\u53E3\u5185\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
4371
+ logger7.warn(`humanClick: \u4E0D\u53EF\u6EDA\u52A8\u76EE\u6807\u4E0D\u53EF\u7269\u7406\u70B9\u51FB\uFF0C\u5DF2\u7528 ${fallback.method} \u6FC0\u6D3B`);
4150
4372
  return true;
4151
4373
  }
4152
4374
  }
@@ -4263,6 +4485,20 @@ var MobileHumanize = {
4263
4485
  const locator = page.locator(selector);
4264
4486
  await MobileHumanize.humanClick(page, locator, { scrollIfNeeded: true });
4265
4487
  await waitJitter(160, 0.4);
4488
+ const readValue = async () => {
4489
+ try {
4490
+ return await locator.inputValue({ timeout: 600 });
4491
+ } catch {
4492
+ return await locator.evaluate((el) => "value" in el ? String(el.value || "") : String(el.textContent || "")).catch(() => "");
4493
+ }
4494
+ };
4495
+ const currentValue = await readValue();
4496
+ if (!currentValue) return;
4497
+ await page.keyboard.press("ControlOrMeta+A");
4498
+ await waitJitter(90, 0.35);
4499
+ await page.keyboard.press("Backspace");
4500
+ await waitJitter(120, 0.35);
4501
+ if (!await readValue()) return;
4266
4502
  await locator.evaluate((el) => {
4267
4503
  if ("value" in el) {
4268
4504
  el.value = "";