@ralphralphai/playwright 2.5.0 → 2.7.0

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.
@@ -70,7 +70,7 @@ function snapshot(value) {
70
70
  }
71
71
  //#endregion
72
72
  //#region package.json
73
- var version = "2.5.0";
73
+ var version = "2.7.0";
74
74
  //#endregion
75
75
  //#region src/actions.ts
76
76
  const BINDING = "__ralphAction";
@@ -551,30 +551,57 @@ async function waitForAnimations(page, timeoutMs) {
551
551
  //#region src/image_responses.ts
552
552
  const IMAGE_HASH_TIMEOUT_MS = 3e4;
553
553
  const hashesByPage = /* @__PURE__ */ new WeakMap();
554
+ /**
555
+ * The latest hash per url across all of a context's pages. The HTTP cache is
556
+ * shared by the context, so this is the copy a 304 tells any of them to reuse.
557
+ */
558
+ const hashesByContext = /* @__PURE__ */ new WeakMap();
554
559
  function sha256(data) {
555
560
  return (0, node_crypto.createHash)("sha256").update(data).digest("hex");
556
561
  }
557
562
  /**
558
- * Hashes every image the page loads, as it loads, so a snapshot hashes the
559
- * bytes the page showed instead of fetching them a second time. A second fetch
560
- * of a page's images comes as a burst, which hosts such as
563
+ * Hashes every image the context's pages load, as they load, so a snapshot
564
+ * hashes the bytes the page showed instead of fetching them a second time. A
565
+ * second fetch of a page's images comes as a burst, which hosts such as
561
566
  * upload.wikimedia.org answer with 429 for a share of them, and the image then
562
567
  * reads as replaced. Only the hash is kept, not the body. Entries persist
563
568
  * across navigations until another request for the same image replaces them
564
569
  * or the page closes.
570
+ *
571
+ * Watched on the context rather than each page: Playwright reports a popup
572
+ * only once its first navigation has committed, and the images that document
573
+ * requests early are gone by the time a listener can be added to the page.
565
574
  */
566
- function watchImageResponses(page) {
567
- if (hashesByPage.has(page)) return;
568
- const hashes = /* @__PURE__ */ new Map();
569
- hashesByPage.set(page, hashes);
570
- page.on("request", (request) => {
571
- if (request.resourceType() !== "image") return;
575
+ function watchImageResponses(context) {
576
+ if (hashesByContext.has(context)) return;
577
+ const latest = /* @__PURE__ */ new Map();
578
+ hashesByContext.set(context, latest);
579
+ context.on("request", (request) => {
580
+ if (request.resourceType() !== "image" || request.serviceWorker()) return;
581
+ const hashes = pageHashes(request.frame().page());
582
+ const cached = latest.get(request.url());
572
583
  const hash = request.response().then(async (response) => {
584
+ if (response?.status() === 304) return cached;
573
585
  return response?.ok() ? sha256(await response.body()) : void 0;
574
586
  }).catch(() => void 0);
575
- for (let redirected = request; redirected !== null; redirected = redirected.redirectedFrom()) hashes.set(redirected.url(), hash);
587
+ for (const url of redirectChain(request)) {
588
+ hashes.set(url, hash);
589
+ latest.set(url, hash);
590
+ }
576
591
  });
592
+ }
593
+ function pageHashes(page) {
594
+ const existing = hashesByPage.get(page);
595
+ if (existing) return existing;
596
+ const hashes = /* @__PURE__ */ new Map();
597
+ hashesByPage.set(page, hashes);
577
598
  page.once("close", () => hashesByPage.delete(page));
599
+ return hashes;
600
+ }
601
+ function redirectChain(request) {
602
+ const urls = [];
603
+ for (let redirected = request; redirected !== null; redirected = redirected.redirectedFrom()) urls.push(redirected.url());
604
+ return urls;
578
605
  }
579
606
  /**
580
607
  * The hash from the latest observed request for this URL, across navigations.
@@ -654,8 +681,10 @@ async function captureAriaSnapshot(page, { viewport, scroll }) {
654
681
  boxes: true,
655
682
  depth: MAX_ARIA_DEPTH
656
683
  });
657
- const visible = (await Promise.all(snapshot.map((node) => toDocument(page, node, scroll)))).flatMap((node) => {
658
- const kept = keepShown(node, viewport.width);
684
+ const placed = await Promise.all(snapshot.map((node) => toDocument(page, node, scroll)));
685
+ const clips = await readClips(page, placed);
686
+ const visible = placed.flatMap((node) => {
687
+ const kept = keepShown(node, viewport.width, clips, void 0);
659
688
  return kept === void 0 ? [] : [kept];
660
689
  });
661
690
  const described = await Promise.all(visible.map((node) => describe(page, node, {}, false)));
@@ -719,12 +748,129 @@ function isReachable(box, viewportWidth) {
719
748
  function isShown(node, viewportWidth) {
720
749
  return node.ref !== void 0 && isReachable(node.box, viewportWidth);
721
750
  }
722
- function keepShown(node, viewportWidth) {
751
+ function edgesOf(box) {
752
+ return {
753
+ left: box.x,
754
+ top: box.y,
755
+ right: box.x + box.width,
756
+ bottom: box.y + box.height
757
+ };
758
+ }
759
+ function intersect(a, b) {
760
+ if (a === void 0) return b;
761
+ return {
762
+ left: Math.max(a.left, b.left),
763
+ top: Math.max(a.top, b.top),
764
+ right: Math.min(a.right, b.right),
765
+ bottom: Math.min(a.bottom, b.bottom)
766
+ };
767
+ }
768
+ const ROUNDING_SLACK = 1;
769
+ function sticksOut(inner, outer) {
770
+ return inner.x < outer.x - ROUNDING_SLACK || inner.y < outer.y - ROUNDING_SLACK || inner.x + inner.width > outer.x + outer.width + ROUNDING_SLACK || inner.y + inner.height > outer.y + outer.height + ROUNDING_SLACK;
771
+ }
772
+ function overlaps(a, b) {
773
+ return Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x) > ROUNDING_SLACK && Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y) > ROUNDING_SLACK;
774
+ }
775
+ function isOutside(box, clip) {
776
+ return box.x >= clip.right || box.x + box.width <= clip.left || box.y >= clip.bottom || box.y + box.height <= clip.top;
777
+ }
778
+ /**
779
+ * The area each element is clipped to by its ancestors' `overflow`, in
780
+ * document space, by ref.
781
+ *
782
+ * The boxes are not clipped (the rows of a 448px scroll box run thousands of
783
+ * pixels down the page), and reading an element's ancestors is a page round
784
+ * trip. Only an element that may overflow one is read: a root, one sticking
785
+ * out of its parent, or one running into its next sibling. The clipping
786
+ * ancestor itself is often missing from the tree, since Playwright unwraps a
787
+ * nameless wrapper with one child. Every other element takes its parent's
788
+ * clip.
789
+ */
790
+ async function readClips(page, nodes) {
791
+ const candidates = [];
792
+ const visit = (node, parent, next, frame) => {
793
+ if (node.box !== void 0 && (parent === void 0 || sticksOut(node.box, parent) || next?.box !== void 0 && overlaps(node.box, next.box))) candidates.push({
794
+ node,
795
+ frame
796
+ });
797
+ const childFrame = node.role === "iframe" && node.box !== void 0 ? intersect(frame, edgesOf(node.box)) : frame;
798
+ const elements = (node.children ?? []).filter((child) => {
799
+ return typeof child !== "string";
800
+ });
801
+ elements.forEach((child, index) => {
802
+ visit(child, node.box, elements[index + 1], childFrame);
803
+ });
804
+ };
805
+ nodes.forEach((node, index) => {
806
+ visit(node, void 0, nodes[index + 1], void 0);
807
+ });
808
+ const clips = await Promise.all(candidates.map(async ({ node, frame }) => {
809
+ const clip = await readClip(page, node);
810
+ return clip === void 0 || node.ref === void 0 ? [] : [[node.ref, intersect(frame, clip)]];
811
+ }));
812
+ return new Map(clips.flat());
813
+ }
814
+ async function readClip(page, node) {
815
+ if (node.box === void 0 || node.ref === void 0) return;
816
+ let read;
817
+ try {
818
+ read = await page.locator(`aria-ref=${node.ref}`).evaluate((element) => {
819
+ const clip = {
820
+ left: -Infinity,
821
+ top: -Infinity,
822
+ right: Infinity,
823
+ bottom: Infinity
824
+ };
825
+ let position = getComputedStyle(element).position;
826
+ for (let ancestor = element.parentElement; ancestor !== null && ancestor !== document.body && ancestor !== document.documentElement; ancestor = ancestor.parentElement) {
827
+ if (position === "fixed") break;
828
+ const style = getComputedStyle(ancestor);
829
+ if (position === "absolute" && style.position === "static" && style.transform === "none") continue;
830
+ position = style.position;
831
+ const clipsX = style.overflowX !== "visible";
832
+ const clipsY = style.overflowY !== "visible";
833
+ if (!clipsX && !clipsY || style.display === "inline") continue;
834
+ const rect = ancestor.getBoundingClientRect();
835
+ const left = rect.left + ancestor.clientLeft;
836
+ const top = rect.top + ancestor.clientTop;
837
+ if (clipsX) {
838
+ clip.left = Math.max(clip.left, left);
839
+ clip.right = Math.min(clip.right, left + ancestor.clientWidth);
840
+ }
841
+ if (clipsY) {
842
+ clip.top = Math.max(clip.top, top);
843
+ clip.bottom = Math.min(clip.bottom, top + ancestor.clientHeight);
844
+ }
845
+ }
846
+ const own = element.getBoundingClientRect();
847
+ return {
848
+ x: own.x,
849
+ y: own.y,
850
+ clip
851
+ };
852
+ }, void 0, { timeout: ELEMENT_LOOKUP_TIMEOUT_MS });
853
+ } catch {
854
+ return;
855
+ }
856
+ const dx = node.box.x - read.x;
857
+ const dy = node.box.y - read.y;
858
+ return {
859
+ left: read.clip.left + dx,
860
+ top: read.clip.top + dy,
861
+ right: read.clip.right + dx,
862
+ bottom: read.clip.bottom + dy
863
+ };
864
+ }
865
+ function keepShown(node, viewportWidth, clips, inherited) {
723
866
  if (node.role === "iframe" && node.box !== void 0 && (node.box.width < MIN_IFRAME_SIZE || node.box.height < MIN_IFRAME_SIZE)) return;
867
+ const clip = (node.ref === void 0 ? void 0 : clips.get(node.ref)) ?? inherited;
868
+ if (clip !== void 0 && node.box !== void 0 && isOutside(node.box, clip)) return;
724
869
  const shown = isShown(node, viewportWidth);
870
+ const childClip = node.role === "iframe" && node.box !== void 0 ? intersect(clip, edgesOf(node.box)) : clip;
725
871
  const children = (node.children ?? []).flatMap((child) => {
726
872
  if (typeof child === "string") return shown ? [child] : [];
727
- const kept = keepShown(child, viewportWidth);
873
+ const kept = keepShown(child, viewportWidth, clips, childClip);
728
874
  return kept === void 0 ? [] : [kept];
729
875
  });
730
876
  if (!shown && children.length === 0) return;
@@ -1305,6 +1451,7 @@ var Recorder = class {
1305
1451
  freezer
1306
1452
  });
1307
1453
  this.actions.observe(context);
1454
+ watchImageResponses(context);
1308
1455
  context.on("page", onPage);
1309
1456
  for (const page of context.pages()) onPage(page);
1310
1457
  return ready;
@@ -1405,7 +1552,6 @@ var Recorder = class {
1405
1552
  }
1406
1553
  }).catch(() => void 0);
1407
1554
  page.on("framenavigated", state.onNavigation);
1408
- watchImageResponses(page);
1409
1555
  this.urlChanged(state);
1410
1556
  }
1411
1557
  urlChanged(state) {
@@ -2039,4 +2185,4 @@ Object.defineProperty(exports, "visitAllLinks", {
2039
2185
  }
2040
2186
  });
2041
2187
 
2042
- //# sourceMappingURL=upload-D40MqSQa.cjs.map
2188
+ //# sourceMappingURL=upload-_UWss__c.cjs.map