@ralphralphai/playwright 2.6.0 → 2.8.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.6.0";
73
+ var version = "2.8.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.
@@ -648,7 +675,7 @@ const MIN_IFRAME_SIZE = 5;
648
675
  * screenshot just showed: sticky headers, lazy loading, scroll-triggered
649
676
  * animation.
650
677
  */
651
- async function captureAriaSnapshot(page, { viewport, scroll }) {
678
+ async function captureAriaSnapshot(page, { viewport, scroll }, ignore = []) {
652
679
  const snapshot = await page.locator("body").ariaSnapshotJSON({
653
680
  mode: "ai",
654
681
  boxes: true,
@@ -660,7 +687,10 @@ async function captureAriaSnapshot(page, { viewport, scroll }) {
660
687
  const kept = keepShown(node, viewport.width, clips, void 0);
661
688
  return kept === void 0 ? [] : [kept];
662
689
  });
663
- const described = await Promise.all(visible.map((node) => describe(page, node, {}, false)));
690
+ const ignoreSelector = ignore.length === 0 ? void 0 : ignore.join(", ");
691
+ const facts = /* @__PURE__ */ new Map();
692
+ const normalized = (await Promise.all(visible.map((node) => readTree(page, node, ignoreSelector, facts)))).filter((node) => node !== void 0);
693
+ const described = await Promise.all(normalized.map((node) => describe(page, node, {}, false, facts)));
664
694
  const json = Buffer.from(JSON.stringify(fillMissingRefs(described)));
665
695
  return json.byteLength > 4194304 ? void 0 : json;
666
696
  }
@@ -847,12 +877,14 @@ function keepShown(node, viewportWidth, clips, inherited) {
847
877
  return kept === void 0 ? [] : [kept];
848
878
  });
849
879
  if (!shown && children.length === 0) return;
850
- const [only] = children;
851
- if (node.role === "generic" && !node.name && (node.children?.length ?? 0) > 1 && children.length === 1 && typeof only !== "string" && only?.ref !== void 0) return only;
852
- return node.children === void 0 ? node : {
880
+ return loneChild(node, children) ?? (node.children === void 0 ? node : {
853
881
  ...node,
854
882
  children
855
- };
883
+ });
884
+ }
885
+ function loneChild(node, children) {
886
+ const [only] = children;
887
+ if (node.role === "generic" && !node.name && (node.children?.length ?? 0) > 1 && children.length === 1 && typeof only !== "string" && only?.ref !== void 0) return only;
856
888
  }
857
889
  function fillMissingRefs(nodes) {
858
890
  let generated = 0;
@@ -871,34 +903,40 @@ function fillMissingRefs(nodes) {
871
903
  };
872
904
  return nodes.map(fill);
873
905
  }
