@skrillex1224/playwright-toolkit 3.0.26 → 3.0.28

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
@@ -202,7 +202,7 @@ var ActorInfo = {
202
202
  mode: "response",
203
203
  prefix: "https://www.doubao.com/thread/",
204
204
  xurl: [
205
- "/samantha/thread/share/save",
205
+ "/share/save",
206
206
  "data",
207
207
  "share_id"
208
208
  ]
@@ -467,10 +467,7 @@ function createInternalLogger(moduleName, explicitLogger) {
467
467
 
468
468
  // src/internals/screenshot.js
469
469
  import delay from "delay";
470
- import { Jimp, JimpMime, intToRGBA } from "jimp";
471
-
472
- // src/internals/constants.js
473
- var PageRuntimeStateKey = "__playwright_toolkit_runtime_state__";
470
+ import { Jimp, intToRGBA } from "jimp";
474
471
 
475
472
  // src/internals/viewport.js
476
473
  var toPositiveInt = (value) => {
@@ -502,35 +499,22 @@ var resolveCurrentViewportSize = async (page, fallback = {}) => {
502
499
  // src/internals/screenshot.js
503
500
  var logger = createInternalLogger("Screenshot");
504
501
  var DEFAULT_TIMEOUT_MS = 5e3;
505
- var FORCED_FULLPAGE_TYPE = "jpeg";
506
- var FORCED_FULLPAGE_QUALITY = 50;
507
- var SUPPORTED_TYPES = /* @__PURE__ */ new Set(["png", "jpeg", "webp"]);
508
- var EXPANDED_SCROLLABLE_CLASS = "__pk_expanded__";
509
- var STITCH_SCROLL_TARGET_ATTR = "data-pk-stitch-scroll-target";
510
502
  var DEFAULT_MAX_HEIGHT = 8e3;
511
- var DEFAULT_SETTLE_MS = 1e3;
512
- var DEFAULT_MOBILE_SETTLE_MS = 50;
513
- var DEFAULT_STITCH_SETTLE_MS = 120;
514
- var DEFAULT_STITCH_OVERLAP_PX = 24;
515
- var MIN_VIRTUALIZED_SCROLL_RATIO = 2.2;
516
- var MIN_SPARSE_SCROLL_RATIO = 4;
517
- var MIN_STITCH_VISIBLE_HEIGHT_PX = 120;
518
- var MIN_STITCH_VISIBLE_HEIGHT_RATIO = 0.22;
519
- var MIN_STITCH_OUTPUT_HEIGHT_RATIO = 0.35;
520
- var MIN_STITCH_OUTPUT_VIEWPORT_RATIO = 1.15;
521
- var MOBILE_VIEWPORT_WIDTH_THRESHOLD = 520;
522
- var DEFAULT_QUALITY_RETRY_ATTEMPTS = 2;
503
+ var DEFAULT_SETTLE_MS = 260;
504
+ var DEFAULT_QUALITY_RETRY_ATTEMPTS = 4;
523
505
  var DEFAULT_QUALITY_RETRY_DELAY_MS = 2500;
524
- var MIN_TRAILING_BLANK_GAP_PX = 320;
525
- var MIN_TRAILING_BLANK_GAP_RATIO = 0.12;
506
+ var EXPANDED_ATTR = "data-pk-screenshot-expanded";
507
+ var SUPPORTED_TYPES = /* @__PURE__ */ new Set(["png", "jpeg", "webp"]);
508
+ var FULLPAGE_FALLBACK_TYPE = "jpeg";
509
+ var FULLPAGE_FALLBACK_QUALITY = 50;
526
510
  var toPositiveNumber = (value, fallback = 0) => {
527
511
  const n = Number(value);
528
512
  if (!Number.isFinite(n) || n <= 0) return fallback;
529
513
  return n;
530
514
  };
531
515
  var toPositiveInteger = (value, fallback = 0) => {
532
- const number = Math.round(Number(value) || 0);
533
- return number > 0 ? number : fallback;
516
+ const n = Math.round(Number(value) || 0);
517
+ return n > 0 ? n : fallback;
534
518
  };
535
519
  var normalizeType = (value) => {
536
520
  const raw = String(value || "png").trim().toLowerCase();
@@ -545,964 +529,585 @@ var normalizeQuality = (value, type) => {
545
529
  if (rounded < 0 || rounded > 100) return void 0;
546
530
  return rounded;
547
531
  };
548
- var resolvePageDevice = async (page) => {
549
- const declared = normalizeDevice(page?.[PageRuntimeStateKey]?.device);
550
- if (declared === Device.Mobile) return Device.Mobile;
551
- const viewport = await resolveCurrentViewportSize(page).catch(() => null);
552
- const viewportWidth = Math.round(Number(viewport?.width) || 0);
553
- if (viewportWidth > 0 && viewportWidth <= MOBILE_VIEWPORT_WIDTH_THRESHOLD) {
554
- return Device.Mobile;
555
- }
556
- return declared;
557
- };
558
- var resolveStitchMode = (options = {}) => {
559
- const raw = options.stitch ?? options.stitching ?? options.strategy;
560
- if (raw === false || raw === "off" || raw === "none" || raw === "single") return "off";
561
- if (raw === true || raw === "force" || raw === "stitched") return "force";
562
- return "auto";
563
- };
564
532
  var buildFullPageClip = (metrics, viewport, maxClipHeight) => {
565
533
  const contentSize = metrics && typeof metrics === "object" ? metrics.contentSize || null : null;
566
534
  const width = Math.max(1, Math.ceil(viewport.width || contentSize?.width || 1));
567
535
  let height = Math.max(1, Math.ceil(contentSize?.height || viewport.height || 1));
568
- if (maxClipHeight > 0) {
569
- height = Math.min(height, maxClipHeight);
570
- }
571
- return {
572
- x: 0,
573
- y: 0,
574
- width,
575
- height,
576
- scale: 1
536
+ if (maxClipHeight > 0) height = Math.min(height, maxClipHeight);
537
+ return { x: 0, y: 0, width, height, scale: 1 };
538
+ };
539
+ var capturePageScreenshot = async (page, options = {}) => {
540
+ const fullPage = Boolean(options.fullPage);
541
+ const type = fullPage ? FULLPAGE_FALLBACK_TYPE : normalizeType(options.type);
542
+ const quality = fullPage ? FULLPAGE_FALLBACK_QUALITY : normalizeQuality(options.quality, type);
543
+ const timeout = toPositiveNumber(options.timeout, DEFAULT_TIMEOUT_MS);
544
+ const maxClipHeight = Math.round(toPositiveNumber(options.maxClipHeight, 0));
545
+ const fallbackOptions = {
546
+ type: type === "webp" ? "png" : type,
547
+ fullPage
577
548
  };
549
+ if (quality !== void 0 && fallbackOptions.type === "jpeg") fallbackOptions.quality = quality;
550
+ if (timeout > 0) fallbackOptions.timeout = timeout;
551
+ try {
552
+ const context = page && typeof page.context === "function" ? page.context() : null;
553
+ if (!context || typeof context.newCDPSession !== "function") {
554
+ throw new Error("CDP session is not available");
555
+ }
556
+ const session = await context.newCDPSession(page);
557
+ try {
558
+ const viewport = await resolveCurrentViewportSize(page);
559
+ const captureParams = {
560
+ format: type,
561
+ fromSurface: true,
562
+ captureBeyondViewport: fullPage,
563
+ optimizeForSpeed: true
564
+ };
565
+ if (quality !== void 0) captureParams.quality = quality;
566
+ if (fullPage) {
567
+ const metrics = await session.send("Page.getLayoutMetrics");
568
+ captureParams.clip = buildFullPageClip(metrics, viewport, maxClipHeight);
569
+ }
570
+ const result = await session.send("Page.captureScreenshot", captureParams);
571
+ if (!result || typeof result.data !== "string" || !result.data) {
572
+ throw new Error("CDP returned empty screenshot data");
573
+ }
574
+ return Buffer.from(result.data, "base64");
575
+ } finally {
576
+ if (typeof session.detach === "function") {
577
+ await session.detach().catch(() => {
578
+ });
579
+ }
580
+ }
581
+ } catch (error) {
582
+ logger.warning(`CDP \u622A\u56FE\u5931\u8D25\uFF0C\u56DE\u9000 page.screenshot: ${error?.message || error}`);
583
+ return await page.screenshot(fallbackOptions);
584
+ }
578
585
  };
579
- var expandScrollableContent = async (page, options = {}) => {
580
- return await page.evaluate(({ className, forceScrollableHeight, visibleOnly, expandDocumentElements }) => {
581
- const body = document.body;
582
- const root = document.documentElement;
583
- const scrollingElement = document.scrollingElement;
584
- const candidates = [];
585
- const seen = /* @__PURE__ */ new Set();
586
- let maxHeight = window.innerHeight || 0;
587
- const pushCandidate = (el) => {
588
- if (!el || seen.has(el)) return;
589
- seen.add(el);
590
- candidates.push(el);
586
+ var readMainFrameScroll = async (page) => {
587
+ return await page.evaluate(() => ({
588
+ x: Math.max(0, Math.round(window.scrollX || document.scrollingElement?.scrollLeft || 0)),
589
+ y: Math.max(0, Math.round(window.scrollY || document.scrollingElement?.scrollTop || 0))
590
+ })).catch(() => ({ x: 0, y: 0 }));
591
+ };
592
+ var restoreMainFrameScroll = async (page, state2) => {
593
+ if (!state2) return;
594
+ await page.evaluate(({ x, y }) => {
595
+ window.scrollTo(Math.max(0, Math.round(Number(x) || 0)), Math.max(0, Math.round(Number(y) || 0)));
596
+ }, state2).catch(() => {
597
+ });
598
+ };
599
+ var freezeViewportMetrics = async (page) => {
600
+ const viewport = await resolveCurrentViewportSize(page).catch(() => null);
601
+ if (!viewport?.width || !viewport?.height) return false;
602
+ return await page.evaluate(({ width, height }) => {
603
+ const key = "__pkScreenshotViewportFreeze";
604
+ if (window[key]?.active) return true;
605
+ const state2 = {
606
+ active: true,
607
+ entries: []
608
+ };
609
+ const override = (target, prop, value) => {
610
+ if (!target) return;
611
+ try {
612
+ const descriptor = Object.getOwnPropertyDescriptor(target, prop);
613
+ state2.entries.push({
614
+ target,
615
+ prop,
616
+ hadOwn: Boolean(descriptor),
617
+ descriptor
618
+ });
619
+ Object.defineProperty(target, prop, {
620
+ configurable: true,
621
+ get: () => value
622
+ });
623
+ } catch {
624
+ }
591
625
  };
592
- pushCandidate(scrollingElement);
593
- pushCandidate(root);
594
- pushCandidate(body);
595
- document.querySelectorAll("*").forEach(pushCandidate);
626
+ override(window, "innerWidth", width);
627
+ override(window, "innerHeight", height);
628
+ if (window.visualViewport) {
629
+ override(window.visualViewport, "width", width);
630
+ override(window.visualViewport, "height", height);
631
+ }
632
+ window[key] = state2;
633
+ return true;
634
+ }, {
635
+ width: Math.max(1, Math.round(viewport.width)),
636
+ height: Math.max(1, Math.round(viewport.height))
637
+ }).catch((error) => {
638
+ logger.warning(`\u51BB\u7ED3 viewport \u6307\u6807\u5931\u8D25: ${error?.message || error}`);
639
+ return false;
640
+ });
641
+ };
642
+ var restoreViewportMetrics = async (page) => {
643
+ await page.evaluate(() => {
644
+ const key = "__pkScreenshotViewportFreeze";
645
+ const state2 = window[key];
646
+ if (!state2?.active) return;
647
+ for (const entry of [...state2.entries].reverse()) {
648
+ try {
649
+ if (entry.hadOwn && entry.descriptor) {
650
+ Object.defineProperty(entry.target, entry.prop, entry.descriptor);
651
+ } else {
652
+ delete entry.target[entry.prop];
653
+ }
654
+ } catch {
655
+ }
656
+ }
657
+ delete window[key];
658
+ }).catch(() => {
659
+ });
660
+ };
661
+ var expandScrollableContentInContext = async (context, options = {}) => {
662
+ return await context.evaluate(async ({
663
+ expandedAttr,
664
+ maxHeight,
665
+ preloadLazyContent,
666
+ preloadSettleMs,
667
+ visibleOnly
668
+ }) => {
669
+ const root = document.documentElement;
670
+ const body = document.body;
671
+ const scrollingElement = document.scrollingElement || root || body;
672
+ const viewportWidth = window.innerWidth || root?.clientWidth || body?.clientWidth || 1;
673
+ const viewportHeight = window.innerHeight || root?.clientHeight || body?.clientHeight || 1;
596
674
  const scrollableOverflow = /* @__PURE__ */ new Set(["auto", "scroll", "overlay"]);
597
675
  const clippingOverflow = /* @__PURE__ */ new Set(["hidden", "clip"]);
676
+ const candidates = [];
677
+ const changed = [];
678
+ const push = (el) => {
679
+ if (el && !candidates.includes(el)) candidates.push(el);
680
+ };
681
+ push(scrollingElement);
682
+ push(root);
683
+ push(body);
684
+ document.querySelectorAll("*").forEach(push);
598
685
  const isDocumentElement = (el) => el === root || el === body || el === scrollingElement;
686
+ const delayMs = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
687
+ const styleValue = (el, prop) => el.style.getPropertyValue(prop) || "";
688
+ const stylePriority = (el, prop) => el.style.getPropertyPriority(prop) || "";
599
689
  const isVisible = (el, style, rect) => {
600
690
  if (!el || !style || !rect) return false;
601
- if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") {
602
- return false;
603
- }
604
- if (Number(style.opacity) === 0) return false;
691
+ if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") return false;
692
+ if (Number(style.opacity || 1) === 0) return false;
605
693
  return rect.width > 0 && rect.height > 0;
606
694
  };
695
+ const intersectsViewport = (rect) => {
696
+ if (!rect || rect.width <= 0 || rect.height <= 0) return false;
697
+ return rect.bottom > 0 && rect.right > 0 && rect.top < viewportHeight && rect.left < viewportWidth;
698
+ };
699
+ const isVisibleInViewport = (el, style, rect) => isVisible(el, style, rect) && intersectsViewport(rect);
607
700
  const hasOverflowValue = (style, values) => {
608
- const overflowY = String(style?.overflowY || "").toLowerCase();
609
701
  const overflow = String(style?.overflow || "").toLowerCase();
610
- return values.has(overflowY) || values.has(overflow);
702
+ const overflowY = String(style?.overflowY || "").toLowerCase();
703
+ return values.has(overflow) || values.has(overflowY);
611
704
  };
612
705
  const isLargeVerticalClipper = (el, rect) => {
613
706
  if (!el || !rect) return false;
614
- const viewportHeight = window.innerHeight || 0;
615
- const visibleHeight = Math.ceil(rect.height || 0);
616
- const clientHeight = Math.ceil(el.clientHeight || 0);
617
- const boxHeight = Math.max(visibleHeight, clientHeight);
707
+ const boxHeight = Math.max(Math.ceil(rect.height || 0), Math.ceil(el.clientHeight || 0));
618
708
  return boxHeight >= Math.max(320, viewportHeight * 0.45);
619
709
  };
620
- const hasLargeVerticalOverflow = (el, rect) => {
621
- if (!isLargeVerticalClipper(el, rect)) return false;
622
- const viewportHeight = window.innerHeight || 0;
623
- const scrollHeight = Math.ceil(el.scrollHeight || 0);
624
- const clientHeight = Math.ceil(el.clientHeight || 0);
625
- const overflowDelta = scrollHeight - clientHeight;
626
- return overflowDelta >= Math.max(160, viewportHeight * 0.18);
627
- };
628
- const rememberOriginalBoxStyles = (el, { includeDocumentElement = false } = {}) => {
629
- if (!el || !includeDocumentElement && isDocumentElement(el) || el.classList.contains(className)) {
630
- return;
631
- }
632
- el.dataset.pkOrigOverflow = el.style.overflow;
633
- el.dataset.pkOrigOverflowPriority = el.style.getPropertyPriority("overflow") || "";
634
- el.dataset.pkOrigHeight = el.style.height;
635
- el.dataset.pkOrigHeightPriority = el.style.getPropertyPriority("height") || "";
636
- el.dataset.pkOrigMinHeight = el.style.minHeight;
637
- el.dataset.pkOrigMinHeightPriority = el.style.getPropertyPriority("min-height") || "";
638
- el.dataset.pkOrigMaxHeight = el.style.maxHeight;
639
- el.dataset.pkOrigMaxHeightPriority = el.style.getPropertyPriority("max-height") || "";
640
- el.classList.add(className);
641
- };
642
- const expandDocumentElementsToHeight = (height) => {
643
- if (!expandDocumentElements) return;
644
- const targetHeight = Math.ceil(Number(height) || 0);
645
- if (targetHeight <= 0) return;
646
- [root, body, scrollingElement].forEach((el) => {
647
- if (!el) return;
648
- rememberOriginalBoxStyles(el, { includeDocumentElement: true });
649
- el.style.setProperty("overflow", "visible", "important");
650
- el.style.setProperty("height", `${targetHeight}px`, "important");
651
- el.style.setProperty("min-height", `${targetHeight}px`, "important");
652
- el.style.setProperty("max-height", "none", "important");
653
- });
654
- };
655
710
  const isScrollableY = (el, style, rect) => {
656
- if (!el || el.scrollHeight <= el.clientHeight + 1) {
657
- return false;
658
- }
659
- if (isDocumentElement(el)) {
660
- return true;
661
- }
662
- if (hasOverflowValue(style, scrollableOverflow)) {
663
- return true;
664
- }
665
- return hasOverflowValue(style, clippingOverflow) && hasLargeVerticalOverflow(el, rect);
711
+ if (!el || Math.ceil(el.scrollHeight || 0) <= Math.ceil(el.clientHeight || 0) + 2) return false;
712
+ if (isDocumentElement(el)) return true;
713
+ if (hasOverflowValue(style, scrollableOverflow)) return true;
714
+ if (!hasOverflowValue(style, clippingOverflow)) return false;
715
+ const overflowDelta = Math.ceil(el.scrollHeight || 0) - Math.ceil(el.clientHeight || 0);
716
+ return isLargeVerticalClipper(el, rect) && overflowDelta >= Math.max(160, viewportHeight * 0.18);
666
717
  };
667
- const measureNeededHeight = (el) => {
668
- if (!el) return 0;
669
- const scrollHeight = Math.ceil(el.scrollHeight || 0);
670
- if (scrollHeight <= 0) return 0;
671
- if (isDocumentElement(el)) {
672
- return scrollHeight;
673
- }
674
- const rect = el.getBoundingClientRect();
675
- if (!rect || rect.width <= 0 || rect.height <= 0) {
676
- return 0;
677
- }
678
- const documentTop = Math.max(0, Math.ceil(rect.top + (window.scrollY || window.pageYOffset || 0)));
679
- return documentTop + scrollHeight;
718
+ const rememberBox = (el) => {
719
+ if (!el || el.getAttribute(expandedAttr) === "1") return;
720
+ el.setAttribute(expandedAttr, "1");
721
+ el.dataset.pkSsOverflow = styleValue(el, "overflow");
722
+ el.dataset.pkSsOverflowPriority = stylePriority(el, "overflow");
723
+ el.dataset.pkSsOverflowX = styleValue(el, "overflow-x");
724
+ el.dataset.pkSsOverflowXPriority = stylePriority(el, "overflow-x");
725
+ el.dataset.pkSsOverflowY = styleValue(el, "overflow-y");
726
+ el.dataset.pkSsOverflowYPriority = stylePriority(el, "overflow-y");
727
+ el.dataset.pkSsHeight = styleValue(el, "height");
728
+ el.dataset.pkSsHeightPriority = stylePriority(el, "height");
729
+ el.dataset.pkSsMinHeight = styleValue(el, "min-height");
730
+ el.dataset.pkSsMinHeightPriority = stylePriority(el, "min-height");
731
+ el.dataset.pkSsMaxHeight = styleValue(el, "max-height");
732
+ el.dataset.pkSsMaxHeightPriority = stylePriority(el, "max-height");
733
+ el.dataset.pkSsWidth = styleValue(el, "width");
734
+ el.dataset.pkSsWidthPriority = stylePriority(el, "width");
735
+ el.dataset.pkSsBoxSizing = styleValue(el, "box-sizing");
736
+ el.dataset.pkSsBoxSizingPriority = stylePriority(el, "box-sizing");
737
+ el.dataset.pkSsAlignSelf = styleValue(el, "align-self");
738
+ el.dataset.pkSsAlignSelfPriority = stylePriority(el, "align-self");
739
+ el.dataset.pkSsJustifySelf = styleValue(el, "justify-self");
740
+ el.dataset.pkSsJustifySelfPriority = stylePriority(el, "justify-self");
741
+ el.dataset.pkSsScrollTop = String(Math.max(0, Math.round(el.scrollTop || 0)));
742
+ el.dataset.pkSsScrollLeft = String(Math.max(0, Math.round(el.scrollLeft || 0)));
743
+ changed.push(el);
680
744
  };
681
- const scrollableElements = [];
682
- const expandedAncestors = [];
683
- const expandElementToScrollHeight = (el) => {
684
- if (!el || isDocumentElement(el)) return;
685
- const scrollHeight = Math.ceil(el.scrollHeight || 0);
686
- if (scrollHeight <= 0) return;
687
- rememberOriginalBoxStyles(el);
745
+ const setExpandedBox = (el, { height, width }) => {
746
+ if (!el) return;
747
+ rememberBox(el);
688
748
  el.style.setProperty("overflow", "visible", "important");
689
- el.style.setProperty("height", `${scrollHeight}px`, "important");
690
- el.style.setProperty("min-height", `${scrollHeight}px`, "important");
749
+ el.style.setProperty("overflow-y", "visible", "important");
691
750
  el.style.setProperty("max-height", "none", "important");
751
+ if (height > 0) {
752
+ const cssHeight = `${Math.ceil(height)}px`;
753
+ el.style.setProperty("height", cssHeight, "important");
754
+ el.style.setProperty("min-height", cssHeight, "important");
755
+ }
756
+ if (!isDocumentElement(el) && width > 0) {
757
+ el.style.setProperty("box-sizing", "border-box", "important");
758
+ el.style.setProperty("width", `${Math.ceil(width)}px`, "important");
759
+ el.style.setProperty("align-self", "flex-start", "important");
760
+ el.style.setProperty("justify-self", "start", "important");
761
+ }
762
+ el.scrollTop = 0;
763
+ el.scrollLeft = 0;
692
764
  };
693
765
  const expandClippingAncestors = (el) => {
694
766
  for (let node = el?.parentElement; node && !isDocumentElement(node); node = node.parentElement) {
695
767
  const style = window.getComputedStyle(node);
696
768
  const rect = node.getBoundingClientRect();
697
769
  if (!isVisible(node, style, rect)) continue;
698
- const scrollableClip = hasOverflowValue(style, scrollableOverflow);
699
- const visualClip = hasOverflowValue(style, clippingOverflow);
700
- if (!scrollableClip && (!visualClip || !isLargeVerticalClipper(node, rect))) continue;
701
- rememberOriginalBoxStyles(node);
770
+ const clips = hasOverflowValue(style, scrollableOverflow) || hasOverflowValue(style, clippingOverflow);
771
+ if (!clips) continue;
772
+ rememberBox(node);
702
773
  node.style.setProperty("overflow", "visible", "important");
774
+ node.style.setProperty("overflow-y", "visible", "important");
703
775
  node.style.setProperty("max-height", "none", "important");
704
- expandedAncestors.push(node);
705
776
  }
706
777
  };
707
- const expandAncestorsToCurrentScrollHeight = () => {
708
- const ancestors = [...new Set(expandedAncestors)];
709
- for (let pass = 0; pass < 2; pass += 1) {
710
- ancestors.forEach((node) => {
711
- if (!node || isDocumentElement(node)) return;
712
- const scrollHeight = Math.ceil(node.scrollHeight || 0);
713
- if (scrollHeight <= node.clientHeight + 1) return;
714
- rememberOriginalBoxStyles(node);
715
- node.style.setProperty("height", `${scrollHeight}px`, "important");
716
- node.style.setProperty("min-height", `${scrollHeight}px`, "important");
717
- node.style.setProperty("max-height", "none", "important");
718
- });
778
+ const preloadCandidates = [];
779
+ for (const el of candidates) {
780
+ const style = window.getComputedStyle(el);
781
+ const rect = isDocumentElement(el) ? { top: 0, left: 0, width: viewportWidth, height: viewportHeight } : el.getBoundingClientRect();
782
+ if (visibleOnly && !isDocumentElement(el) && !isVisibleInViewport(el, style, rect)) continue;
783
+ if (!isScrollableY(el, style, rect)) continue;
784
+ preloadCandidates.push(el);
785
+ }
786
+ if (preloadLazyContent && preloadCandidates.length > 0) {
787
+ preloadCandidates.forEach((el) => {
788
+ rememberBox(el);
789
+ const maxTop = Math.max(0, Math.ceil((el.scrollHeight || 0) - (el.clientHeight || viewportHeight || 0)));
790
+ if (isDocumentElement(el)) {
791
+ window.scrollTo(window.scrollX || 0, maxTop);
792
+ } else {
793
+ el.scrollTop = maxTop;
794
+ el.dispatchEvent(new Event("scroll", { bubbles: true }));
795
+ }
796
+ });
797
+ window.dispatchEvent(new Event("scroll"));
798
+ await delayMs(preloadSettleMs);
799
+ }
800
+ let targetHeight = viewportHeight;
801
+ let scrollableCount = 0;
802
+ let expandedCount = 0;
803
+ for (const el of preloadCandidates) {
804
+ const rect = isDocumentElement(el) ? { top: 0, left: 0, width: viewportWidth, height: viewportHeight } : el.getBoundingClientRect();
805
+ const scrollHeight = Math.ceil(el.scrollHeight || 0);
806
+ const clientHeight = Math.ceil(el.clientHeight || viewportHeight || 0);
807
+ if (scrollHeight <= clientHeight + 2) continue;
808
+ scrollableCount += 1;
809
+ if (isDocumentElement(el)) {
810
+ targetHeight = Math.max(targetHeight, scrollHeight);
811
+ el.scrollTop = 0;
812
+ continue;
719
813
  }
720
- };
721
- const measureExpandedRenderedBottom = () => {
722
- let renderedBottom = maxHeight;
814
+ const documentTop = Math.max(0, Math.ceil(rect.top + (window.scrollY || window.pageYOffset || 0)));
815
+ const width = Math.max(1, Math.ceil(rect.width || el.clientWidth || viewportWidth || 1));
816
+ targetHeight = Math.max(targetHeight, documentTop + scrollHeight);
817
+ expandClippingAncestors(el);
818
+ setExpandedBox(el, { height: scrollHeight, width });
819
+ expandedCount += 1;
820
+ }
821
+ for (let pass = 0; pass < 2; pass += 1) {
822
+ for (const el of changed) {
823
+ if (!el || isDocumentElement(el)) continue;
824
+ const height = Math.ceil(el.scrollHeight || 0);
825
+ if (height > Math.ceil(el.clientHeight || 0) + 2) {
826
+ el.style.setProperty("height", `${height}px`, "important");
827
+ el.style.setProperty("min-height", `${height}px`, "important");
828
+ }
829
+ }
830
+ }
831
+ const measureDocumentHeight = () => Math.max(
832
+ viewportHeight,
833
+ Math.ceil(root?.scrollHeight || 0),
834
+ Math.ceil(body?.scrollHeight || 0),
835
+ Math.ceil(scrollingElement?.scrollHeight || 0)
836
+ );
837
+ const measureExpandedBottom = () => {
838
+ let bottom = viewportHeight;
723
839
  const scrollY = window.scrollY || window.pageYOffset || 0;
724
- candidates.forEach((el) => {
840
+ document.querySelectorAll(`[${expandedAttr}="1"]`).forEach((el) => {
725
841
  if (!el || isDocumentElement(el)) return;
726
- if (!el.classList.contains(className) && !el.closest(`.${className}`)) return;
727
- const style = window.getComputedStyle(el);
728
842
  const rect = el.getBoundingClientRect();
729
- if (!isVisible(el, style, rect)) return;
730
- renderedBottom = Math.max(renderedBottom, Math.ceil(rect.bottom + scrollY));
843
+ if (!rect || rect.width <= 0 || rect.height <= 0) return;
844
+ bottom = Math.max(bottom, Math.ceil(rect.bottom + scrollY));
731
845
  });
732
- return renderedBottom;
846
+ return bottom;
733
847
  };
734
- const measureDocumentScrollHeight = () => {
735
- return Math.max(
736
- maxHeight,
737
- Math.ceil(root?.scrollHeight || 0),
738
- Math.ceil(body?.scrollHeight || 0),
739
- Math.ceil(scrollingElement?.scrollHeight || 0)
740
- );
741
- };
742
- candidates.forEach((el) => {
743
- const style = window.getComputedStyle(el);
744
- const rect = el.getBoundingClientRect();
745
- if (visibleOnly && !isVisible(el, style, rect)) {
746
- return;
747
- }
748
- if (!isScrollableY(el, style, rect)) {
749
- return;
750
- }
751
- const neededHeight = measureNeededHeight(el);
752
- if (neededHeight <= 0) {
753
- return;
754
- }
755
- if (neededHeight > maxHeight) {
756
- maxHeight = neededHeight;
757
- }
758
- scrollableElements.push(el);
759
- if (isDocumentElement(el)) {
760
- return;
761
- }
762
- expandClippingAncestors(el);
763
- if (forceScrollableHeight) {
764
- expandElementToScrollHeight(el);
765
- return;
766
- }
767
- rememberOriginalBoxStyles(el);
768
- el.style.overflow = "visible";
769
- el.style.height = "auto";
770
- el.style.maxHeight = "none";
848
+ targetHeight = Math.max(targetHeight, measureDocumentHeight(), measureExpandedBottom());
849
+ targetHeight = Math.min(Math.max(1, Math.ceil(targetHeight)), Math.max(1, Math.ceil(maxHeight || targetHeight)));
850
+ [root, body, scrollingElement].forEach((el) => {
851
+ if (!el) return;
852
+ setExpandedBox(el, { height: targetHeight, width: 0 });
771
853
  });
772
- expandAncestorsToCurrentScrollHeight();
773
- scrollableElements.forEach((el) => {
774
- const neededHeight = measureNeededHeight(el);
775
- if (neededHeight > maxHeight) {
776
- maxHeight = neededHeight;
854
+ window.scrollTo(0, 0);
855
+ document.querySelectorAll(`[${expandedAttr}="1"]`).forEach((el) => {
856
+ if (!isDocumentElement(el)) {
857
+ el.scrollTop = 0;
858
+ el.scrollLeft = 0;
777
859
  }
778
860
  });
779
- maxHeight = Math.max(maxHeight, measureDocumentScrollHeight(), measureExpandedRenderedBottom());
780
- if (expandDocumentElements) {
781
- for (let pass = 0; pass < 2; pass += 1) {
782
- expandDocumentElementsToHeight(maxHeight);
783
- maxHeight = Math.max(maxHeight, measureDocumentScrollHeight(), measureExpandedRenderedBottom());
784
- }
785
- }
786
- return Math.ceil(maxHeight);
861
+ window.dispatchEvent(new Event("scroll"));
862
+ await delayMs(preloadSettleMs);
863
+ return {
864
+ height: Math.max(targetHeight, measureDocumentHeight(), measureExpandedBottom()),
865
+ scrollableCount,
866
+ expandedCount
867
+ };
787
868
  }, {
788
- className: EXPANDED_SCROLLABLE_CLASS,
789
- forceScrollableHeight: options.forceScrollableHeight !== false,
790
- visibleOnly: options.visibleOnly !== false,
791
- expandDocumentElements: options.expandDocumentElements === true
869
+ expandedAttr: EXPANDED_ATTR,
870
+ maxHeight: toPositiveInteger(options.maxHeight, DEFAULT_MAX_HEIGHT),
871
+ preloadLazyContent: options.preloadLazyContent !== false,
872
+ preloadSettleMs: Math.max(0, Math.min(500, Number(options.preloadSettleMs ?? options.settleMs ?? 120) || 0)),
873
+ visibleOnly: options.visibleOnly !== false
874
+ }).catch((error) => {
875
+ logger.warning(`\u5C55\u5F00\u6EDA\u52A8\u5BB9\u5668\u5931\u8D25: ${error?.message || error}`);
876
+ return { height: 0, scrollableCount: 0, expandedCount: 0 };
792
877
  });
793
878
  };
794
- var adjustAffixedElementsForExpandedScreenshot = async (page, options = {}) => {
795
- return await page.evaluate(({ className, targetHeight }) => {
796
- const viewportWidth = window.innerWidth || document.documentElement?.clientWidth || document.body?.clientWidth || 0;
797
- const viewportHeight = window.innerHeight || document.documentElement?.clientHeight || document.body?.clientHeight || 0;
798
- const scrollY = window.scrollY || window.pageYOffset || 0;
799
- const safeTargetHeight = Math.ceil(Number(targetHeight) || 0);
800
- if (safeTargetHeight <= viewportHeight + 1) {
801
- return 0;
802
- }
803
- const hasOwn2 = (source, key) => Object.prototype.hasOwnProperty.call(source, key);
804
- const isVisible = (el, style, rect) => {
805
- if (!el || !style || !rect) return false;
806
- if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") {
807
- return false;
808
- }
809
- if (Number(style.opacity) === 0) return false;
810
- return rect.width > 0 && rect.height > 0;
811
- };
812
- const edgeThreshold = Math.max(24, Math.min(96, viewportHeight * 0.12));
813
- const maxAffixedHeight = Math.max(56, viewportHeight * 0.65);
814
- const candidates = [];
815
- document.querySelectorAll("*").forEach((el) => {
816
- const style = window.getComputedStyle(el);
817
- const position = String(style.position || "").toLowerCase();
818
- if (position !== "fixed" && position !== "sticky") return;
819
- const rect = el.getBoundingClientRect();
820
- if (!isVisible(el, style, rect)) return;
821
- if (rect.height > maxAffixedHeight) return;
822
- if (rect.width < viewportWidth * 0.25 && rect.height < 80) return;
823
- const top = Number.parseFloat(style.top);
824
- const bottom = Number.parseFloat(style.bottom);
825
- const hasTopRule = Number.isFinite(top) && top <= Math.max(160, viewportHeight * 0.25);
826
- const hasBottomRule = Number.isFinite(bottom) && bottom <= Math.max(160, viewportHeight * 0.25);
827
- const isNearViewportTop = rect.top <= edgeThreshold;
828
- const isNearViewportBottom = rect.bottom >= viewportHeight - edgeThreshold;
829
- const edge = (hasBottomRule || isNearViewportBottom) && !(hasTopRule || isNearViewportTop) ? "bottom" : "top";
830
- if (position === "fixed" && edge === "top" && !hasTopRule && !isNearViewportTop) return;
831
- if (position === "fixed" && edge === "bottom" && !hasBottomRule && !isNearViewportBottom) return;
832
- if (position === "sticky" && !hasTopRule && !hasBottomRule && !isNearViewportTop && !isNearViewportBottom) return;
833
- candidates.push({ el, position, edge });
834
- });
835
- const candidateSet = new Set(candidates.map(({ el }) => el));
836
- const topLevelCandidates = candidates.filter(({ el }) => {
837
- for (let parent = el.parentElement; parent; parent = parent.parentElement) {
838
- if (candidateSet.has(parent)) return false;
879
+ var expandFrameElement = async (frame, height) => {
880
+ const handle = await frame.frameElement?.().catch(() => null);
881
+ if (!handle) return false;
882
+ try {
883
+ return await handle.evaluate((el, { expandedAttr, targetHeight }) => {
884
+ if (!el || targetHeight <= 0) return false;
885
+ if (el.getAttribute(expandedAttr) !== "1") {
886
+ el.setAttribute(expandedAttr, "1");
887
+ el.dataset.pkSsOverflow = el.style.getPropertyValue("overflow") || "";
888
+ el.dataset.pkSsOverflowPriority = el.style.getPropertyPriority("overflow") || "";
889
+ el.dataset.pkSsOverflowX = el.style.getPropertyValue("overflow-x") || "";
890
+ el.dataset.pkSsOverflowXPriority = el.style.getPropertyPriority("overflow-x") || "";
891
+ el.dataset.pkSsOverflowY = el.style.getPropertyValue("overflow-y") || "";
892
+ el.dataset.pkSsOverflowYPriority = el.style.getPropertyPriority("overflow-y") || "";
893
+ el.dataset.pkSsHeight = el.style.getPropertyValue("height") || "";
894
+ el.dataset.pkSsHeightPriority = el.style.getPropertyPriority("height") || "";
895
+ el.dataset.pkSsMinHeight = el.style.getPropertyValue("min-height") || "";
896
+ el.dataset.pkSsMinHeightPriority = el.style.getPropertyPriority("min-height") || "";
897
+ el.dataset.pkSsMaxHeight = el.style.getPropertyValue("max-height") || "";
898
+ el.dataset.pkSsMaxHeightPriority = el.style.getPropertyPriority("max-height") || "";
899
+ el.dataset.pkSsWidth = el.style.getPropertyValue("width") || "";
900
+ el.dataset.pkSsWidthPriority = el.style.getPropertyPriority("width") || "";
901
+ el.dataset.pkSsBoxSizing = el.style.getPropertyValue("box-sizing") || "";
902
+ el.dataset.pkSsBoxSizingPriority = el.style.getPropertyPriority("box-sizing") || "";
903
+ el.dataset.pkSsAlignSelf = el.style.getPropertyValue("align-self") || "";
904
+ el.dataset.pkSsAlignSelfPriority = el.style.getPropertyPriority("align-self") || "";
905
+ el.dataset.pkSsJustifySelf = el.style.getPropertyValue("justify-self") || "";
906
+ el.dataset.pkSsJustifySelfPriority = el.style.getPropertyPriority("justify-self") || "";
907
+ el.dataset.pkSsScrollTop = String(Math.max(0, Math.round(el.scrollTop || 0)));
908
+ el.dataset.pkSsScrollLeft = String(Math.max(0, Math.round(el.scrollLeft || 0)));
839
909
  }
910
+ el.style.setProperty("overflow", "visible", "important");
911
+ el.style.setProperty("overflow-y", "visible", "important");
912
+ el.style.setProperty("height", `${Math.ceil(targetHeight)}px`, "important");
913
+ el.style.setProperty("min-height", `${Math.ceil(targetHeight)}px`, "important");
914
+ el.style.setProperty("max-height", "none", "important");
915
+ el.style.setProperty("align-self", "flex-start", "important");
916
+ el.style.setProperty("justify-self", "start", "important");
840
917
  return true;
918
+ }, { expandedAttr: EXPANDED_ATTR, targetHeight: Math.ceil(height) });
919
+ } finally {
920
+ await handle.dispose?.().catch(() => {
841
921
  });
842
- topLevelCandidates.forEach(({ el, position, edge }) => {
843
- if (!hasOwn2(el.dataset, "pkAffixedAdjusted")) {
844
- el.dataset.pkAffixedAdjusted = "1";
845
- el.dataset.pkOrigPosition = el.style.getPropertyValue("position") || "";
846
- el.dataset.pkOrigPositionPriority = el.style.getPropertyPriority("position") || "";
847
- el.dataset.pkOrigTop = el.style.getPropertyValue("top") || "";
848
- el.dataset.pkOrigTopPriority = el.style.getPropertyPriority("top") || "";
849
- el.dataset.pkOrigBottom = el.style.getPropertyValue("bottom") || "";
850
- el.dataset.pkOrigBottomPriority = el.style.getPropertyPriority("bottom") || "";
851
- el.dataset.pkOrigTranslate = el.style.getPropertyValue("translate") || "";
852
- el.dataset.pkOrigTranslatePriority = el.style.getPropertyPriority("translate") || "";
853
- }
854
- el.classList.add(className);
855
- if (position === "fixed") {
856
- const deltaY = edge === "bottom" ? safeTargetHeight - viewportHeight - scrollY : -scrollY;
857
- if (Math.abs(deltaY) > 1) {
858
- el.style.setProperty("translate", `0 ${Math.round(deltaY)}px`, "important");
859
- }
860
- return;
861
- }
862
- el.style.setProperty("position", "relative", "important");
863
- el.style.setProperty("top", "auto", "important");
864
- el.style.setProperty("bottom", "auto", "important");
865
- });
866
- return topLevelCandidates.length;
867
- }, {
868
- className: EXPANDED_SCROLLABLE_CLASS,
869
- targetHeight: options.targetHeight
870
- });
922
+ }
871
923
  };
872
- var restoreAffixedElementsForExpandedScreenshot = async (page) => {
873
- await page.evaluate((className) => {
924
+ var restoreExpandedStateInContext = async (context) => {
925
+ await context.evaluate(({ expandedAttr }) => {
874
926
  const hasOwn2 = (source, key) => Object.prototype.hasOwnProperty.call(source, key);
875
- const expansionKeys = [
876
- "pkOrigOverflow",
877
- "pkOrigHeight",
878
- "pkOrigMinHeight",
879
- "pkOrigMaxHeight"
880
- ];
881
- document.querySelectorAll('[data-pk-affixed-adjusted="1"]').forEach((el) => {
882
- if (hasOwn2(el.dataset, "pkOrigPosition")) {
883
- el.style.setProperty("position", el.dataset.pkOrigPosition || "", el.dataset.pkOrigPositionPriority || "");
884
- delete el.dataset.pkOrigPosition;
885
- delete el.dataset.pkOrigPositionPriority;
886
- }
887
- if (hasOwn2(el.dataset, "pkOrigTop")) {
888
- el.style.setProperty("top", el.dataset.pkOrigTop || "", el.dataset.pkOrigTopPriority || "");
889
- delete el.dataset.pkOrigTop;
890
- delete el.dataset.pkOrigTopPriority;
891
- }
892
- if (hasOwn2(el.dataset, "pkOrigBottom")) {
893
- el.style.setProperty("bottom", el.dataset.pkOrigBottom || "", el.dataset.pkOrigBottomPriority || "");
894
- delete el.dataset.pkOrigBottom;
895
- delete el.dataset.pkOrigBottomPriority;
896
- }
897
- if (hasOwn2(el.dataset, "pkOrigTranslate")) {
898
- el.style.setProperty("translate", el.dataset.pkOrigTranslate || "", el.dataset.pkOrigTranslatePriority || "");
899
- delete el.dataset.pkOrigTranslate;
900
- delete el.dataset.pkOrigTranslatePriority;
901
- }
902
- delete el.dataset.pkAffixedAdjusted;
903
- const stillExpanded = expansionKeys.some((key) => hasOwn2(el.dataset, key));
904
- if (!stillExpanded) {
905
- el.classList.remove(className);
927
+ const restoreStyle = (el, prop, valueKey, priorityKey) => {
928
+ if (!hasOwn2(el.dataset, valueKey)) return;
929
+ const value = el.dataset[valueKey] || "";
930
+ const priority = el.dataset[priorityKey] || "";
931
+ if (value) {
932
+ el.style.setProperty(prop, value, priority);
933
+ } else {
934
+ el.style.removeProperty(prop);
906
935
  }
936
+ delete el.dataset[valueKey];
937
+ delete el.dataset[priorityKey];
938
+ };
939
+ document.querySelectorAll(`[${expandedAttr}="1"]`).forEach((el) => {
940
+ restoreStyle(el, "overflow", "pkSsOverflow", "pkSsOverflowPriority");
941
+ restoreStyle(el, "overflow-x", "pkSsOverflowX", "pkSsOverflowXPriority");
942
+ restoreStyle(el, "overflow-y", "pkSsOverflowY", "pkSsOverflowYPriority");
943
+ restoreStyle(el, "height", "pkSsHeight", "pkSsHeightPriority");
944
+ restoreStyle(el, "min-height", "pkSsMinHeight", "pkSsMinHeightPriority");
945
+ restoreStyle(el, "max-height", "pkSsMaxHeight", "pkSsMaxHeightPriority");
946
+ restoreStyle(el, "width", "pkSsWidth", "pkSsWidthPriority");
947
+ restoreStyle(el, "box-sizing", "pkSsBoxSizing", "pkSsBoxSizingPriority");
948
+ restoreStyle(el, "align-self", "pkSsAlignSelf", "pkSsAlignSelfPriority");
949
+ restoreStyle(el, "justify-self", "pkSsJustifySelf", "pkSsJustifySelfPriority");
950
+ if (hasOwn2(el.dataset, "pkSsScrollTop")) {
951
+ el.scrollTop = Math.max(0, Math.round(Number(el.dataset.pkSsScrollTop) || 0));
952
+ delete el.dataset.pkSsScrollTop;
953
+ }
954
+ if (hasOwn2(el.dataset, "pkSsScrollLeft")) {
955
+ el.scrollLeft = Math.max(0, Math.round(Number(el.dataset.pkSsScrollLeft) || 0));
956
+ delete el.dataset.pkSsScrollLeft;
957
+ }
958
+ el.removeAttribute(expandedAttr);
907
959
  });
908
- }, EXPANDED_SCROLLABLE_CLASS);
960
+ window.dispatchEvent(new Event("scroll"));
961
+ }, {
962
+ expandedAttr: EXPANDED_ATTR
963
+ }).catch(() => {
964
+ });
909
965
  };
910
- var measureMeaningfulContentBounds = async (page) => {
911
- return await page.evaluate(() => {
966
+ var resetScrollPositionInContext = async (context) => {
967
+ await context.evaluate(() => {
968
+ const root = document.documentElement;
912
969
  const body = document.body;
913
- if (!body) return null;
914
- const viewportWidth = window.innerWidth || document.documentElement?.clientWidth || body.clientWidth || 0;
915
- const viewportHeight = window.innerHeight || document.documentElement?.clientHeight || body.clientHeight || 0;
916
- const scrollX = window.scrollX || window.pageXOffset || 0;
917
- const scrollY = window.scrollY || window.pageYOffset || 0;
918
- const bounds = {
919
- left: Number.POSITIVE_INFINITY,
920
- top: Number.POSITIVE_INFINITY,
921
- right: 0,
922
- bottom: 0,
923
- nodes: 0
924
- };
925
- const isVisible = (el, style, rect) => {
926
- if (!el || !style || !rect) return false;
927
- if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") return false;
928
- if (Number(style.opacity) === 0) return false;
929
- return rect.width > 1 && rect.height > 1;
930
- };
931
- const hasFixedAncestor = (el) => {
932
- for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
933
- const position = String(window.getComputedStyle(node).position || "").toLowerCase();
934
- if (position === "fixed") {
935
- return true;
936
- }
937
- }
938
- return false;
939
- };
940
- const parseRgbColor = (value) => {
941
- const raw = String(value || "").trim();
942
- const match = raw.match(/rgba?\(([^)]+)\)/i);
943
- if (!match) return null;
944
- const parts = match[1].replace(/\//g, " ").split(/[,\s]+/).map((part) => part.trim()).filter(Boolean);
945
- if (parts.length < 3) return null;
946
- const normalizeChannel = (part) => {
947
- if (part.endsWith("%")) {
948
- return Math.max(0, Math.min(255, Number.parseFloat(part) * 2.55));
949
- }
950
- return Math.max(0, Math.min(255, Number.parseFloat(part)));
951
- };
952
- const alpha = parts.length >= 4 ? parts[3].endsWith("%") ? Number.parseFloat(parts[3]) / 100 : Number.parseFloat(parts[3]) : 1;
953
- return {
954
- r: normalizeChannel(parts[0]),
955
- g: normalizeChannel(parts[1]),
956
- b: normalizeChannel(parts[2]),
957
- a: Number.isFinite(alpha) ? Math.max(0, Math.min(1, alpha)) : 1
958
- };
959
- };
960
- const colorLuminance = (color) => {
961
- const channel = (value) => {
962
- const ratio = Math.max(0, Math.min(255, value)) / 255;
963
- return ratio <= 0.03928 ? ratio / 12.92 : ((ratio + 0.055) / 1.055) ** 2.4;
964
- };
965
- return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);
966
- };
967
- const contrastRatio = (a, b) => {
968
- const l1 = colorLuminance(a);
969
- const l2 = colorLuminance(b);
970
- const lighter = Math.max(l1, l2);
971
- const darker = Math.min(l1, l2);
972
- return (lighter + 0.05) / (darker + 0.05);
973
- };
974
- const resolveBackgroundColor = (el) => {
975
- for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
976
- const background = parseRgbColor(window.getComputedStyle(node).backgroundColor);
977
- if (background && background.a > 0.2) {
978
- return background;
979
- }
980
- }
981
- return { r: 255, g: 255, b: 255, a: 1 };
982
- };
983
- const isLowInformationTextPaint = (el, style) => {
984
- const color = parseRgbColor(style?.color);
985
- if (!color) return false;
986
- const opacity = Number(style.opacity);
987
- if (color.a <= 0.08 || Number.isFinite(opacity) && opacity <= 0.18) {
988
- return true;
989
- }
990
- const max = Math.max(color.r, color.g, color.b);
991
- const min = Math.min(color.r, color.g, color.b);
992
- if (!(min >= 224 && max - min <= 24 && color.a >= 0.85)) {
993
- return false;
994
- }
995
- const background = resolveBackgroundColor(el);
996
- return colorLuminance(background) > 0.76 && contrastRatio(color, background) < 1.35;
997
- };
998
- const addRect = (rect) => {
999
- if (!rect || rect.width <= 1 || rect.height <= 1) return;
1000
- const left = Math.max(0, Math.floor(rect.left + scrollX));
1001
- const top = Math.max(0, Math.floor(rect.top + scrollY));
1002
- const right = Math.ceil(rect.right + scrollX);
1003
- const bottom = Math.ceil(rect.bottom + scrollY);
1004
- if (right <= left || bottom <= top) return;
1005
- bounds.left = Math.min(bounds.left, left);
1006
- bounds.top = Math.min(bounds.top, top);
1007
- bounds.right = Math.max(bounds.right, right);
1008
- bounds.bottom = Math.max(bounds.bottom, bottom);
1009
- bounds.nodes += 1;
1010
- };
1011
- const addElement = (el, { minArea = 12, text = false } = {}) => {
1012
- if (!el || el.nodeType !== 1 || hasFixedAncestor(el)) return;
1013
- const style = window.getComputedStyle(el);
1014
- if (text && isLowInformationTextPaint(el, style)) return;
1015
- const rects = Array.from(el.getClientRects ? el.getClientRects() : []);
1016
- rects.forEach((rect) => {
1017
- if (!isVisible(el, style, rect)) return;
1018
- if (rect.width * rect.height < minArea) return;
1019
- addRect(rect);
1020
- });
1021
- };
1022
- const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT);
1023
- let visitedTextNodes = 0;
1024
- while (walker.nextNode() && visitedTextNodes < 6e3) {
1025
- const node = walker.currentNode;
1026
- visitedTextNodes += 1;
1027
- const text = String(node.nodeValue || "").replace(/\s+/g, " ").trim();
1028
- if (text.length < 2) continue;
1029
- addElement(node.parentElement, { minArea: 8, text: true });
1030
- }
1031
- document.querySelectorAll("img,video,canvas,svg,picture").forEach((el) => {
1032
- addElement(el, { minArea: 900 });
970
+ const scrollingElement = document.scrollingElement || root || body;
971
+ window.scrollTo(0, 0);
972
+ [root, body, scrollingElement].forEach((el) => {
973
+ if (!el) return;
974
+ el.scrollTop = 0;
975
+ el.scrollLeft = 0;
976
+ el.dispatchEvent(new Event("scroll", { bubbles: true }));
1033
977
  });
1034
- if (!bounds.nodes || !Number.isFinite(bounds.left)) return null;
1035
- const paddingX = Math.max(16, Math.min(96, viewportWidth * 0.04));
1036
- const paddingY = Math.max(24, Math.min(160, viewportHeight * 0.18));
1037
- return {
1038
- left: Math.max(0, Math.floor(bounds.left - paddingX)),
1039
- top: Math.max(0, Math.floor(bounds.top - paddingY)),
1040
- right: Math.ceil(bounds.right + paddingX),
1041
- bottom: Math.ceil(bounds.bottom + paddingY),
1042
- width: Math.max(1, Math.ceil(bounds.right - bounds.left)),
1043
- height: Math.max(1, Math.ceil(bounds.bottom - bounds.top)),
1044
- nodes: bounds.nodes,
1045
- viewport: {
1046
- width: Math.max(1, Math.ceil(viewportWidth || 1)),
1047
- height: Math.max(1, Math.ceil(viewportHeight || 1))
1048
- }
1049
- };
1050
- }).catch((error) => {
1051
- logger.warning(`\u622A\u56FE\u5185\u5BB9\u8FB9\u754C\u6D4B\u91CF\u5931\u8D25: ${error?.message || error}`);
1052
- return null;
978
+ window.dispatchEvent(new Event("scroll"));
979
+ }).catch(() => {
1053
980
  });
1054
981
  };
1055
982
  var prepareExpandedFullPageScreenshot = async (page, options = {}) => {
1056
- const originalViewport = await resolveCurrentViewportSize(page);
1057
983
  const maxHeight = toPositiveInteger(options.maxHeight, DEFAULT_MAX_HEIGHT);
1058
- const device = await resolvePageDevice(page);
1059
- const preserveViewport = options.preserveViewport ?? device === Device.Mobile;
1060
- const defaultSettleMs = preserveViewport ? DEFAULT_MOBILE_SETTLE_MS : DEFAULT_SETTLE_MS;
1061
- const requestedSettleMs = Math.max(0, Number(options.settleMs ?? defaultSettleMs) || 0);
1062
- const settleMs = preserveViewport ? Math.min(requestedSettleMs, DEFAULT_MOBILE_SETTLE_MS) : requestedSettleMs;
1063
- const maxScrollHeight = await expandScrollableContent(page, {
1064
- forceScrollableHeight: options.forceScrollableHeight,
1065
- visibleOnly: options.visibleOnly !== false,
1066
- expandDocumentElements: preserveViewport
1067
- });
1068
- const contentBounds = await measureMeaningfulContentBounds(page);
1069
- let targetHeight = Math.min(maxScrollHeight, maxHeight);
1070
- if (contentBounds?.bottom > 0) {
1071
- const contentBottom = Math.min(Math.ceil(contentBounds.bottom), maxHeight);
1072
- const trailingGap = targetHeight - contentBottom;
1073
- const minTrailingGap = Math.max(
1074
- MIN_TRAILING_BLANK_GAP_PX,
1075
- Math.floor(targetHeight * MIN_TRAILING_BLANK_GAP_RATIO),
1076
- Math.floor(originalViewport.height * 0.45)
1077
- );
1078
- if (trailingGap >= minTrailingGap && contentBottom >= originalViewport.height * 0.65) {
1079
- targetHeight = Math.max(1, contentBottom);
1080
- logger.info(`\u957F\u622A\u56FE\u88C1\u6389\u4F4E\u4FE1\u606F\u5C3E\u90E8: contentBottom=${contentBottom}, originalHeight=${Math.min(maxScrollHeight, maxHeight)}`);
1081
- }
1082
- }
1083
- let viewportResized = false;
1084
- if (!preserveViewport) {
1085
- await page.setViewportSize({
1086
- width: originalViewport.width,
1087
- height: targetHeight
984
+ const settleMs = Math.max(0, Number(options.settleMs ?? DEFAULT_SETTLE_MS) || 0);
985
+ const frames = typeof page.frames === "function" ? page.frames() : [page.mainFrame?.()].filter(Boolean);
986
+ const mainFrame = page.mainFrame?.() || frames[0] || null;
987
+ let targetHeight = 0;
988
+ let scrollableCount = 0;
989
+ let expandedCount = 0;
990
+ for (const frame of frames) {
991
+ if (!frame || frame === mainFrame) continue;
992
+ const state2 = await expandScrollableContentInContext(frame, {
993
+ ...options,
994
+ maxHeight,
995
+ settleMs
1088
996
  });
1089
- viewportResized = true;
1090
- } else {
1091
- await adjustAffixedElementsForExpandedScreenshot(page, { targetHeight }).catch((error) => {
1092
- logger.warning(`\u79FB\u52A8\u7AEF\u5438\u9644\u5143\u7D20\u91CD\u5B9A\u4F4D\u5931\u8D25: ${error?.message || error}`);
997
+ if (state2.height > 0) {
998
+ await expandFrameElement(frame, Math.min(state2.height, maxHeight)).catch(() => false);
999
+ await resetScrollPositionInContext(frame);
1000
+ }
1001
+ targetHeight = Math.max(targetHeight, state2.height || 0);
1002
+ scrollableCount += state2.scrollableCount || 0;
1003
+ expandedCount += state2.expandedCount || 0;
1004
+ }
1005
+ if (mainFrame) {
1006
+ const state2 = await expandScrollableContentInContext(mainFrame, {
1007
+ ...options,
1008
+ maxHeight,
1009
+ settleMs
1093
1010
  });
1094
- }
1095
- if (settleMs > 0) {
1096
- await delay(settleMs);
1097
- }
1011
+ targetHeight = Math.max(targetHeight, state2.height || 0);
1012
+ scrollableCount += state2.scrollableCount || 0;
1013
+ expandedCount += state2.expandedCount || 0;
1014
+ await resetScrollPositionInContext(mainFrame);
1015
+ }
1016
+ targetHeight = Math.min(Math.max(1, Math.ceil(targetHeight || 0)), maxHeight);
1017
+ if (settleMs > 0) await delay(settleMs);
1018
+ logger.info(`\u5C55\u5F00\u5F0F\u957F\u622A\u56FE\u76EE\u6807: height=${targetHeight}, scrollables=${scrollableCount}, expanded=${expandedCount}`);
1098
1019
  return {
1099
- originalViewport,
1100
- maxScrollHeight,
1101
1020
  targetHeight,
1102
- contentBounds,
1103
- preserveViewport,
1104
- viewportResized
1021
+ frames,
1022
+ settleMs
1105
1023
  };
1106
1024
  };
1107
1025
  var restoreExpandedFullPageScreenshot = async (page, state2 = {}) => {
1108
- await page.evaluate((className) => {
1109
- const targets = new Set([
1110
- ...document.querySelectorAll(`.${className}`),
1111
- document.documentElement,
1112
- document.body,
1113
- document.scrollingElement
1114
- ].filter((el) => el?.classList?.contains(className)));
1115
- targets.forEach((el) => {
1116
- el.style.setProperty("overflow", el.dataset.pkOrigOverflow || "", el.dataset.pkOrigOverflowPriority || "");
1117
- el.style.setProperty("height", el.dataset.pkOrigHeight || "", el.dataset.pkOrigHeightPriority || "");
1118
- el.style.setProperty("min-height", el.dataset.pkOrigMinHeight || "", el.dataset.pkOrigMinHeightPriority || "");
1119
- el.style.setProperty("max-height", el.dataset.pkOrigMaxHeight || "", el.dataset.pkOrigMaxHeightPriority || "");
1120
- delete el.dataset.pkOrigOverflow;
1121
- delete el.dataset.pkOrigOverflowPriority;
1122
- delete el.dataset.pkOrigHeight;
1123
- delete el.dataset.pkOrigHeightPriority;
1124
- delete el.dataset.pkOrigMinHeight;
1125
- delete el.dataset.pkOrigMinHeightPriority;
1126
- delete el.dataset.pkOrigMaxHeight;
1127
- delete el.dataset.pkOrigMaxHeightPriority;
1128
- el.classList.remove(className);
1129
- });
1130
- }, EXPANDED_SCROLLABLE_CLASS);
1131
- if (state2?.originalViewport && state2?.viewportResized) {
1132
- await page.setViewportSize(state2.originalViewport);
1026
+ const frames = Array.isArray(state2.frames) ? [...state2.frames].reverse() : typeof page.frames === "function" ? [...page.frames()].reverse() : [];
1027
+ for (const frame of frames) {
1028
+ await restoreExpandedStateInContext(frame);
1133
1029
  }
1134
1030
  };
1135
- var resolveVirtualizedScrollTarget = async (page, options = {}) => {
1136
- const stitchMode = resolveStitchMode(options);
1137
- if (stitchMode === "off") return null;
1138
- const device = await resolvePageDevice(page);
1139
- if (stitchMode !== "force" && device !== Device.Mobile) {
1140
- return null;
1141
- }
1142
- return await page.evaluate((config) => {
1143
- const viewportWidth = window.innerWidth || document.documentElement?.clientWidth || document.body?.clientWidth || 0;
1144
- const viewportHeight = window.innerHeight || document.documentElement?.clientHeight || document.body?.clientHeight || 0;
1145
- const root = document.documentElement;
1146
- const body = document.body;
1147
- const scrollingElement = document.scrollingElement;
1148
- const candidates = [];
1149
- const seen = /* @__PURE__ */ new Set();
1150
- const scrollableOverflow = /* @__PURE__ */ new Set(["auto", "scroll", "overlay"]);
1151
- const clippingOverflow = /* @__PURE__ */ new Set(["hidden", "clip"]);
1152
- const pushCandidate = (el2) => {
1153
- if (!el2 || seen.has(el2)) return;
1154
- seen.add(el2);
1155
- candidates.push(el2);
1156
- };
1157
- pushCandidate(scrollingElement);
1158
- pushCandidate(root);
1159
- pushCandidate(body);
1160
- document.querySelectorAll("*").forEach(pushCandidate);
1161
- const isDocumentElement = (el2) => el2 === root || el2 === body || el2 === scrollingElement;
1162
- const isVisible = (el2, style, rect) => {
1163
- if (!el2 || !style || !rect) return false;
1164
- if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") return false;
1165
- if (Number(style.opacity) === 0) return false;
1166
- return rect.width > 0 && rect.height > 0;
1167
- };
1168
- const hasOverflowValue = (style, values) => {
1169
- const overflowY = String(style?.overflowY || "").toLowerCase();
1170
- const overflow = String(style?.overflow || "").toLowerCase();
1171
- return values.has(overflowY) || values.has(overflow);
1172
- };
1173
- const looksScrollable = (el2, style) => {
1174
- if (!el2 || el2.scrollHeight <= el2.clientHeight + 1) return false;
1175
- if (isDocumentElement(el2)) return true;
1176
- return hasOverflowValue(style, scrollableOverflow) || hasOverflowValue(style, clippingOverflow);
1177
- };
1178
- const textHeightFor = (el2) => {
1179
- const nodes = isDocumentElement(el2) ? document.querySelectorAll("body *") : el2.querySelectorAll("*");
1180
- let textHeight = 0;
1181
- let textNodes = 0;
1182
- const maxNodes = 3500;
1183
- for (const node of nodes) {
1184
- if (textNodes >= maxNodes) break;
1185
- const text = String(node.textContent || "").replace(/\s+/g, " ").trim();
1186
- if (text.length < 2) continue;
1187
- let childHasText = false;
1188
- for (const child of node.children || []) {
1189
- if (String(child.textContent || "").replace(/\s+/g, " ").trim().length >= 2) {
1190
- childHasText = true;
1191
- break;
1192
- }
1193
- }
1194
- if (childHasText) continue;
1195
- const style = window.getComputedStyle(node);
1196
- const rect = node.getBoundingClientRect();
1197
- if (!isVisible(node, style, rect)) continue;
1198
- textNodes += 1;
1199
- textHeight += Math.min(Math.ceil(rect.height || 0), 900);
1200
- }
1201
- return { textHeight, textNodes };
1202
- };
1203
- const resolveEdgeOcclusion = () => {
1204
- let top = 0;
1205
- let bottom = 0;
1206
- const maxHeight = Math.max(48, viewportHeight * 0.45);
1207
- document.querySelectorAll("*").forEach((el2) => {
1208
- const style = window.getComputedStyle(el2);
1209
- const position = String(style.position || "").toLowerCase();
1210
- if (position !== "fixed" && position !== "sticky") return;
1211
- const rect = el2.getBoundingClientRect();
1212
- if (!isVisible(el2, style, rect)) return;
1213
- if (rect.height <= 0 || rect.height > maxHeight) return;
1214
- if (rect.width < viewportWidth * 0.35) return;
1215
- if (rect.top <= Math.max(16, viewportHeight * 0.08)) {
1216
- top = Math.max(top, Math.ceil(rect.bottom));
1217
- }
1218
- if (rect.bottom >= viewportHeight - Math.max(16, viewportHeight * 0.08)) {
1219
- bottom = Math.max(bottom, Math.ceil(viewportHeight - rect.top));
1220
- }
1221
- });
1222
- return {
1223
- top: Math.max(0, Math.min(Math.ceil(top), Math.floor(viewportHeight * 0.45))),
1224
- bottom: Math.max(0, Math.min(Math.ceil(bottom), Math.floor(viewportHeight * 0.45)))
1225
- };
1226
- };
1227
- let best = null;
1228
- candidates.forEach((el2) => {
1229
- const style = window.getComputedStyle(el2);
1230
- const rect = isDocumentElement(el2) ? {
1231
- top: 0,
1232
- bottom: viewportHeight,
1233
- left: 0,
1234
- right: viewportWidth,
1235
- width: viewportWidth,
1236
- height: viewportHeight
1237
- } : el2.getBoundingClientRect();
1238
- if (!isDocumentElement(el2) && !isVisible(el2, style, rect)) return;
1239
- if (!looksScrollable(el2, style)) return;
1240
- const scrollHeight = Math.ceil(el2.scrollHeight || 0);
1241
- const clientHeight = Math.ceil(el2.clientHeight || viewportHeight || 0);
1242
- if (scrollHeight < Math.max(900, clientHeight * config.minScrollRatio)) return;
1243
- const visibleTop = Math.max(0, Math.round(rect.top || 0));
1244
- const visibleBottom = Math.min(viewportHeight, Math.round(rect.bottom || viewportHeight));
1245
- const visibleHeight = Math.max(0, visibleBottom - visibleTop);
1246
- const minVisibleHeight = Math.max(
1247
- config.minVisibleHeightPx,
1248
- Math.floor(viewportHeight * config.minVisibleHeightRatio)
1249
- );
1250
- if (!config.force && !isDocumentElement(el2) && visibleHeight < minVisibleHeight) return;
1251
- const { textHeight, textNodes } = textHeightFor(el2);
1252
- const sparseRatio = scrollHeight / Math.max(1, textHeight);
1253
- const sparse = config.force || textNodes > 0 && sparseRatio >= config.minSparseRatio && scrollHeight - textHeight > clientHeight;
1254
- if (!sparse) return;
1255
- const visibleWidth = Math.max(1, Math.ceil(rect.width || viewportWidth || 1));
1256
- const score2 = scrollHeight * Math.min(visibleWidth, viewportWidth || visibleWidth) * Math.min(1, visibleHeight / Math.max(1, viewportHeight));
1257
- if (!best || score2 > best.score) {
1258
- best = {
1259
- el: el2,
1260
- score: score2,
1261
- kind: isDocumentElement(el2) ? "document" : "element",
1262
- scrollHeight,
1263
- clientHeight,
1264
- scrollTop: Math.max(0, Math.round(el2.scrollTop || 0)),
1265
- rect: {
1266
- top: Math.max(0, Math.round(rect.top || 0)),
1267
- bottom: Math.min(viewportHeight, Math.round(rect.bottom || viewportHeight)),
1268
- left: Math.max(0, Math.round(rect.left || 0)),
1269
- width: Math.max(1, Math.round(rect.width || viewportWidth || 1)),
1270
- height: Math.max(1, Math.round(rect.height || viewportHeight || 1))
1271
- },
1272
- visibleHeight,
1273
- textHeight,
1274
- textNodes,
1275
- sparseRatio
1276
- };
1277
- }
1278
- });
1279
- if (!best) return null;
1280
- document.querySelectorAll(`[${config.attrName}="1"]`).forEach((node) => {
1281
- node.removeAttribute(config.attrName);
1282
- });
1283
- if (best.kind === "element") {
1284
- best.el.setAttribute(config.attrName, "1");
1285
- }
1286
- const { el, score, ...target } = best;
1287
- return {
1288
- ...target,
1289
- viewport: {
1290
- width: Math.max(1, Math.ceil(viewportWidth || best.rect.width || 1)),
1291
- height: Math.max(1, Math.ceil(viewportHeight || best.rect.height || 1))
1292
- },
1293
- edgeOcclusion: resolveEdgeOcclusion()
1294
- };
1295
- }, {
1296
- attrName: STITCH_SCROLL_TARGET_ATTR,
1297
- force: stitchMode === "force",
1298
- minScrollRatio: MIN_VIRTUALIZED_SCROLL_RATIO,
1299
- minSparseRatio: MIN_SPARSE_SCROLL_RATIO,
1300
- minVisibleHeightPx: MIN_STITCH_VISIBLE_HEIGHT_PX,
1301
- minVisibleHeightRatio: MIN_STITCH_VISIBLE_HEIGHT_RATIO
1302
- }).catch((error) => {
1303
- logger.warning(`\u865A\u62DF\u6EDA\u52A8\u622A\u56FE\u63A2\u6D4B\u5931\u8D25: ${error?.message || error}`);
1304
- return null;
1305
- });
1306
- };
1307
- var setStitchScrollTop = async (page, target, scrollTop) => {
1308
- await page.evaluate(({ attrName, kind, top }) => {
1309
- const el = kind === "document" ? document.scrollingElement || document.documentElement || document.body : document.querySelector(`[${attrName}="1"]`);
1310
- if (!el) return;
1311
- el.scrollTop = Math.max(0, Math.round(Number(top) || 0));
1312
- el.dispatchEvent(new Event("scroll", { bubbles: true }));
1313
- window.dispatchEvent(new Event("scroll"));
1314
- }, {
1315
- attrName: STITCH_SCROLL_TARGET_ATTR,
1316
- kind: target.kind,
1317
- top: scrollTop
1318
- });
1319
- };
1320
- var cleanupStitchTarget = async (page) => {
1321
- await page.evaluate((attrName) => {
1322
- document.querySelectorAll(`[${attrName}="1"]`).forEach((node) => {
1323
- node.removeAttribute(attrName);
1324
- });
1325
- }, STITCH_SCROLL_TARGET_ATTR).catch(() => {
1326
- });
1327
- };
1328
- var cropImage = (image, crop) => image.clone().crop({
1329
- x: Math.max(0, Math.round(crop.x || 0)),
1330
- y: Math.max(0, Math.round(crop.y || 0)),
1331
- w: Math.max(1, Math.round(crop.w || 1)),
1332
- h: Math.max(1, Math.round(crop.h || 1))
1333
- });
1334
- var captureStitchedScrollableScreenshot = async (page, target, options = {}) => {
1335
- const maxHeight = toPositiveInteger(options.maxHeight, DEFAULT_MAX_HEIGHT);
1336
- const settleMs = Math.max(0, Number(options.stitchSettleMs ?? DEFAULT_STITCH_SETTLE_MS) || 0);
1337
- const overlapPx = Math.max(0, Math.round(Number(options.stitchOverlapPx ?? DEFAULT_STITCH_OVERLAP_PX) || 0));
1338
- const viewport = target.viewport || await resolveCurrentViewportSize(page);
1339
- const rect = target.rect || {
1340
- top: 0,
1341
- bottom: viewport.height,
1342
- left: 0,
1343
- width: viewport.width,
1344
- height: viewport.height
1345
- };
1346
- const edge = target.edgeOcclusion || { top: 0, bottom: 0 };
1347
- const topCrop = Math.max(0, Math.min(rect.top, viewport.height - 1));
1348
- const topOverlay = Math.max(topCrop, Math.min(edge.top || 0, viewport.height - 1));
1349
- const bottomOverlay = Math.max(0, Math.min(edge.bottom || 0, viewport.height - topOverlay - 1));
1350
- const middleCropY = Math.max(topCrop, topOverlay);
1351
- const middleCropBottom = Math.max(
1352
- middleCropY + 1,
1353
- Math.min(rect.bottom || viewport.height, viewport.height - bottomOverlay)
1354
- );
1355
- const middleCropHeight = Math.max(1, middleCropBottom - middleCropY);
1356
- const scrollStep = Math.max(120, middleCropHeight - overlapPx);
1357
- const maxScrollTop = Math.max(0, Math.ceil(target.scrollHeight - target.clientHeight));
1358
- const positions = [];
1359
- for (let top = 0; top < maxScrollTop; top += scrollStep) {
1360
- positions.push(Math.round(top));
1361
- }
1362
- if (!positions.includes(maxScrollTop)) {
1363
- positions.push(maxScrollTop);
1364
- }
1365
- if (positions.length === 0) {
1366
- positions.push(0);
1367
- }
1368
- const segments = [];
1369
- let totalHeight = 0;
1370
- let outputWidth = Math.max(1, Math.round(viewport.width || rect.width || 1));
1031
+ var captureExpandedFullPageScreenshotOnce = async (page, options = {}) => {
1032
+ const originalViewport = await resolveCurrentViewportSize(page).catch(() => null);
1033
+ const originalMainScroll = await readMainFrameScroll(page);
1034
+ let state2 = null;
1035
+ let viewportResized = false;
1371
1036
  try {
1372
- for (let index = 0; index < positions.length && totalHeight < maxHeight; index += 1) {
1373
- const isFirst = index === 0;
1374
- const isLast = index === positions.length - 1;
1375
- await setStitchScrollTop(page, target, positions[index]);
1376
- if (settleMs > 0) {
1377
- await delay(settleMs);
1378
- }
1379
- const buffer = await capturePageScreenshot(page, {
1380
- type: options.type || "png",
1381
- quality: options.quality,
1382
- timeout: options.timeout
1037
+ await freezeViewportMetrics(page);
1038
+ state2 = await prepareExpandedFullPageScreenshot(page, options);
1039
+ const targetHeight = Math.max(1, Math.ceil(state2.targetHeight || options.maxHeight || DEFAULT_MAX_HEIGHT));
1040
+ if (originalViewport?.width && originalViewport?.height && targetHeight > Math.ceil(originalViewport.height) + 1) {
1041
+ await page.setViewportSize({
1042
+ width: Math.ceil(originalViewport.width),
1043
+ height: targetHeight
1383
1044
  });
1384
- const image = await Jimp.read(buffer);
1385
- outputWidth = Math.min(outputWidth, image.bitmap.width);
1386
- const cropY = isFirst ? 0 : middleCropY;
1387
- const cropBottom = isLast ? image.bitmap.height : middleCropBottom;
1388
- let cropHeight = Math.max(1, Math.min(image.bitmap.height, cropBottom) - cropY);
1389
- const remaining = maxHeight - totalHeight;
1390
- if (cropHeight > remaining) {
1391
- cropHeight = remaining;
1392
- }
1393
- segments.push(cropImage(image, {
1394
- x: 0,
1395
- y: cropY,
1396
- w: outputWidth,
1397
- h: cropHeight
1398
- }));
1399
- totalHeight += cropHeight;
1045
+ viewportResized = true;
1046
+ if (state2.settleMs > 0) await delay(state2.settleMs);
1400
1047
  }
1401
- const canvas = new Jimp({
1402
- width: outputWidth,
1403
- height: Math.max(1, totalHeight),
1404
- color: 4294967295
1048
+ return await capturePageScreenshot(page, {
1049
+ ...options,
1050
+ fullPage: true,
1051
+ maxClipHeight: targetHeight
1405
1052
  });
1406
- let y = 0;
1407
- for (const segment of segments) {
1408
- canvas.composite(segment, 0, y);
1409
- y += segment.bitmap.height;
1053
+ } finally {
1054
+ if (options.restore) {
1055
+ await restoreExpandedFullPageScreenshot(page, state2);
1056
+ if (viewportResized && originalViewport?.width && originalViewport?.height) {
1057
+ await page.setViewportSize({
1058
+ width: Math.ceil(originalViewport.width),
1059
+ height: Math.ceil(originalViewport.height)
1060
+ }).catch((error) => {
1061
+ logger.warning(`\u6062\u590D viewport \u5931\u8D25: ${error?.message || error}`);
1062
+ });
1063
+ }
1410
1064
  }
1411
- logger.info(
1412
- `\u865A\u62DF\u6EDA\u52A8\u5206\u6BB5\u622A\u56FE: segments=${segments.length}, height=${totalHeight}, scrollHeight=${target.scrollHeight}, textNodes=${target.textNodes}, sparseRatio=${Number(target.sparseRatio || 0).toFixed(2)}`
1413
- );
1414
- const expectedHeight = Math.min(maxHeight, Math.max(target.scrollHeight || 0, viewport.height || 0));
1415
- const minPlausibleHeight = Math.max(
1416
- Math.floor((viewport.height || rect.height || 0) * MIN_STITCH_OUTPUT_VIEWPORT_RATIO),
1417
- Math.floor(expectedHeight * MIN_STITCH_OUTPUT_HEIGHT_RATIO)
1418
- );
1419
- if (positions.length > 1 && expectedHeight > (viewport.height || rect.height || 0) * 1.3 && totalHeight < minPlausibleHeight) {
1420
- logger.warning(
1421
- `\u865A\u62DF\u6EDA\u52A8\u5206\u6BB5\u622A\u56FE\u7ED3\u679C\u8FC7\u77ED\uFF0C\u964D\u7EA7\u666E\u901A\u957F\u622A\u56FE: segments=${segments.length}, height=${totalHeight}, expectedMin=${minPlausibleHeight}, scrollHeight=${target.scrollHeight}, rect=${rect.width}x${rect.height}@${rect.top}`
1422
- );
1423
- return null;
1065
+ await restoreViewportMetrics(page);
1066
+ if (options.restore) {
1067
+ await restoreMainFrameScroll(page, originalMainScroll);
1424
1068
  }
1425
- return await canvas.getBuffer(JimpMime.png);
1426
- } finally {
1427
- await setStitchScrollTop(page, target, target.scrollTop || 0).catch(() => {
1428
- });
1429
- await cleanupStitchTarget(page);
1430
1069
  }
1431
1070
  };
1432
- var isLightBlankPixel = ({ r, g, b }) => {
1433
- const max = Math.max(r, g, b);
1434
- const min = Math.min(r, g, b);
1435
- return max >= 238 && max - min <= 18;
1436
- };
1437
1071
  var isDarkBlankPixel = ({ r, g, b }) => {
1438
1072
  const max = Math.max(r, g, b);
1439
1073
  const min = Math.min(r, g, b);
1440
1074
  return max <= 34 && max - min <= 10;
1441
1075
  };
1442
- var isLowInfoPixel = ({ r, g, b }) => {
1443
- const max = Math.max(r, g, b);
1444
- const min = Math.min(r, g, b);
1445
- return max - min <= 12;
1446
- };
1447
1076
  var analyzeScreenshotBuffer = async (buffer) => {
1448
1077
  const image = await Jimp.read(buffer);
1449
- const width = image.bitmap.width;
1450
- const height = image.bitmap.height;
1078
+ const { width, height } = image.bitmap;
1451
1079
  const stepY = Math.max(1, Math.floor(height / 900));
1452
1080
  const stepX = Math.max(1, Math.floor(width / 420));
1453
- const rows = [];
1454
- let lightRows = 0;
1455
1081
  let darkRows = 0;
1456
- let lowInfoRows = 0;
1457
1082
  let loadingLikeRows = 0;
1458
- const rowFlags = [];
1083
+ let rows = 0;
1459
1084
  for (let y = 0; y < height; y += stepY) {
1460
- let light = 0;
1085
+ rows += 1;
1461
1086
  let dark = 0;
1462
- let lowInfo = 0;
1463
1087
  let edge = 0;
1464
1088
  let count = 0;
1465
1089
  let prev = null;
1466
1090
  for (let x = 0; x < width; x += stepX) {
1467
1091
  const rgba = intToRGBA(image.getPixelColor(x, y));
1468
1092
  count += 1;
1469
- if (isLightBlankPixel(rgba)) light += 1;
1470
1093
  if (isDarkBlankPixel(rgba)) dark += 1;
1471
- if (isLowInfoPixel(rgba)) lowInfo += 1;
1472
1094
  if (prev) {
1473
1095
  const diff = Math.abs(rgba.r - prev.r) + Math.abs(rgba.g - prev.g) + Math.abs(rgba.b - prev.b);
1474
1096
  if (diff > 45) edge += 1;
1475
1097
  }
1476
1098
  prev = rgba;
1477
1099
  }
1478
- const stat = {
1479
- y,
1480
- lightRatio: light / Math.max(1, count),
1481
- darkRatio: dark / Math.max(1, count),
1482
- lowInfoRatio: lowInfo / Math.max(1, count),
1483
- edgeRatio: edge / Math.max(1, count - 1)
1484
- };
1485
- rows.push(stat);
1486
- const lightBlank = stat.lightRatio >= 0.965 && stat.edgeRatio <= 0.015;
1487
- const darkBlank = stat.darkRatio >= 0.965 && stat.edgeRatio <= 0.012;
1488
- const lowInfoBlank = stat.lowInfoRatio >= 0.965 && stat.edgeRatio <= 0.012;
1489
- const loadingLike = stat.darkRatio >= 0.88 && stat.edgeRatio <= 0.025;
1490
- if (lightBlank) lightRows += 1;
1491
- if (darkBlank) darkRows += 1;
1492
- if (lowInfoBlank) lowInfoRows += 1;
1493
- if (loadingLike) loadingLikeRows += 1;
1494
- rowFlags.push({ lightBlank, darkBlank, lowInfoBlank, loadingLike });
1495
- }
1496
- const sampledRows = Math.max(1, rows.length);
1100
+ const darkRatio = dark / Math.max(1, count);
1101
+ const edgeRatio = edge / Math.max(1, count - 1);
1102
+ if (darkRatio >= 0.965 && edgeRatio <= 0.012) darkRows += 1;
1103
+ if (darkRatio >= 0.88 && edgeRatio <= 0.025) loadingLikeRows += 1;
1104
+ }
1105
+ const sampledRows = Math.max(1, rows);
1497
1106
  return {
1498
- image,
1499
1107
  width,
1500
1108
  height,
1501
- lightBlankRowsRatio: lightRows / sampledRows,
1502
1109
  darkBlankRowsRatio: darkRows / sampledRows,
1503
- lowInfoRowsRatio: lowInfoRows / sampledRows,
1504
- loadingLikeRowsRatio: loadingLikeRows / sampledRows,
1505
- rowFlags
1110
+ loadingLikeRowsRatio: loadingLikeRows / sampledRows
1506
1111
  };
1507
1112
  };
1508
1113
  var resolveScreenshotQualityIssue = (analysis) => {
@@ -1512,109 +1117,6 @@ var resolveScreenshotQualityIssue = (analysis) => {
1512
1117
  }
1513
1118
  return null;
1514
1119
  };
1515
- var cropBufferToContentBounds = async (buffer, bounds, options = {}) => {
1516
- if (!bounds || bounds.nodes <= 0) return buffer;
1517
- const image = options.image || await Jimp.read(buffer);
1518
- const width = image.bitmap.width;
1519
- const height = image.bitmap.height;
1520
- const safeLeft = Math.max(0, Math.min(width - 1, Math.floor(bounds.left || 0)));
1521
- const safeRight = Math.max(safeLeft + 1, Math.min(width, Math.ceil(bounds.right || width)));
1522
- const safeTop = Math.max(0, Math.min(height - 1, Math.floor(bounds.top || 0)));
1523
- const safeBottom = Math.max(safeTop + 1, Math.min(height, Math.ceil(bounds.bottom || height)));
1524
- const cropW = safeRight - safeLeft;
1525
- const cropH = safeBottom - safeTop;
1526
- const shouldCropX = cropW > 80 && cropW <= width * 0.82 && (safeLeft > width * 0.04 || width - safeRight > width * 0.04);
1527
- const shouldCropY = cropH > 160 && cropH <= height * 0.88 && height - safeBottom > Math.max(320, height * 0.1);
1528
- if (!shouldCropX && !shouldCropY) {
1529
- return buffer;
1530
- }
1531
- const x = shouldCropX ? safeLeft : 0;
1532
- const y = shouldCropY ? safeTop : 0;
1533
- const w = shouldCropX ? cropW : width;
1534
- const h = shouldCropY ? cropH : height;
1535
- logger.info(`\u5185\u5BB9\u611F\u77E5\u88C1\u526A\u622A\u56FE: x=${x}, y=${y}, w=${w}, h=${h}, original=${width}x${height}`);
1536
- return await cropImage(image, { x, y, w, h }).getBuffer(JimpMime.png);
1537
- };
1538
- var captureExpandedFullPageScreenshotOnce = async (page, options = {}) => {
1539
- const stitchedTarget = await resolveVirtualizedScrollTarget(page, options);
1540
- if (stitchedTarget) {
1541
- const buffer = await captureStitchedScrollableScreenshot(page, stitchedTarget, options);
1542
- if (buffer) {
1543
- return buffer;
1544
- }
1545
- }
1546
- const state2 = await prepareExpandedFullPageScreenshot(page, options);
1547
- try {
1548
- const buffer = await capturePageScreenshot(page, {
1549
- fullPage: true,
1550
- type: options.type || "png",
1551
- quality: options.quality,
1552
- timeout: options.timeout,
1553
- maxClipHeight: state2.targetHeight
1554
- });
1555
- return await cropBufferToContentBounds(buffer, state2.contentBounds);
1556
- } finally {
1557
- await restoreAffixedElementsForExpandedScreenshot(page).catch((error) => {
1558
- logger.warning(`\u79FB\u52A8\u7AEF\u5438\u9644\u5143\u7D20\u6062\u590D\u5931\u8D25: ${error?.message || error}`);
1559
- });
1560
- if (options.restore) {
1561
- await restoreExpandedFullPageScreenshot(page, state2);
1562
- }
1563
- }
1564
- };
1565
- var capturePageScreenshot = async (page, options = {}) => {
1566
- const fullPage = Boolean(options.fullPage);
1567
- const type = fullPage ? FORCED_FULLPAGE_TYPE : normalizeType(options.type);
1568
- const quality = fullPage ? FORCED_FULLPAGE_QUALITY : normalizeQuality(options.quality, type);
1569
- const timeout = toPositiveNumber(options.timeout, DEFAULT_TIMEOUT_MS);
1570
- const maxClipHeight = Math.round(toPositiveNumber(options.maxClipHeight, 0));
1571
- const fallbackOptions = {
1572
- type: type === "webp" ? "png" : type,
1573
- fullPage
1574
- };
1575
- if (quality !== void 0 && fallbackOptions.type === "jpeg") {
1576
- fallbackOptions.quality = quality;
1577
- }
1578
- if (timeout > 0) {
1579
- fallbackOptions.timeout = timeout;
1580
- }
1581
- try {
1582
- const context = page && typeof page.context === "function" ? page.context() : null;
1583
- if (!context || typeof context.newCDPSession !== "function") {
1584
- throw new Error("CDP session is not available");
1585
- }
1586
- const session = await context.newCDPSession(page);
1587
- try {
1588
- const metrics = await session.send("Page.getLayoutMetrics");
1589
- const viewport = await resolveCurrentViewportSize(page);
1590
- const captureParams = {
1591
- format: type,
1592
- fromSurface: true,
1593
- captureBeyondViewport: fullPage,
1594
- optimizeForSpeed: true
1595
- };
1596
- if (quality !== void 0) {
1597
- captureParams.quality = quality;
1598
- }
1599
- if (fullPage) {
1600
- captureParams.clip = buildFullPageClip(metrics, viewport, maxClipHeight);
1601
- }
1602
- const result = await session.send("Page.captureScreenshot", captureParams);
1603
- if (!result || typeof result.data !== "string" || !result.data) {
1604
- throw new Error("CDP returned empty screenshot data");
1605
- }
1606
- return Buffer.from(result.data, "base64");
1607
- } finally {
1608
- if (typeof session.detach === "function") {
1609
- await session.detach().catch(() => {
1610
- });
1611
- }
1612
- }
1613
- } catch (error) {
1614
- logger.warning(`CDP \u622A\u56FE\u5931\u8D25\uFF0C\u56DE\u9000 page.screenshot: ${error?.message || error}`);
1615
- return await page.screenshot(fallbackOptions);
1616
- }
1617
- };
1618
1120
  var captureExpandedFullPageScreenshot = async (page, options = {}) => {
1619
1121
  const attempts = Math.max(1, toPositiveInteger(options.qualityRetryAttempts, DEFAULT_QUALITY_RETRY_ATTEMPTS));
1620
1122
  const retryDelayMs = Math.max(0, Number(options.qualityRetryDelayMs ?? DEFAULT_QUALITY_RETRY_DELAY_MS) || 0);
@@ -1631,9 +1133,7 @@ var captureExpandedFullPageScreenshot = async (page, options = {}) => {
1631
1133
  lastBuffer = buffer;
1632
1134
  lastAnalysis = analysis;
1633
1135
  lastIssue = issue;
1634
- if (!issue) {
1635
- return buffer;
1636
- }
1136
+ if (!issue) return buffer;
1637
1137
  if (attempt < attempts && retryDelayMs > 0) {
1638
1138
  logger.warning(`\u622A\u56FE\u7591\u4F3C\u5F02\u5E38(${issue})\uFF0C\u7B49\u5F85\u540E\u91CD\u8BD5: attempt=${attempt}/${attempts}`);
1639
1139
  await delay(retryDelayMs);
@@ -1641,7 +1141,7 @@ var captureExpandedFullPageScreenshot = async (page, options = {}) => {
1641
1141
  }
1642
1142
  if (lastIssue && lastAnalysis) {
1643
1143
  logger.warning(
1644
- `\u622A\u56FE\u8D28\u91CF\u4ECD\u5F02\u5E38(${lastIssue})\uFF0C\u8FD4\u56DE\u6700\u4F73\u53EF\u5F97\u7ED3\u679C: light=${lastAnalysis.lightBlankRowsRatio.toFixed(2)}, dark=${lastAnalysis.darkBlankRowsRatio.toFixed(2)}, loading=${lastAnalysis.loadingLikeRowsRatio.toFixed(2)}`
1144
+ `\u622A\u56FE\u8D28\u91CF\u4ECD\u5F02\u5E38(${lastIssue})\uFF0C\u8FD4\u56DE\u6700\u4F73\u53EF\u5F97\u7ED3\u679C: dark=${lastAnalysis.darkBlankRowsRatio.toFixed(2)}, loading=${lastAnalysis.loadingLikeRowsRatio.toFixed(2)}`
1645
1145
  );
1646
1146
  }
1647
1147
  return lastBuffer;
@@ -2093,6 +1593,9 @@ var ProxyMeterRuntime = {
2093
1593
  getProxyMeterSnapshot
2094
1594
  };
2095
1595
 
1596
+ // src/internals/constants.js
1597
+ var PageRuntimeStateKey = "__playwright_toolkit_runtime_state__";
1598
+
2096
1599
  // src/runtime-env.js
2097
1600
  var BROWSER_PROFILE_SCHEMA_VERSION = 1;
2098
1601
  var SUPPORTED_CLOAK_FINGERPRINT_PLATFORMS = /* @__PURE__ */ new Set(["linux", "macos", "windows"]);
@@ -9672,11 +9175,11 @@ var resolveScreenshotWatermarkifyMeta = async (page, options = {}) => {
9672
9175
  watermark: options.watermark,
9673
9176
  strip: options.strip,
9674
9177
  stripLogoSrc,
9675
- device: resolvePageDevice2(page)
9178
+ device: resolvePageDevice(page)
9676
9179
  };
9677
9180
  };
9678
9181
  var buildFontFamily = () => 'MiSans, "SF Pro Display", "PingFang SC", "Helvetica Neue", Arial, sans-serif';
9679
- var resolvePageDevice2 = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
9182
+ var resolvePageDevice = (page) => normalizeDevice(page?.[PageRuntimeStateKey]?.device);
9680
9183
  var findStripSegment = (segments, kind) => {
9681
9184
  const found = Array.isArray(segments) ? segments.find((segment) => segment?.kind === kind) : null;
9682
9185
  return found && typeof found === "object" ? found : {};
@@ -10328,7 +9831,7 @@ var watermarkifyScreenshotBuffer = async (buffer, meta, page = null, options = {
10328
9831
  };
10329
9832
 
10330
9833
  // src/internals/compression.js
10331
- import { Jimp as Jimp2, JimpMime as JimpMime2, ResizeStrategy } from "jimp";
9834
+ import { Jimp as Jimp2, JimpMime, ResizeStrategy } from "jimp";
10332
9835
  var logger15 = createInternalLogger("Compression");
10333
9836
  var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
10334
9837
  var DEFAULT_SCREENSHOT_OUTPUT_TYPE = "jpeg";
@@ -10398,7 +9901,7 @@ var encodeJpeg = async (sourceImage, compression, scale, quality) => {
10398
9901
  mode: ResizeStrategy.BILINEAR
10399
9902
  });
10400
9903
  }
10401
- const buffer = await image.getBuffer(JimpMime2.jpeg, { quality });
9904
+ const buffer = await image.getBuffer(JimpMime.jpeg, { quality });
10402
9905
  return {
10403
9906
  buffer,
10404
9907
  bytes: getBase64BytesFromBuffer(buffer),