@cronn/element-snapshot 0.14.1 → 0.16.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 +20 -79
- package/dist/browser-lib.iife.js +89 -95
- package/dist/index.d.mts +20 -17
- package/dist/index.mjs +26 -29
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
# Element Snapshots
|
|
1
|
+
# Playwright Element Snapshots
|
|
2
2
|
|
|
3
|
-
Element
|
|
3
|
+
Element snapshots for Playwright.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Read the full documentation](https://cronn.github.io/file-snapshots/playwright/element-snapshots/)
|
|
6
6
|
|
|
7
7
|
> [!NOTE]
|
|
8
8
|
> Element Snapshots are currently experimental. Not all HTML elements, ARIA roles and attributes are covered. Breaking changes to the snapshot format are possible.
|
|
9
9
|
|
|
10
10
|
## Getting Started
|
|
11
11
|
|
|
12
|
-
###
|
|
12
|
+
### Installation
|
|
13
13
|
|
|
14
14
|
```shell
|
|
15
|
-
|
|
15
|
+
pnpm add -D @cronn/element-snapshot
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```shell
|
|
19
|
-
|
|
19
|
+
npm install -D @cronn/element-snapshot
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
```shell
|
|
23
|
-
|
|
23
|
+
yarn add -D @cronn/element-snapshot
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Writing Tests
|
|
27
27
|
|
|
28
|
-
This package provides multiple snapshot formats for different use cases:
|
|
29
|
-
|
|
30
|
-
- **General-Purpose Snapshots**: Human-readable JSON format for all element types
|
|
31
|
-
- **Markdown Table Snapshots**: Specialized format for tables and grids ([documentation](docs/markdown-table-snapshot.md))
|
|
32
|
-
- **Custom Snapshots**: Build your own snapshot formats using raw element data and utility functions
|
|
33
|
-
|
|
34
|
-
### General-Purpose Snapshots
|
|
35
|
-
|
|
36
|
-
The function `snapshotElement` provides a general-purpose snapshot including all supported roles and attributes. It can be used to achieve a high test coverage, but can become hard to read for complex HTML structures.
|
|
37
|
-
|
|
38
28
|
```ts
|
|
39
|
-
import {
|
|
29
|
+
import { semanticSnapshot } from "@cronn/element-snapshot";
|
|
40
30
|
import { defineValidationFileExpect } from "@cronn/playwright-file-snapshots";
|
|
41
31
|
|
|
42
32
|
const expect = defineValidationFileExpect();
|
|
43
33
|
|
|
44
34
|
test("matches element snapshot", async ({ page }) => {
|
|
45
|
-
await
|
|
35
|
+
await page.setContent(`
|
|
36
|
+
<main>
|
|
37
|
+
<h1>List</h1>
|
|
38
|
+
<ul>
|
|
39
|
+
<li>Apple</li>
|
|
40
|
+
<li>Peach</li>
|
|
41
|
+
</ul>
|
|
42
|
+
</main>
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
await expect(semanticSnapshot(page.getByRole("main"))).toMatchJsonFile();
|
|
46
46
|
});
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
**Output (`matches_element_snapshot.json`):**
|
|
50
50
|
|
|
51
51
|
```json
|
|
52
52
|
{
|
|
@@ -70,62 +70,3 @@ Element Snapshot Example:
|
|
|
70
70
|
]
|
|
71
71
|
}
|
|
72
72
|
```
|
|
73
|
-
|
|
74
|
-
To improve the specificity of certain tests, `snapshotElement` can be called on certain areas of the page only:
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
test("matches navigation snapshot", async ({ page }) => {
|
|
78
|
-
await expect(snapshotElement(page.getByRole("navigation"))).toMatchJsonFile({
|
|
79
|
-
name: "navigation",
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
#### Snapshot Options
|
|
85
|
-
|
|
86
|
-
Snapshot options can be passed when calling the snapshot function:
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
await expect(
|
|
90
|
-
snapshotElement(page.getByLabel("My select"), {
|
|
91
|
-
filter: (element) => element.role === "heading",
|
|
92
|
-
}),
|
|
93
|
-
).toMatchJsonFile({
|
|
94
|
-
name: "headings",
|
|
95
|
-
});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
| Option | Default Value | Description |
|
|
99
|
-
| ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
-
| `filter` | `() => true` | Include only elements in the snapshot for which the specified filter returns `true`. |
|
|
101
|
-
| `recurseFilter` | `false` | Recursively apply specified filter to children of filtered elements. By default, recursion ends when the filter returns `true` for an element. Should be `true` for filters intended to remove specific elements recursively. |
|
|
102
|
-
| `includeComboboxOptions` | `false` | Include combobox options in the snapshot. |
|
|
103
|
-
|
|
104
|
-
### Custom Snapshots
|
|
105
|
-
|
|
106
|
-
The function `snapshotElementRaw` provides the raw element snapshot in a strictly typed structure. This format is not well suited to be read by humans, but can be utilized to derive custom snapshot formats, e.g. for HTML tables.
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
import { snapshotElementRaw } from "@cronn/element-snapshot";
|
|
110
|
-
|
|
111
|
-
test("matches custom snapshot", async ({ page }) => {
|
|
112
|
-
const tableSnapshot = await snapshotElementRaw(page.getByRole("table"));
|
|
113
|
-
const markdownTable = transformToMarkdownTable(tableSnapshot);
|
|
114
|
-
|
|
115
|
-
await expect(markdownTable).toMatchTextFile({ fileExtension: "md" });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
function transformToMarkdownTable(snapshot: Array<NodeSnapshot>): string {
|
|
119
|
-
// transform table snapshot to markdown table
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
#### Utility Functions for Custom Snapshots
|
|
124
|
-
|
|
125
|
-
| Function | Description |
|
|
126
|
-
| ---------------- | --------------------------------------------------------------------- |
|
|
127
|
-
| `filter` | Filters node snapshots based on the provided predicate function. |
|
|
128
|
-
| `filterByRole` | Filters node snapshots by the specified role. |
|
|
129
|
-
| `includeRole` | Includes only elements with the specified role. |
|
|
130
|
-
| `excludeRole` | Excludes elements with the specified role. |
|
|
131
|
-
| `getTextContent` | Aggregates and normalizes the text content of all provided snapshots. |
|
package/dist/browser-lib.iife.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
(function() {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//#region src/browser/attribute.ts
|
|
2
|
+
//#region src/browser/attribute.ts
|
|
5
3
|
function stringAttribute(value) {
|
|
6
4
|
return value !== null && value !== "" ? value : void 0;
|
|
7
5
|
}
|
|
@@ -22,15 +20,16 @@
|
|
|
22
20
|
if (referenceId === null) return null;
|
|
23
21
|
return element.ownerDocument.getElementById(referenceId);
|
|
24
22
|
}
|
|
25
|
-
|
|
26
|
-
//#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/utils/factories.ts
|
|
25
|
+
function textSnapshot(name) {
|
|
26
|
+
return {
|
|
27
|
+
role: "text",
|
|
28
|
+
name
|
|
29
|
+
};
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
//#
|
|
33
|
-
//#region src/utils/guards.ts
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/utils/guards.ts
|
|
34
33
|
function isTextSnapshot(snapshot) {
|
|
35
34
|
return hasRole("text", snapshot);
|
|
36
35
|
}
|
|
@@ -38,9 +37,8 @@
|
|
|
38
37
|
if (typeof role === "string") return role === snapshot.role;
|
|
39
38
|
return role.includes(snapshot.role);
|
|
40
39
|
}
|
|
41
|
-
|
|
42
|
-
//#
|
|
43
|
-
//#region src/utils/filter.ts
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/utils/filter.ts
|
|
44
42
|
function filter(options) {
|
|
45
43
|
const { predicate, snapshots, recurse = false } = options;
|
|
46
44
|
return snapshots.flatMap((snapshot) => {
|
|
@@ -63,24 +61,39 @@
|
|
|
63
61
|
function hasChildren(snapshot) {
|
|
64
62
|
return "children" in snapshot && snapshot.children.length > 0;
|
|
65
63
|
}
|
|
66
|
-
|
|
67
|
-
//#
|
|
68
|
-
|
|
69
|
-
/**
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/utils/text.ts
|
|
66
|
+
/**
|
|
70
67
|
* Aggregates and normalizes the text content of all provided snapshots
|
|
71
68
|
*/
|
|
72
69
|
function getTextContent(snapshots) {
|
|
73
|
-
return
|
|
70
|
+
return mergeTexts(filter({
|
|
74
71
|
snapshots,
|
|
75
72
|
predicate: isTextSnapshot
|
|
76
|
-
})
|
|
73
|
+
}));
|
|
77
74
|
}
|
|
78
75
|
function normalizeText(text) {
|
|
79
76
|
return text.replaceAll(/\s+/g, " ").trim();
|
|
80
77
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
function mergeTexts(texts) {
|
|
79
|
+
return normalizeText(texts.map((textNode) => textNode.name).join(" "));
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/browser/children.ts
|
|
83
|
+
function snapshotChildren(node, excludedNodes = []) {
|
|
84
|
+
return mergeConsecutiveTexts(Array.from(node.childNodes).map((childNode) => snapshotNodeRecursive(childNode, excludedNodes)).filter((childSnapshot) => childSnapshot !== null).flat());
|
|
85
|
+
}
|
|
86
|
+
function mergeConsecutiveTexts(children) {
|
|
87
|
+
return children.reduce((mergedChildren, child) => {
|
|
88
|
+
const lastMergedChildIndex = mergedChildren.length - 1;
|
|
89
|
+
const lastMergedChild = mergedChildren[lastMergedChildIndex];
|
|
90
|
+
if (lastMergedChild !== void 0 && isTextSnapshot(lastMergedChild) && isTextSnapshot(child)) mergedChildren[lastMergedChildIndex] = textSnapshot(mergeTexts([lastMergedChild, child]));
|
|
91
|
+
else mergedChildren.push(child);
|
|
92
|
+
return mergedChildren;
|
|
93
|
+
}, []);
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/browser/text.ts
|
|
84
97
|
function snapshotTextNode(textNode) {
|
|
85
98
|
if (textNode.textContent === null) return null;
|
|
86
99
|
const normalizedText = normalizeText(textNode.textContent);
|
|
@@ -104,18 +117,16 @@
|
|
|
104
117
|
const textContent = getTextContent(children);
|
|
105
118
|
return textContent.length > 0 ? textContent : void 0;
|
|
106
119
|
}
|
|
107
|
-
|
|
108
|
-
//#
|
|
109
|
-
//#region src/browser/name.ts
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/browser/name.ts
|
|
110
122
|
function resolveAccessibleName(element, includeTextContent = true) {
|
|
111
123
|
const labelledByElement = resolveElementReference(element, "aria-labelledby");
|
|
112
124
|
if (labelledByElement !== null) return resolveAccessibleTextContent(labelledByElement);
|
|
113
125
|
if (element.ariaLabel !== null) return element.ariaLabel;
|
|
114
126
|
if (includeTextContent) return resolveAccessibleTextContent(element);
|
|
115
127
|
}
|
|
116
|
-
|
|
117
|
-
//#
|
|
118
|
-
//#region src/browser/state.ts
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/browser/state.ts
|
|
119
130
|
function disableableAttributes(element) {
|
|
120
131
|
return { disabled: booleanAttribute(isNativeInputElement(element) || element instanceof HTMLButtonElement ? element.hasAttribute("disabled") : false) ?? booleanAttribute(element.ariaDisabled) };
|
|
121
132
|
}
|
|
@@ -136,9 +147,8 @@
|
|
|
136
147
|
function isNativeInputElement(element) {
|
|
137
148
|
return element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement;
|
|
138
149
|
}
|
|
139
|
-
|
|
140
|
-
//#
|
|
141
|
-
//#region src/browser/button.ts
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/browser/button.ts
|
|
142
152
|
const PRESSED_VALUES = new Set([
|
|
143
153
|
"true",
|
|
144
154
|
"mixed",
|
|
@@ -161,9 +171,8 @@
|
|
|
161
171
|
if (pressed === "mixed") return pressed;
|
|
162
172
|
return booleanAttribute(pressed === "true");
|
|
163
173
|
}
|
|
164
|
-
|
|
165
|
-
//#
|
|
166
|
-
//#region src/browser/description.ts
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/browser/description.ts
|
|
167
176
|
function discribableAttributes(element) {
|
|
168
177
|
return { description: resolveDescription(element) };
|
|
169
178
|
}
|
|
@@ -172,9 +181,8 @@
|
|
|
172
181
|
if (describedByElement !== null) return resolveAccessibleTextContent(describedByElement);
|
|
173
182
|
if (element.ariaDescription !== null) return element.ariaDescription;
|
|
174
183
|
}
|
|
175
|
-
|
|
176
|
-
//#
|
|
177
|
-
//#region src/browser/container.ts
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/browser/container.ts
|
|
178
186
|
const CONTAINER_ROLES = new Set([
|
|
179
187
|
"paragraph",
|
|
180
188
|
"list",
|
|
@@ -202,13 +210,15 @@
|
|
|
202
210
|
"alert",
|
|
203
211
|
"menu",
|
|
204
212
|
"tablist",
|
|
205
|
-
"tabpanel"
|
|
213
|
+
"tabpanel",
|
|
214
|
+
"label"
|
|
206
215
|
]);
|
|
216
|
+
const NAMEABLE_CONTAINER_ROLES = new Set(["label"]);
|
|
207
217
|
function snapshotContainer(role, element) {
|
|
208
218
|
const children = snapshotChildren(element);
|
|
209
219
|
return {
|
|
210
220
|
role,
|
|
211
|
-
name: resolveAccessibleName(element,
|
|
221
|
+
name: resolveAccessibleName(element, NAMEABLE_CONTAINER_ROLES.has(role)),
|
|
212
222
|
attributes: {},
|
|
213
223
|
children
|
|
214
224
|
};
|
|
@@ -216,9 +226,8 @@
|
|
|
216
226
|
function isContainerRole(role) {
|
|
217
227
|
return CONTAINER_ROLES.has(role);
|
|
218
228
|
}
|
|
219
|
-
|
|
220
|
-
//#
|
|
221
|
-
//#region src/browser/selector.ts
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/browser/selector.ts
|
|
222
231
|
function selectorList(...selectors) {
|
|
223
232
|
return selectors.join(",");
|
|
224
233
|
}
|
|
@@ -228,18 +237,16 @@
|
|
|
228
237
|
function attributeSelector(name, value) {
|
|
229
238
|
return `[${name}='${value}']`;
|
|
230
239
|
}
|
|
231
|
-
|
|
232
|
-
//#
|
|
233
|
-
//#region src/browser/utils.ts
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/browser/utils.ts
|
|
234
242
|
function getElementTagName(element) {
|
|
235
243
|
return element.tagName.toLowerCase();
|
|
236
244
|
}
|
|
237
245
|
function isWithinElement(element, containerSelector) {
|
|
238
246
|
return element.closest(containerSelector) !== null;
|
|
239
247
|
}
|
|
240
|
-
|
|
241
|
-
//#
|
|
242
|
-
//#region src/browser/role.ts
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/browser/role.ts
|
|
243
250
|
const ELEMENT_ROLES = {
|
|
244
251
|
main: "main",
|
|
245
252
|
nav: "navigation",
|
|
@@ -279,7 +286,9 @@
|
|
|
279
286
|
fieldset: "group",
|
|
280
287
|
progress: "progressbar",
|
|
281
288
|
hr: "separator",
|
|
282
|
-
img: "img"
|
|
289
|
+
img: "img",
|
|
290
|
+
label: "label",
|
|
291
|
+
legend: "label"
|
|
283
292
|
};
|
|
284
293
|
const CONTEXT_DEPENDENT_ROLES = {
|
|
285
294
|
banner: isTopLevelBanner,
|
|
@@ -397,9 +406,8 @@
|
|
|
397
406
|
function isWithinTablist(element) {
|
|
398
407
|
return isWithinElement(element, roleSelector("tablist"));
|
|
399
408
|
}
|
|
400
|
-
|
|
401
|
-
//#
|
|
402
|
-
//#region src/browser/input.ts
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/browser/input.ts
|
|
403
411
|
function snapshotInput(element) {
|
|
404
412
|
if (element instanceof HTMLInputElement) return snapshotInputElement(element);
|
|
405
413
|
if (element instanceof HTMLTextAreaElement) return snapshotTextareaElement(element);
|
|
@@ -462,9 +470,8 @@
|
|
|
462
470
|
if (element instanceof HTMLSelectElement || element instanceof HTMLButtonElement) return;
|
|
463
471
|
return stringAttribute(element.placeholder);
|
|
464
472
|
}
|
|
465
|
-
|
|
466
|
-
//#
|
|
467
|
-
//#region src/browser/combobox.ts
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/browser/combobox.ts
|
|
468
475
|
function snapshotCombobox(element) {
|
|
469
476
|
if (!isCombobox(element)) return null;
|
|
470
477
|
const options = snapshotOptions(element);
|
|
@@ -511,9 +518,8 @@
|
|
|
511
518
|
children: snapshotChildren(element)
|
|
512
519
|
};
|
|
513
520
|
}
|
|
514
|
-
|
|
515
|
-
//#
|
|
516
|
-
//#region src/browser/dialog.ts
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/browser/dialog.ts
|
|
517
523
|
function snapshotDialogWithRole(role) {
|
|
518
524
|
return (element) => snapshotDialog(role, element);
|
|
519
525
|
}
|
|
@@ -528,9 +534,8 @@
|
|
|
528
534
|
children: snapshotChildren(element)
|
|
529
535
|
};
|
|
530
536
|
}
|
|
531
|
-
|
|
532
|
-
//#
|
|
533
|
-
//#region src/browser/group.ts
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/browser/group.ts
|
|
534
539
|
function snapshotGroup(element) {
|
|
535
540
|
return {
|
|
536
541
|
role: "group",
|
|
@@ -552,9 +557,8 @@
|
|
|
552
557
|
children: snapshotChildren(element)
|
|
553
558
|
};
|
|
554
559
|
}
|
|
555
|
-
|
|
556
|
-
//#
|
|
557
|
-
//#region src/browser/heading.ts
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/browser/heading.ts
|
|
558
562
|
const LEVEL_REGEXP = /^H([1-6])$/;
|
|
559
563
|
function snapshotHeading(element) {
|
|
560
564
|
if (!(element instanceof HTMLHeadingElement)) return null;
|
|
@@ -568,9 +572,8 @@
|
|
|
568
572
|
function resolveLevel(element) {
|
|
569
573
|
return numericAttribute(LEVEL_REGEXP.exec(element.nodeName)?.at(1) ?? null);
|
|
570
574
|
}
|
|
571
|
-
|
|
572
|
-
//#
|
|
573
|
-
//#region src/browser/image.ts
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/browser/image.ts
|
|
574
577
|
function snapshotImage(element) {
|
|
575
578
|
const imageName = resolveImageName(element);
|
|
576
579
|
if (imageName === void 0) return null;
|
|
@@ -585,9 +588,8 @@
|
|
|
585
588
|
if (element instanceof HTMLImageElement) return stringAttribute(element.alt);
|
|
586
589
|
return resolveAccessibleName(element, true);
|
|
587
590
|
}
|
|
588
|
-
|
|
589
|
-
//#
|
|
590
|
-
//#region src/browser/link.ts
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/browser/link.ts
|
|
591
593
|
function snapshotLink(element) {
|
|
592
594
|
return {
|
|
593
595
|
role: "link",
|
|
@@ -599,9 +601,8 @@
|
|
|
599
601
|
function linkAttributes(element) {
|
|
600
602
|
return { url: stringAttribute(element.getAttribute("href")) };
|
|
601
603
|
}
|
|
602
|
-
|
|
603
|
-
//#
|
|
604
|
-
//#region src/browser/list.ts
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/browser/list.ts
|
|
605
606
|
function snapshotMenuitem(element) {
|
|
606
607
|
return {
|
|
607
608
|
role: "menuitem",
|
|
@@ -613,9 +614,8 @@
|
|
|
613
614
|
children: snapshotChildren(element)
|
|
614
615
|
};
|
|
615
616
|
}
|
|
616
|
-
|
|
617
|
-
//#
|
|
618
|
-
//#region src/browser/progressbar.ts
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/browser/progressbar.ts
|
|
619
619
|
function snapshotProgressbar(element) {
|
|
620
620
|
return {
|
|
621
621
|
role: "progressbar",
|
|
@@ -628,9 +628,8 @@
|
|
|
628
628
|
if (element instanceof HTMLProgressElement) return element.value;
|
|
629
629
|
return numericAttribute(element.ariaValueNow);
|
|
630
630
|
}
|
|
631
|
-
|
|
632
|
-
//#
|
|
633
|
-
//#region src/browser/separator.ts
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/browser/separator.ts
|
|
634
633
|
function snapshotSeparator(element) {
|
|
635
634
|
return {
|
|
636
635
|
role: "separator",
|
|
@@ -638,9 +637,8 @@
|
|
|
638
637
|
children: snapshotPresentationalChildren(element)
|
|
639
638
|
};
|
|
640
639
|
}
|
|
641
|
-
|
|
642
|
-
//#
|
|
643
|
-
//#region src/browser/tab.ts
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/browser/tab.ts
|
|
644
642
|
function snapshotTab(element) {
|
|
645
643
|
return {
|
|
646
644
|
role: "tab",
|
|
@@ -652,9 +650,8 @@
|
|
|
652
650
|
children: snapshotChildren(element)
|
|
653
651
|
};
|
|
654
652
|
}
|
|
655
|
-
|
|
656
|
-
//#
|
|
657
|
-
//#region src/browser/table.ts
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/browser/table.ts
|
|
658
655
|
const SORT_TYPES = new Set([
|
|
659
656
|
"ascending",
|
|
660
657
|
"descending",
|
|
@@ -668,9 +665,8 @@
|
|
|
668
665
|
children: snapshotChildren(element)
|
|
669
666
|
};
|
|
670
667
|
}
|
|
671
|
-
|
|
672
|
-
//#
|
|
673
|
-
//#region src/browser/element.ts
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/browser/element.ts
|
|
674
670
|
const ROLE_SNAPSHOTS = {
|
|
675
671
|
button: snapshotButton,
|
|
676
672
|
checkbox: snapshotInput,
|
|
@@ -739,10 +735,8 @@
|
|
|
739
735
|
const cssStyles = window.getComputedStyle(element);
|
|
740
736
|
return cssStyles.display === "none" || cssStyles.visibility === "hidden" || cssStyles.visibility === "collapse";
|
|
741
737
|
}
|
|
742
|
-
|
|
743
|
-
//#
|
|
744
|
-
//#region src/browser-lib.ts
|
|
738
|
+
//#endregion
|
|
739
|
+
//#region src/browser-lib.ts
|
|
745
740
|
window.snapshotElement = snapshotElement;
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
})();
|
|
741
|
+
//#endregion
|
|
742
|
+
})();
|
package/dist/index.d.mts
CHANGED
|
@@ -51,7 +51,7 @@ interface OptionAttributes {
|
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
53
|
//#region src/browser/container.d.ts
|
|
54
|
-
declare const CONTAINER_ROLES: Set<"paragraph" | "list" | "listitem" | "listbox" | "article" | "banner" | "complementary" | "contentinfo" | "form" | "main" | "navigation" | "region" | "search" | "descriptionlist" | "term" | "definition" | "table" | "grid" | "rowgroup" | "row" | "rowheader" | "cell" | "gridcell" | "alert" | "menu" | "tablist" | "tabpanel">;
|
|
54
|
+
declare const CONTAINER_ROLES: Set<"paragraph" | "list" | "listitem" | "listbox" | "article" | "banner" | "complementary" | "contentinfo" | "form" | "main" | "navigation" | "region" | "search" | "descriptionlist" | "term" | "definition" | "table" | "grid" | "rowgroup" | "row" | "rowheader" | "cell" | "gridcell" | "alert" | "menu" | "tablist" | "tabpanel" | "label">;
|
|
55
55
|
interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {}
|
|
56
56
|
type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
|
|
57
57
|
//#endregion
|
|
@@ -127,7 +127,7 @@ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes
|
|
|
127
127
|
type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
|
|
128
128
|
//#endregion
|
|
129
129
|
//#region src/playwright/markdown-table.d.ts
|
|
130
|
-
interface
|
|
130
|
+
interface MarkdownTableSnapshotOptions {
|
|
131
131
|
/**
|
|
132
132
|
* Show sort indicator for column headers
|
|
133
133
|
*
|
|
@@ -138,7 +138,7 @@ interface TableSnapshotOptions {
|
|
|
138
138
|
/**
|
|
139
139
|
* Creates a Markdown table snapshot from a table or grid element
|
|
140
140
|
*/
|
|
141
|
-
declare function markdownTableSnapshot(locator: Locator, options?:
|
|
141
|
+
declare function markdownTableSnapshot(locator: Locator, options?: MarkdownTableSnapshotOptions): Promise<string>;
|
|
142
142
|
//#endregion
|
|
143
143
|
//#region src/types/snapshot.d.ts
|
|
144
144
|
type SnapshotByRole<TRole extends NodeRole> = NodeSnapshot & {
|
|
@@ -186,7 +186,7 @@ declare function filter(options: FilterOptions): Array<NodeSnapshot>;
|
|
|
186
186
|
declare function filterByRole<TRole extends NodeRole>(role: TRole, snapshots: Array<NodeSnapshot>): Array<SnapshotByRole<TRole>>;
|
|
187
187
|
//#endregion
|
|
188
188
|
//#region src/playwright/snapshot.d.ts
|
|
189
|
-
interface
|
|
189
|
+
interface SemanticSnapshotOptions {
|
|
190
190
|
/**
|
|
191
191
|
* Include only elements in the snapshot for which the specified filter returns `true`
|
|
192
192
|
*
|
|
@@ -210,23 +210,26 @@ interface ElementSnapshotOptions {
|
|
|
210
210
|
includeComboboxOptions?: boolean;
|
|
211
211
|
}
|
|
212
212
|
/**
|
|
213
|
-
*
|
|
213
|
+
* Captures a semantic snapshot of the specified locator's DOM structure.
|
|
214
214
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* @param
|
|
220
|
-
* @
|
|
215
|
+
* This function creates a raw snapshot of the DOM element identified by the locator
|
|
216
|
+
* and transforms it into a semantic representation using the provided options.
|
|
217
|
+
*
|
|
218
|
+
* @param locator - The Locator instance that identifies the DOM element to snapshot
|
|
219
|
+
* @param options - Optional configuration settings for controlling the snapshot behavior
|
|
220
|
+
* @return A promise that resolves to the semantic snapshot
|
|
221
221
|
*/
|
|
222
|
-
declare function
|
|
222
|
+
declare function semanticSnapshot(locator: Locator, options?: SemanticSnapshotOptions): Promise<unknown>;
|
|
223
223
|
/**
|
|
224
|
-
*
|
|
224
|
+
* Captures a raw snapshot of the element matched by the provided locator.
|
|
225
|
+
*
|
|
226
|
+
* Returns an array of node snapshots representing the current state of the matched elements
|
|
227
|
+
* in the DOM, including their properties, attributes, and structure.
|
|
225
228
|
*
|
|
226
|
-
* @param locator Locator
|
|
227
|
-
* @
|
|
229
|
+
* @param locator - The Locator instance used to identify the target element to snapshot
|
|
230
|
+
* @return A promise that resolves to an array of NodeSnapshot objects representing the captured element
|
|
228
231
|
*/
|
|
229
|
-
declare function
|
|
232
|
+
declare function rawSnapshot(locator: Locator): Promise<Array<NodeSnapshot>>;
|
|
230
233
|
//#endregion
|
|
231
234
|
//#region src/utils/predicates.d.ts
|
|
232
235
|
/**
|
|
@@ -244,4 +247,4 @@ declare function excludeRole<TRole extends NodeRole>(role: TRole | Array<TRole>)
|
|
|
244
247
|
*/
|
|
245
248
|
declare function getTextContent(snapshots: Array<NodeSnapshot>): string;
|
|
246
249
|
//#endregion
|
|
247
|
-
export { type ButtonSnapshot, type ColumnheaderSnapshot, type ComboboxSnapshot, type ContainerSnapshot, type DialogSnapshot, type ElementRole, type ElementSnapshot, type GroupSnapshot, type HeadingSnapshot, type ImageSnapshot, type InputSnapshot, type LinkSnapshot, type MenuitemSnapshot, type NodeRole, type NodeSnapshot, type OptionSnapshot, type ProgressbarSnapshot, type RadiogroupSnapshot, type SeparatorSnapshot, type TabSnapshot, type TextSnapshot, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot,
|
|
250
|
+
export { type ButtonSnapshot, type ColumnheaderSnapshot, type ComboboxSnapshot, type ContainerSnapshot, type DialogSnapshot, type ElementRole, type ElementSnapshot, type GroupSnapshot, type HeadingSnapshot, type ImageSnapshot, type InputSnapshot, type LinkSnapshot, type MarkdownTableSnapshotOptions, type MenuitemSnapshot, type NodeRole, type NodeSnapshot, type OptionSnapshot, type ProgressbarSnapshot, type RadiogroupSnapshot, type SeparatorSnapshot, type SnapshotByRole, type TabSnapshot, type TextSnapshot, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { markdownTable } from "markdown-table";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { packageDirectory } from "package-directory";
|
|
4
|
-
|
|
5
4
|
//#region src/utils/guards.ts
|
|
6
5
|
function isTextSnapshot(snapshot) {
|
|
7
6
|
return hasRole("text", snapshot);
|
|
@@ -14,7 +13,6 @@ function isEmpty(value) {
|
|
|
14
13
|
if (typeof value === "string" || Array.isArray(value)) return value.length === 0;
|
|
15
14
|
return Object.keys(value).length === 0;
|
|
16
15
|
}
|
|
17
|
-
|
|
18
16
|
//#endregion
|
|
19
17
|
//#region src/utils/predicates.ts
|
|
20
18
|
/**
|
|
@@ -29,7 +27,6 @@ function includeRole(role) {
|
|
|
29
27
|
function excludeRole(role) {
|
|
30
28
|
return (snapshot) => !hasRole(role, snapshot);
|
|
31
29
|
}
|
|
32
|
-
|
|
33
30
|
//#endregion
|
|
34
31
|
//#region src/utils/filter.ts
|
|
35
32
|
function filter(options) {
|
|
@@ -64,22 +61,23 @@ function filterByRole(role, snapshots) {
|
|
|
64
61
|
function hasChildren(snapshot) {
|
|
65
62
|
return "children" in snapshot && snapshot.children.length > 0;
|
|
66
63
|
}
|
|
67
|
-
|
|
68
64
|
//#endregion
|
|
69
65
|
//#region src/utils/text.ts
|
|
70
66
|
/**
|
|
71
67
|
* Aggregates and normalizes the text content of all provided snapshots
|
|
72
68
|
*/
|
|
73
69
|
function getTextContent(snapshots) {
|
|
74
|
-
return
|
|
70
|
+
return mergeTexts(filter({
|
|
75
71
|
snapshots,
|
|
76
72
|
predicate: isTextSnapshot
|
|
77
|
-
})
|
|
73
|
+
}));
|
|
78
74
|
}
|
|
79
75
|
function normalizeText(text) {
|
|
80
76
|
return text.replaceAll(/\s+/g, " ").trim();
|
|
81
77
|
}
|
|
82
|
-
|
|
78
|
+
function mergeTexts(texts) {
|
|
79
|
+
return normalizeText(texts.map((textNode) => textNode.name).join(" "));
|
|
80
|
+
}
|
|
83
81
|
//#endregion
|
|
84
82
|
//#region src/playwright/proxy.ts
|
|
85
83
|
var ElementSnapshotProxy = class {
|
|
@@ -107,10 +105,9 @@ var ElementSnapshotProxy = class {
|
|
|
107
105
|
return packageDir;
|
|
108
106
|
}
|
|
109
107
|
};
|
|
110
|
-
|
|
111
108
|
//#endregion
|
|
112
|
-
//#region src/playwright/transformer.ts
|
|
113
|
-
var
|
|
109
|
+
//#region src/playwright/semantic-snapshot-transformer.ts
|
|
110
|
+
var SemanticSnapshotTransformer = class {
|
|
114
111
|
filter;
|
|
115
112
|
recurseFilter;
|
|
116
113
|
includeComboboxOptions;
|
|
@@ -206,33 +203,34 @@ var ElementSnapshotTransformer = class {
|
|
|
206
203
|
return { [role]: content };
|
|
207
204
|
}
|
|
208
205
|
};
|
|
209
|
-
|
|
210
206
|
//#endregion
|
|
211
207
|
//#region src/playwright/snapshot.ts
|
|
212
208
|
/**
|
|
213
|
-
*
|
|
209
|
+
* Captures a semantic snapshot of the specified locator's DOM structure.
|
|
210
|
+
*
|
|
211
|
+
* This function creates a raw snapshot of the DOM element identified by the locator
|
|
212
|
+
* and transforms it into a semantic representation using the provided options.
|
|
214
213
|
*
|
|
215
|
-
* @
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* ```
|
|
219
|
-
* @param locator Locator for the element to snapshot
|
|
220
|
-
* @experimental
|
|
214
|
+
* @param locator - The Locator instance that identifies the DOM element to snapshot
|
|
215
|
+
* @param options - Optional configuration settings for controlling the snapshot behavior
|
|
216
|
+
* @return A promise that resolves to the semantic snapshot
|
|
221
217
|
*/
|
|
222
|
-
async function
|
|
223
|
-
const snapshot = await
|
|
224
|
-
return new
|
|
218
|
+
async function semanticSnapshot(locator, options) {
|
|
219
|
+
const snapshot = await rawSnapshot(locator);
|
|
220
|
+
return new SemanticSnapshotTransformer(options).transform(snapshot);
|
|
225
221
|
}
|
|
226
222
|
/**
|
|
227
|
-
*
|
|
223
|
+
* Captures a raw snapshot of the element matched by the provided locator.
|
|
224
|
+
*
|
|
225
|
+
* Returns an array of node snapshots representing the current state of the matched elements
|
|
226
|
+
* in the DOM, including their properties, attributes, and structure.
|
|
228
227
|
*
|
|
229
|
-
* @param locator Locator
|
|
230
|
-
* @
|
|
228
|
+
* @param locator - The Locator instance used to identify the target element to snapshot
|
|
229
|
+
* @return A promise that resolves to an array of NodeSnapshot objects representing the captured element
|
|
231
230
|
*/
|
|
232
|
-
async function
|
|
231
|
+
async function rawSnapshot(locator) {
|
|
233
232
|
return await new ElementSnapshotProxy(locator.page()).snapshotElement(locator);
|
|
234
233
|
}
|
|
235
|
-
|
|
236
234
|
//#endregion
|
|
237
235
|
//#region src/playwright/markdown-table.ts
|
|
238
236
|
const tableRoles = ["table", "grid"];
|
|
@@ -250,7 +248,7 @@ const sortIndicators = {
|
|
|
250
248
|
* Creates a Markdown table snapshot from a table or grid element
|
|
251
249
|
*/
|
|
252
250
|
async function markdownTableSnapshot(locator, options = {}) {
|
|
253
|
-
const { columnHeaders, rows } = parseTable(await
|
|
251
|
+
const { columnHeaders, rows } = parseTable(await rawSnapshot(locator), options);
|
|
254
252
|
return markdownTable([columnHeaders, ...rows]);
|
|
255
253
|
}
|
|
256
254
|
function parseTable(snapshot, options) {
|
|
@@ -283,6 +281,5 @@ function getCellTexts(row) {
|
|
|
283
281
|
snapshots: row.children
|
|
284
282
|
}).map((cell) => getTextContent(cell.children));
|
|
285
283
|
}
|
|
286
|
-
|
|
287
284
|
//#endregion
|
|
288
|
-
export { excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot,
|
|
285
|
+
export { excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cronn/element-snapshot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Element snapshots for Playwright",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/cronn/file-snapshots/issues"
|
|
@@ -12,19 +12,19 @@
|
|
|
12
12
|
"url": "git+https://github.com/cronn/file-snapshots.git",
|
|
13
13
|
"directory": "packages/element-snapshot"
|
|
14
14
|
},
|
|
15
|
-
"homepage": "https://github.
|
|
15
|
+
"homepage": "https://cronn.github.io/file-snapshots/playwright/element-snapshots/",
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": "^20 || ^22 || >=24"
|
|
18
18
|
},
|
|
19
19
|
"devEngines": {
|
|
20
20
|
"runtime": {
|
|
21
21
|
"name": "node",
|
|
22
|
-
"version": ">=24.
|
|
22
|
+
"version": ">=24.14.1 <25",
|
|
23
23
|
"onFail": "error"
|
|
24
24
|
},
|
|
25
25
|
"packageManager": {
|
|
26
26
|
"name": "pnpm",
|
|
27
|
-
"version": ">=10.
|
|
27
|
+
"version": ">=10.33.0 <11",
|
|
28
28
|
"onFail": "error"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
@@ -47,22 +47,22 @@
|
|
|
47
47
|
"@arethetypeswrong/core": "0.18.2",
|
|
48
48
|
"@playwright/test": "1.58.2",
|
|
49
49
|
"@trivago/prettier-plugin-sort-imports": "6.0.2",
|
|
50
|
-
"@types/node": "24.
|
|
51
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
52
|
-
"@vitest/coverage-v8": "4.0
|
|
53
|
-
"eslint": "10.0
|
|
50
|
+
"@types/node": "24.12.0",
|
|
51
|
+
"@typescript/native-preview": "7.0.0-dev.20260323.1",
|
|
52
|
+
"@vitest/coverage-v8": "4.1.0",
|
|
53
|
+
"eslint": "10.1.0",
|
|
54
54
|
"eslint-config-prettier": "10.1.8",
|
|
55
55
|
"eslint-plugin-check-file": "3.3.1",
|
|
56
56
|
"eslint-plugin-unused-imports": "4.4.1",
|
|
57
57
|
"prettier": "3.8.1",
|
|
58
58
|
"publint": "0.3.18",
|
|
59
|
-
"tsdown": "0.
|
|
60
|
-
"typescript-eslint": "8.
|
|
61
|
-
"vitest": "4.0
|
|
62
|
-
"@cronn/playwright-file-snapshots": "1.0.
|
|
59
|
+
"tsdown": "0.21.4",
|
|
60
|
+
"typescript-eslint": "8.57.1",
|
|
61
|
+
"vitest": "4.1.0",
|
|
62
|
+
"@cronn/playwright-file-snapshots": "1.0.2",
|
|
63
|
+
"@cronn/shared-configs": "0.0.0",
|
|
63
64
|
"@cronn/test-utils": "0.0.0",
|
|
64
|
-
"@cronn/vitest-file-snapshots": "1.0.
|
|
65
|
-
"@cronn/shared-configs": "0.0.0"
|
|
65
|
+
"@cronn/vitest-file-snapshots": "1.0.2"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@playwright/test": "^1.55"
|