874
- async function readElement(page, ref, isImage) {
906
+ async function readElement(page, ref, isImage, ignoreSelector) {
875
907
  try {
876
- return await page.locator(`aria-ref=${ref}`).evaluate((element, { properties, isImage, dynamicAttribute }) => {
908
+ return await page.locator(`aria-ref=${ref}`).evaluate((element, { properties, isImage, dynamicAttribute, ignoreSelector }) => {
877
909
  const computed = getComputedStyle(element);
878
910
  const typed = "computedStyleMap" in element ? element.computedStyleMap() : void 0;
879
911
  const style = Object.fromEntries(properties.map((property) => [property, property.startsWith("margin-") && typed?.get(property)?.toString() === "auto" ? "auto" : computed.getPropertyValue(property)]));
880
912
  const dynamic = element.closest(`[${dynamicAttribute}]`) !== null;
881
- if (!isImage) return {
913
+ let ignored = false;
914
+ for (let at = element; ignoreSelector !== void 0 && at !== null && !ignored;) {
915
+ try {
916
+ ignored = at.matches(ignoreSelector);
917
+ } catch {
918
+ break;
919
+ }
920
+ const root = at.getRootNode();
921
+ at = at.assignedSlot ?? at.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
922
+ }
923
+ const facts = {
882
924
  style,
883
- dynamic
925
+ dynamic,
926
+ ignored
884
927
  };
928
+ if (!isImage) return facts;
885
929
  if (element instanceof HTMLImageElement) return {
886
- style,
887
- dynamic,
930
+ ...facts,
888
931
  image: { src: element.currentSrc || element.src }
889
932
  };
890
933
  if (element instanceof SVGElement) return {
891
- style,
892
- dynamic,
934
+ ...facts,
893
935
  image: { markup: element.outerHTML }
894
936
  };
895
937
  const backgroundUrl = /url\("(.+)"\)/.exec(computed.backgroundImage)?.[1];
896
- return backgroundUrl === void 0 ? {
897
- style,
898
- dynamic
899
- } : {
900
- style,
901
- dynamic,
938
+ return backgroundUrl === void 0 ? facts : {
939
+ ...facts,
902
940
  image: { src: new URL(backgroundUrl, document.baseURI).href }
903
941
  };
904
942
  }, {
@@ -908,7 +946,8 @@ async function readElement(page, ref, isImage) {
908
946
  ...isImage ? Object.keys(IMAGE_PROPERTIES) : []
909
947
  ],
910
948
  isImage,
911
- dynamicAttribute: DYNAMIC_ATTRIBUTE
949
+ dynamicAttribute: DYNAMIC_ATTRIBUTE,
950
+ ignoreSelector
912
951
  }, { timeout: ELEMENT_LOOKUP_TIMEOUT_MS });
913
952
  } catch {
914
953
  return;
@@ -930,13 +969,29 @@ function distinctStyle(style, inherited) {
930
969
  });
931
970
  return distinct.length === 0 ? void 0 : Object.fromEntries(distinct);
932
971
  }
933
- async function describe(page, child, inherited, insideDynamic) {
972
+ async function readTree(page, child, ignoreSelector, factsByRef) {
973
+ if (typeof child === "string") return child;
974
+ const facts = child.ref === void 0 ? void 0 : await readElement(page, child.ref, child.role === "img", ignoreSelector);
975
+ if (facts?.ignored) return;
976
+ if (child.ref !== void 0 && facts !== void 0) factsByRef.set(child.ref, facts);
977
+ const children = child.children === void 0 ? void 0 : (await Promise.all(child.children.map((grandchild) => readTree(page, grandchild, ignoreSelector, factsByRef)))).filter((grandchild) => grandchild !== void 0);
978
+ if (children !== void 0) {
979
+ if (children.length === 0 && (child.ref === void 0 || child.role === "generic" && !child.name && (child.children?.length ?? 0) > 0)) return;
980
+ const only = loneChild(child, children);
981
+ if (only !== void 0) return only;
982
+ }
983
+ return children === void 0 ? child : {
984
+ ...child,
985
+ children
986
+ };
987
+ }
988
+ async function describe(page, child, inherited, insideDynamic, factsByRef) {
934
989
  if (typeof child === "string") return child;
935
- const facts = child.ref === void 0 ? void 0 : await readElement(page, child.ref, child.role === "img");
990
+ const facts = child.ref === void 0 ? void 0 : factsByRef.get(child.ref);
936
991
  const style = facts === void 0 ? void 0 : distinctStyle(facts.style, inherited);
937
992
  const imageHash = facts?.image === void 0 ? void 0 : await hashImage(page, facts.image);
938
993
  const dynamic = facts?.dynamic === true && !insideDynamic;
939
- const children = child.children === void 0 ? void 0 : await Promise.all(child.children.map((grandchild) => describe(page, grandchild, facts?.style ?? inherited, insideDynamic || dynamic)));
994
+ const children = child.children === void 0 ? void 0 : await Promise.all(child.children.map((grandchild) => describe(page, grandchild, facts?.style ?? inherited, insideDynamic || dynamic, factsByRef)));
940
995
  return {
941
996
  ...child,
942
997
  ...style && { style },
@@ -1424,6 +1479,7 @@ var Recorder = class {
1424
1479
  freezer
1425
1480
  });
1426
1481
  this.actions.observe(context);
1482
+ watchImageResponses(context);
1427
1483
  context.on("page", onPage);
1428
1484
  for (const page of context.pages()) onPage(page);
1429
1485
  return ready;
@@ -1524,7 +1580,6 @@ var Recorder = class {
1524
1580
  }
1525
1581
  }).catch(() => void 0);
1526
1582
  page.on("framenavigated", state.onNavigation);
1527
- watchImageResponses(page);
1528
1583
  this.urlChanged(state);
1529
1584
  }
1530
1585
  urlChanged(state) {
@@ -1617,7 +1672,7 @@ var Recorder = class {
1617
1672
  before,
1618
1673
  image,
1619
1674
  after,
1620
- snapshot: await Promise.race([captureAriaSnapshot(page, after).catch(() => void 0), (0, node_timers_promises.setTimeout)(Math.max(0, deadline.endsAt - Date.now()), void 0, { ref: false })])
1675
+ snapshot: await Promise.race([captureAriaSnapshot(page, after, this.options.ignoreInDiff).catch(() => void 0), (0, node_timers_promises.setTimeout)(Math.max(0, deadline.endsAt - Date.now()), void 0, { ref: false })])
1621
1676
  };
1622
1677
  };
1623
1678
  const { before, image, after, snapshot } = freezer ? await freezer.capture(page, deadline.signal, capture, deadline.endsAt) : await capture();
@@ -2158,4 +2213,4 @@ Object.defineProperty(exports, "visitAllLinks", {
2158
2213
  }
2159
2214
  });
2160
2215
 
2161
- //# sourceMappingURL=upload-H-EycbCA.cjs.map
2216
+ //# sourceMappingURL=upload-KEwXpYxG.cjs.map