@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.
- package/README.md +19 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +1 -1
- package/dist/reporter.cjs +1 -1
- package/dist/reporter.mjs +1 -1
- package/dist/{upload-CGIMWRQM.mjs → upload-CcdDM33Z.mjs} +93 -38
- package/dist/upload-CcdDM33Z.mjs.map +1 -0
- package/dist/{upload-H-EycbCA.cjs → upload-KEwXpYxG.cjs} +93 -38
- package/dist/upload-KEwXpYxG.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/upload-CGIMWRQM.mjs.map +0 -1
- package/dist/upload-H-EycbCA.cjs.map +0 -1
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-
|
|
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-
|
|
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
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-
|
|
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.
|
|
48
|
+
var version = "2.8.0";
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/actions.ts
|
|
51
51
|
const BINDING = "__ralphAction";
|
|
@@ -526,30 +526,57 @@ async function waitForAnimations(page, timeoutMs) {
|
|
|
526
526
|
//#region src/image_responses.ts
|
|
527
527
|
const IMAGE_HASH_TIMEOUT_MS = 3e4;
|
|
528
528
|
const hashesByPage = /* @__PURE__ */ new WeakMap();
|
|
529
|
+
/**
|
|
530
|
+
* The latest hash per url across all of a context's pages. The HTTP cache is
|
|
531
|
+
* shared by the context, so this is the copy a 304 tells any of them to reuse.
|
|
532
|
+
*/
|
|
533
|
+
const hashesByContext = /* @__PURE__ */ new WeakMap();
|
|
529
534
|
function sha256(data) {
|
|
530
535
|
return createHash("sha256").update(data).digest("hex");
|
|
531
536
|
}
|
|
532
537
|
/**
|
|
533
|
-
* Hashes every image the
|
|
534
|
-
* bytes the page showed instead of fetching them a second time. A
|
|
535
|
-
* of a page's images comes as a burst, which hosts such as
|
|
538
|
+
* Hashes every image the context's pages load, as they load, so a snapshot
|
|
539
|
+
* hashes the bytes the page showed instead of fetching them a second time. A
|
|
540
|
+
* second fetch of a page's images comes as a burst, which hosts such as
|
|
536
541
|
* upload.wikimedia.org answer with 429 for a share of them, and the image then
|
|
537
542
|
* reads as replaced. Only the hash is kept, not the body. Entries persist
|
|
538
543
|
* across navigations until another request for the same image replaces them
|
|
539
544
|
* or the page closes.
|
|
545
|
+
*
|
|
546
|
+
* Watched on the context rather than each page: Playwright reports a popup
|
|
547
|
+
* only once its first navigation has committed, and the images that document
|
|
548
|
+
* requests early are gone by the time a listener can be added to the page.
|
|
540
549
|
*/
|
|
541
|
-
function watchImageResponses(
|
|
542
|
-
if (
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (request.resourceType() !== "image") return;
|
|
550
|
+
function watchImageResponses(context) {
|
|
551
|
+
if (hashesByContext.has(context)) return;
|
|
552
|
+
const latest = /* @__PURE__ */ new Map();
|
|
553
|
+
hashesByContext.set(context, latest);
|
|
554
|
+
context.on("request", (request) => {
|
|
555
|
+
if (request.resourceType() !== "image" || request.serviceWorker()) return;
|
|
556
|
+
const hashes = pageHashes(request.frame().page());
|
|
557
|
+
const cached = latest.get(request.url());
|
|
547
558
|
const hash = request.response().then(async (response) => {
|
|
559
|
+
if (response?.status() === 304) return cached;
|
|
548
560
|
return response?.ok() ? sha256(await response.body()) : void 0;
|
|
549
561
|
}).catch(() => void 0);
|
|
550
|
-
for (
|
|
562
|
+
for (const url of redirectChain(request)) {
|
|
563
|
+
hashes.set(url, hash);
|
|
564
|
+
latest.set(url, hash);
|
|
565
|
+
}
|
|
551
566
|
});
|
|
567
|
+
}
|
|
568
|
+
function pageHashes(page) {
|
|
569
|
+
const existing = hashesByPage.get(page);
|
|
570
|
+
if (existing) return existing;
|
|
571
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
572
|
+
hashesByPage.set(page, hashes);
|
|
552
573
|
page.once("close", () => hashesByPage.delete(page));
|
|
574
|
+
return hashes;
|
|
575
|
+
}
|
|
576
|
+
function redirectChain(request) {
|
|
577
|
+
const urls = [];
|
|
578
|
+
for (let redirected = request; redirected !== null; redirected = redirected.redirectedFrom()) urls.push(redirected.url());
|
|
579
|
+
return urls;
|
|
553
580
|
}
|
|
554
581
|
/**
|
|
555
582
|
* The hash from the latest observed request for this URL, across navigations.
|
|
@@ -623,7 +650,7 @@ const MIN_IFRAME_SIZE = 5;
|
|
|
623
650
|
* screenshot just showed: sticky headers, lazy loading, scroll-triggered
|
|
624
651
|
* animation.
|
|
625
652
|
*/
|
|
626
|
-
async function captureAriaSnapshot(page, { viewport, scroll }) {
|
|
653
|
+
async function captureAriaSnapshot(page, { viewport, scroll }, ignore = []) {
|
|
627
654
|
const snapshot = await page.locator("body").ariaSnapshotJSON({
|
|
628
655
|
mode: "ai",
|
|
629
656
|
boxes: true,
|
|
@@ -635,7 +662,10 @@ async function captureAriaSnapshot(page, { viewport, scroll }) {
|
|
|
635
662
|
const kept = keepShown(node, viewport.width, clips, void 0);
|
|
636
663
|
return kept === void 0 ? [] : [kept];
|
|
637
664
|
});
|
|
638
|
-
const
|
|
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)));
|
|
639
669
|
const json = Buffer.from(JSON.stringify(fillMissingRefs(described)));
|
|
640
670
|
return json.byteLength > 4194304 ? void 0 : json;
|
|
641
671
|
}
|
|
@@ -822,12 +852,14 @@ function keepShown(node, viewportWidth, clips, inherited) {
|
|
|
822
852
|
return kept === void 0 ? [] : [kept];
|
|
823
853
|
});
|
|
824
854
|
if (!shown && children.length === 0) return;
|
|
825
|
-
|
|
826
|
-
if (node.role === "generic" && !node.name && (node.children?.length ?? 0) > 1 && children.length === 1 && typeof only !== "string" && only?.ref !== void 0) return only;
|
|
827
|
-
return node.children === void 0 ? node : {
|
|
855
|
+
return loneChild(node, children) ?? (node.children === void 0 ? node : {
|
|
828
856
|
...node,
|
|
829
857
|
children
|
|
830
|
-
};
|
|
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;
|
|
831
863
|
}
|
|
832
864
|
function fillMissingRefs(nodes) {
|
|
833
865
|
let generated = 0;
|
|
@@ -846,34 +878,40 @@ function fillMissingRefs(nodes) {
|
|
|
846
878
|
};
|
|
847
879
|
return nodes.map(fill);
|
|
848
880
|
}
|
|
849
|
-
async function readElement(page, ref, isImage) {
|
|
881
|
+
async function readElement(page, ref, isImage, ignoreSelector) {
|
|
850
882
|
try {
|
|
851
|
-
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 }) => {
|
|
852
884
|
const computed = getComputedStyle(element);
|
|
853
885
|
const typed = "computedStyleMap" in element ? element.computedStyleMap() : void 0;
|
|
854
886
|
const style = Object.fromEntries(properties.map((property) => [property, property.startsWith("margin-") && typed?.get(property)?.toString() === "auto" ? "auto" : computed.getPropertyValue(property)]));
|
|
855
887
|
const dynamic = element.closest(`[${dynamicAttribute}]`) !== null;
|
|
856
|
-
|
|
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 = {
|
|
857
899
|
style,
|
|
858
|
-
dynamic
|
|
900
|
+
dynamic,
|
|
901
|
+
ignored
|
|
859
902
|
};
|
|
903
|
+
if (!isImage) return facts;
|
|
860
904
|
if (element instanceof HTMLImageElement) return {
|
|
861
|
-
|
|
862
|
-
dynamic,
|
|
905
|
+
...facts,
|
|
863
906
|
image: { src: element.currentSrc || element.src }
|
|
864
907
|
};
|
|
865
908
|
if (element instanceof SVGElement) return {
|
|
866
|
-
|
|
867
|
-
dynamic,
|
|
909
|
+
...facts,
|
|
868
910
|
image: { markup: element.outerHTML }
|
|
869
911
|
};
|
|
870
912
|
const backgroundUrl = /url\("(.+)"\)/.exec(computed.backgroundImage)?.[1];
|
|
871
|
-
return backgroundUrl === void 0 ? {
|
|
872
|
-
|
|
873
|
-
dynamic
|
|
874
|
-
} : {
|
|
875
|
-
style,
|
|
876
|
-
dynamic,
|
|
913
|
+
return backgroundUrl === void 0 ? facts : {
|
|
914
|
+
...facts,
|
|
877
915
|
image: { src: new URL(backgroundUrl, document.baseURI).href }
|
|
878
916
|
};
|
|
879
917
|
}, {
|
|
@@ -883,7 +921,8 @@ async function readElement(page, ref, isImage) {
|
|
|
883
921
|
...isImage ? Object.keys(IMAGE_PROPERTIES) : []
|
|
884
922
|
],
|
|
885
923
|
isImage,
|
|
886
|
-
dynamicAttribute: DYNAMIC_ATTRIBUTE
|
|
924
|
+
dynamicAttribute: DYNAMIC_ATTRIBUTE,
|
|
925
|
+
ignoreSelector
|
|
887
926
|
}, { timeout: ELEMENT_LOOKUP_TIMEOUT_MS });
|
|
888
927
|
} catch {
|
|
889
928
|
return;
|
|
@@ -905,13 +944,29 @@ function distinctStyle(style, inherited) {
|
|
|
905
944
|
});
|
|
906
945
|
return distinct.length === 0 ? void 0 : Object.fromEntries(distinct);
|
|
907
946
|
}
|
|
908
|
-
async function
|
|
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) {
|
|
909
964
|
if (typeof child === "string") return child;
|
|
910
|
-
const facts = child.ref === void 0 ? void 0 :
|
|
965
|
+
const facts = child.ref === void 0 ? void 0 : factsByRef.get(child.ref);
|
|
911
966
|
const style = facts === void 0 ? void 0 : distinctStyle(facts.style, inherited);
|
|
912
967
|
const imageHash = facts?.image === void 0 ? void 0 : await hashImage(page, facts.image);
|
|
913
968
|
const dynamic = facts?.dynamic === true && !insideDynamic;
|
|
914
|
-
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)));
|
|
915
970
|
return {
|
|
916
971
|
...child,
|
|
917
972
|
...style && { style },
|
|
@@ -1399,6 +1454,7 @@ var Recorder = class {
|
|
|
1399
1454
|
freezer
|
|
1400
1455
|
});
|
|
1401
1456
|
this.actions.observe(context);
|
|
1457
|
+
watchImageResponses(context);
|
|
1402
1458
|
context.on("page", onPage);
|
|
1403
1459
|
for (const page of context.pages()) onPage(page);
|
|
1404
1460
|
return ready;
|
|
@@ -1499,7 +1555,6 @@ var Recorder = class {
|
|
|
1499
1555
|
}
|
|
1500
1556
|
}).catch(() => void 0);
|
|
1501
1557
|
page.on("framenavigated", state.onNavigation);
|
|
1502
|
-
watchImageResponses(page);
|
|
1503
1558
|
this.urlChanged(state);
|
|
1504
1559
|
}
|
|
1505
1560
|
urlChanged(state) {
|
|
@@ -1592,7 +1647,7 @@ var Recorder = class {
|
|
|
1592
1647
|
before,
|
|
1593
1648
|
image,
|
|
1594
1649
|
after,
|
|
1595
|
-
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 })])
|
|
1596
1651
|
};
|
|
1597
1652
|
};
|
|
1598
1653
|
const { before, image, after, snapshot } = freezer ? await freezer.capture(page, deadline.signal, capture, deadline.endsAt) : await capture();
|
|
@@ -2026,4 +2081,4 @@ async function responseDetail(response) {
|
|
|
2026
2081
|
//#endregion
|
|
2027
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 };
|
|
2028
2083
|
|
|
2029
|
-
//# sourceMappingURL=upload-
|
|
2084
|
+
//# sourceMappingURL=upload-CcdDM33Z.mjs.map
|