@ralphralphai/playwright 2.7.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.
package/README.md CHANGED
@@ -254,6 +254,24 @@ Pass an `edgeLabel` to describe the step in your own words, e.g.
254
254
  `ralph.recordNode(page, { name: 'Done', edgeLabel: 'Sign in' })`. A later call
255
255
  without one keeps it.
256
256
 
257
+ ### Excluding elements from snapshots
258
+
259
+ For parts of the page that change on their own, such as ads, chat widgets or
260
+ live counters, list CSS selectors in `ignoreInDiff`:
261
+
262
+ ```ts
263
+ ralphOptions: {
264
+ ignoreInDiff: ['.ad-slot', '#intercom-container', 'live-clock'],
265
+ },
266
+ ```
267
+
268
+ Matched elements and their descendants, including content in open shadow roots
269
+ and slots, are left out of the ARIA snapshot used for diffing. They still appear
270
+ in the screenshot, and the space they take up still moves the elements around
271
+ them. Text that sits inline in a larger element, like a bare `<span>` in a
272
+ paragraph, belongs to that element's text and is not removed. Wrap it in an
273
+ element with a role, or match the whole paragraph.
274
+
257
275
  ### When recording fails
258
276
 
259
277
  - A failed `recordNode` call rejects.
@@ -414,6 +432,7 @@ export default defineConfig<RalphFixtures>({
414
432
  | `ready` | | `(page, signal) => Promise<void>`. Waits for your app before an automatic recording. Honor `signal`. |
415
433
  | `reduceMotion` | `false` | Emulates `prefers-reduced-motion: reduce` and waits for finite animations before recording. |
416
434
  | `freezeDuringCapture` | `false` | Finishes finite browser animations, allows JS-driven DOM changes to settle within a bounded wait, then freezes timers and animations across capture. |
435
+ | `ignoreInDiff` | | CSS selectors for subtrees to omit from the ARIA snapshot. See [Excluding elements from snapshots](#excluding-elements-from-snapshots). |
417
436
  | `strictNodeTags` | `false` | Fails the test when the app reports a tag outside what `ralph.setNodeConditions` declared. |
418
437
  | `ignoreUntaggedCalls` | `false` | Makes `recordNode`, `setNodeConditions`, and `observe` no-ops in untagged tests instead of throwing. |
419
438
 
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_upload = require("./upload-_UWss__c.cjs");
2
+ const require_upload = require("./upload-KEwXpYxG.cjs");
3
3
  let node_path = require("node:path");
4
4
  node_path = require_upload.__toESM(node_path, 1);
5
5
  let _playwright_test = require("@playwright/test");
package/dist/index.d.mts CHANGED
@@ -18,6 +18,11 @@ type RalphOptions = {
18
18
  */
19
19
  freezeDuringCapture?: boolean;
20
20
  recordTimeoutMs?: number;
21
+ /**
22
+ * CSS selectors for subtrees the diff ignores, such as an ad slot or a chat
23
+ * widget. They are left out of the aria snapshot but stay in the screenshot.
24
+ */
25
+ ignoreInDiff?: string[];
21
26
  ready?: (page: Page, signal: AbortSignal) => Promise<void>;
22
27
  /**
23
28
  * Fail the test when the app reports a tag outside the set the test
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as resolveMode, a as prepareArtifactUpload, f as Recorder, g as loadConfigSync, h as loadConfig, i as createRawGraphBundle, l as uploadRawGraph, p as visitAllLinks, u as MANIFEST_ATTACHMENT } from "./upload-BsCzkoup.mjs";
1
+ import { _ as resolveMode, a as prepareArtifactUpload, f as Recorder, g as loadConfigSync, h as loadConfig, i as createRawGraphBundle, l as uploadRawGraph, p as visitAllLinks, u as MANIFEST_ATTACHMENT } from "./upload-CcdDM33Z.mjs";
2
2
  import path from "node:path";
3
3
  import { expect, mergeTests, test as test$1 } from "@playwright/test";
4
4
  //#region src/screens.ts
package/dist/reporter.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_upload = require("./upload-_UWss__c.cjs");
1
+ const require_upload = require("./upload-KEwXpYxG.cjs");
2
2
  let node_path = require("node:path");
3
3
  node_path = require_upload.__toESM(node_path, 1);
4
4
  let node_fs_promises = require("node:fs/promises");
package/dist/reporter.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as resolveMode, a as prepareArtifactUpload, c as resultLink, d as REPORTER_ENV, l as uploadRawGraph, m as version, n as bundleImagePath, o as registerDeployment, r as bundleSnapshotPath, s as resolveUploadOptions, t as RAW_GRAPH_BUNDLE_INDEX, u as MANIFEST_ATTACHMENT } from "./upload-BsCzkoup.mjs";
1
+ import { _ as resolveMode, a as prepareArtifactUpload, c as resultLink, d as REPORTER_ENV, l as uploadRawGraph, m as version, n as bundleImagePath, o as registerDeployment, r as bundleSnapshotPath, s as resolveUploadOptions, t as RAW_GRAPH_BUNDLE_INDEX, u as MANIFEST_ATTACHMENT } from "./upload-CcdDM33Z.mjs";
2
2
  import path from "node:path";
3
3
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { randomUUID } from "node:crypto";
@@ -45,7 +45,7 @@ function snapshot(value) {
45
45
  }
46
46
  //#endregion
47
47
  //#region package.json
48
- var version = "2.7.0";
48
+ var version = "2.8.0";
49
49
  //#endregion
50
50
  //#region src/actions.ts
51
51
  const BINDING = "__ralphAction";
@@ -650,7 +650,7 @@ const MIN_IFRAME_SIZE = 5;
650
650
  * screenshot just showed: sticky headers, lazy loading, scroll-triggered
651
651
  * animation.
652
652
  */
653
- async function captureAriaSnapshot(page, { viewport, scroll }) {
653
+ async function captureAriaSnapshot(page, { viewport, scroll }, ignore = []) {
654
654
  const snapshot = await page.locator("body").ariaSnapshotJSON({
655
655
  mode: "ai",
656
656
  boxes: true,
@@ -662,7 +662,10 @@ async function captureAriaSnapshot(page, { viewport, scroll }) {
662
662
  const kept = keepShown(node, viewport.width, clips, void 0);
663
663
  return kept === void 0 ? [] : [kept];
664
664
  });
665
- const described = await Promise.all(visible.map((node) => describe(page, node, {}, false)));
665
+ const ignoreSelector = ignore.length === 0 ? void 0 : ignore.join(", ");
666
+ const facts = /* @__PURE__ */ new Map();
667
+ const normalized = (await Promise.all(visible.map((node) => readTree(page, node, ignoreSelector, facts)))).filter((node) => node !== void 0);
668
+ const described = await Promise.all(normalized.map((node) => describe(page, node, {}, false, facts)));
666
669
  const json = Buffer.from(JSON.stringify(fillMissingRefs(described)));
667
670
  return json.byteLength > 4194304 ? void 0 : json;
668
671
  }
@@ -849,12 +852,14 @@ function keepShown(node, viewportWidth, clips, inherited) {
849
852
  return kept === void 0 ? [] : [kept];
850
853
  });
851
854
  if (!shown && children.length === 0) return;
852
- const [only] = children;
853
- if (node.role === "generic" && !node.name && (node.children?.length ?? 0) > 1 && children.length === 1 && typeof only !== "string" && only?.ref !== void 0) return only;
854
- return node.children === void 0 ? node : {
855
+ return loneChild(node, children) ?? (node.children === void 0 ? node : {
855
856
  ...node,
856
857
  children
857
- };
858
+ });
859
+ }
860
+ function loneChild(node, children) {
861
+ const [only] = children;
862
+ if (node.role === "generic" && !node.name && (node.children?.length ?? 0) > 1 && children.length === 1 && typeof only !== "string" && only?.ref !== void 0) return only;
858
863
  }
859
864
  function fillMissingRefs(nodes) {
860
865
  let generated = 0;
@@ -873,34 +878,40 @@ function fillMissingRefs(nodes) {
873
878
  };
874
879
  return nodes.map(fill);
875
880
  }
876
- async function readElement(page, ref, isImage) {
881
+ async function readElement(page, ref, isImage, ignoreSelector) {
877
882
  try {
878
- return await page.locator(`aria-ref=${ref}`).evaluate((element, { properties, isImage, dynamicAttribute }) => {
883
+ return await page.locator(`aria-ref=${ref}`).evaluate((element, { properties, isImage, dynamicAttribute, ignoreSelector }) => {
879
884
  const computed = getComputedStyle(element);
880
885
  const typed = "computedStyleMap" in element ? element.computedStyleMap() : void 0;
881
886
  const style = Object.fromEntries(properties.map((property) => [property, property.startsWith("margin-") && typed?.get(property)?.toString() === "auto" ? "auto" : computed.getPropertyValue(property)]));
882
887
  const dynamic = element.closest(`[${dynamicAttribute}]`) !== null;
883
- if (!isImage) return {
888
+ let ignored = false;
889
+ for (let at = element; ignoreSelector !== void 0 && at !== null && !ignored;) {
890
+ try {
891
+ ignored = at.matches(ignoreSelector);
892
+ } catch {
893
+ break;
894
+ }
895
+ const root = at.getRootNode();
896
+ at = at.assignedSlot ?? at.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
897
+ }
898
+ const facts = {
884
899
  style,
885
- dynamic
900
+ dynamic,
901
+ ignored
886
902
  };
903
+ if (!isImage) return facts;
887
904
  if (element instanceof HTMLImageElement) return {
888
- style,
889
- dynamic,
905
+ ...facts,
890
906
  image: { src: element.currentSrc || element.src }
891
907
  };
892
908
  if (element instanceof SVGElement) return {
893
- style,
894
- dynamic,
909
+ ...facts,
895
910
  image: { markup: element.outerHTML }
896
911
  };
897
912
  const backgroundUrl = /url\("(.+)"\)/.exec(computed.backgroundImage)?.[1];
898
- return backgroundUrl === void 0 ? {
899
- style,
900
- dynamic
901
- } : {
902
- style,
903
- dynamic,
913
+ return backgroundUrl === void 0 ? facts : {
914
+ ...facts,
904
915
  image: { src: new URL(backgroundUrl, document.baseURI).href }
905
916
  };
906
917
  }, {
@@ -910,7 +921,8 @@ async function readElement(page, ref, isImage) {
910
921
  ...isImage ? Object.keys(IMAGE_PROPERTIES) : []
911
922
  ],
912
923
  isImage,
913
- dynamicAttribute: DYNAMIC_ATTRIBUTE
924
+ dynamicAttribute: DYNAMIC_ATTRIBUTE,
925
+ ignoreSelector
914
926
  }, { timeout: ELEMENT_LOOKUP_TIMEOUT_MS });
915
927
  } catch {
916
928
  return;
@@ -932,13 +944,29 @@ function distinctStyle(style, inherited) {
932
944
  });
933
945
  return distinct.length === 0 ? void 0 : Object.fromEntries(distinct);
934
946
  }
935
- async function describe(page, child, inherited, insideDynamic) {
947
+ async function readTree(page, child, ignoreSelector, factsByRef) {
948
+ if (typeof child === "string") return child;
949
+ const facts = child.ref === void 0 ? void 0 : await readElement(page, child.ref, child.role === "img", ignoreSelector);
950
+ if (facts?.ignored) return;
951
+ if (child.ref !== void 0 && facts !== void 0) factsByRef.set(child.ref, facts);
952
+ 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);
953
+ if (children !== void 0) {
954
+ if (children.length === 0 && (child.ref === void 0 || child.role === "generic" && !child.name && (child.children?.length ?? 0) > 0)) return;
955
+ const only = loneChild(child, children);
956
+ if (only !== void 0) return only;
957
+ }
958
+ return children === void 0 ? child : {
959
+ ...child,
960
+ children
961
+ };
962
+ }
963
+ async function describe(page, child, inherited, insideDynamic, factsByRef) {
936
964
  if (typeof child === "string") return child;
937
- const facts = child.ref === void 0 ? void 0 : await readElement(page, child.ref, child.role === "img");
965
+ const facts = child.ref === void 0 ? void 0 : factsByRef.get(child.ref);
938
966
  const style = facts === void 0 ? void 0 : distinctStyle(facts.style, inherited);
939
967
  const imageHash = facts?.image === void 0 ? void 0 : await hashImage(page, facts.image);
940
968
  const dynamic = facts?.dynamic === true && !insideDynamic;
941
- const children = child.children === void 0 ? void 0 : await Promise.all(child.children.map((grandchild) => describe(page, grandchild, facts?.style ?? inherited, insideDynamic || dynamic)));
969
+ const children = child.children === void 0 ? void 0 : await Promise.all(child.children.map((grandchild) => describe(page, grandchild, facts?.style ?? inherited, insideDynamic || dynamic, factsByRef)));
942
970
  return {
943
971
  ...child,
944
972
  ...style && { style },
@@ -1619,7 +1647,7 @@ var Recorder = class {
1619
1647
  before,
1620
1648
  image,
1621
1649
  after,
1622
- snapshot: await Promise.race([captureAriaSnapshot(page, after).catch(() => void 0), setTimeout$1(Math.max(0, deadline.endsAt - Date.now()), void 0, { ref: false })])
1650
+ snapshot: await Promise.race([captureAriaSnapshot(page, after, this.options.ignoreInDiff).catch(() => void 0), setTimeout$1(Math.max(0, deadline.endsAt - Date.now()), void 0, { ref: false })])
1623
1651
  };
1624
1652
  };
1625
1653
  const { before, image, after, snapshot } = freezer ? await freezer.capture(page, deadline.signal, capture, deadline.endsAt) : await capture();
@@ -2053,4 +2081,4 @@ async function responseDetail(response) {
2053
2081
  //#endregion
2054
2082
  export { resolveMode as _, prepareArtifactUpload as a, resultLink as c, REPORTER_ENV as d, Recorder as f, loadConfigSync as g, loadConfig as h, createRawGraphBundle as i, uploadRawGraph as l, version as m, bundleImagePath as n, registerDeployment as o, visitAllLinks as p, bundleSnapshotPath as r, resolveUploadOptions as s, RAW_GRAPH_BUNDLE_INDEX as t, MANIFEST_ATTACHMENT as u };
2055
2083
 
2056
- //# sourceMappingURL=upload-BsCzkoup.mjs.map
2084
+ //# sourceMappingURL=upload-CcdDM33Z.mjs.map