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