@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 CHANGED
@@ -1,52 +1,52 @@
1
- # Element Snapshots
1
+ # Playwright Element Snapshots
2
2
 
3
- Element Snapshots are a custom snapshot implementation inspired by [Playwright's ARIA Snapshots](https://playwright.dev/docs/aria-snapshots#aria-snapshots) and the [Accessibility Object Model](https://wicg.github.io/aom/explainer.html). They cover additional properties, e.g. validation attributes like `required` or `invalid` on form inputs, and are optimized to be serialized as JSON.
3
+ Element snapshots for Playwright.
4
4
 
5
- Element Snapshots are designed to be used in combination with [playwright-file-snapshots](../playwright-file-snapshots/README.md) for writing Playwright tests.
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
- ### Adding the library to your project
12
+ ### Installation
13
13
 
14
14
  ```shell
15
- npm install -D @cronn/element-snapshot
15
+ pnpm add -D @cronn/element-snapshot
16
16
  ```
17
17
 
18
18
  ```shell
19
- yarn add -D @cronn/element-snapshot
19
+ npm install -D @cronn/element-snapshot
20
20
  ```
21
21
 
22
22
  ```shell
23
- pnpm add -D @cronn/element-snapshot
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 { snapshotElement } from "@cronn/element-snapshot";
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 expect(snapshotElement(page.getByRole("main"))).toMatchJsonFile();
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
- Element Snapshot Example:
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. |
@@ -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
- //#endregion
27
- //#region src/browser/children.ts
28
- function snapshotChildren(node, excludedNodes = []) {
29
- return Array.from(node.childNodes).map((childNode) => snapshotNodeRecursive(childNode, excludedNodes)).filter((childSnapshot) => childSnapshot !== null).flat();
23
+ //#endregion
24
+ //#region src/utils/factories.ts
25
+ function textSnapshot(name) {
26
+ return {
27
+ role: "text",
28
+ name
29
+ };
30
30
  }
31
-
32
- //#endregion
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
- //#endregion
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
- //#endregion
68
- //#region src/utils/text.ts
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 normalizeText(filter({
70
+ return mergeTexts(filter({
74
71
  snapshots,
75
72
  predicate: isTextSnapshot
76
- }).map((textNode) => textNode.name).join(" "));
73
+ }));
77
74
  }
78
75
  function normalizeText(text) {
79
76
  return text.replaceAll(/\s+/g, " ").trim();
80
77
  }
81
-
82
- //#endregion
83
- //#region src/browser/text.ts
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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, false),
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
744
- //#region src/browser-lib.ts
738
+ //#endregion
739
+ //#region src/browser-lib.ts
745
740
  window.snapshotElement = snapshotElement;
746
-
747
- //#endregion
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 TableSnapshotOptions {
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?: TableSnapshotOptions): Promise<string>;
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 ElementSnapshotOptions {
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
- * Creates an Element Snapshot of the specified element
213
+ * Captures a semantic snapshot of the specified locator's DOM structure.
214
214
  *
215
- * @example
216
- * ```ts
217
- * await expect(snapshotElement(page.getByRole("main"))).toMatchJsonSnapshot();
218
- * ```
219
- * @param locator Locator for the element to snapshot
220
- * @experimental
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 snapshotElement(locator: Locator, options?: ElementSnapshotOptions): Promise<unknown>;
222
+ declare function semanticSnapshot(locator: Locator, options?: SemanticSnapshotOptions): Promise<unknown>;
223
223
  /**
224
- * Creates an Element Snapshot of the specified element, returning the raw snapshot data for custom transformations
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 for the element to snapshot
227
- * @experimental
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 snapshotElementRaw(locator: Locator): Promise<Array<NodeSnapshot>>;
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, snapshotElement, snapshotElementRaw };
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 normalizeText(filter({
70
+ return mergeTexts(filter({
75
71
  snapshots,
76
72
  predicate: isTextSnapshot
77
- }).map((textNode) => textNode.name).join(" "));
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 ElementSnapshotTransformer = class {
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
- * Creates an Element Snapshot of the specified element
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
- * @example
216
- * ```ts
217
- * await expect(snapshotElement(page.getByRole("main"))).toMatchJsonSnapshot();
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 snapshotElement(locator, options) {
223
- const snapshot = await snapshotElementRaw(locator);
224
- return new ElementSnapshotTransformer(options).transform(snapshot);
218
+ async function semanticSnapshot(locator, options) {
219
+ const snapshot = await rawSnapshot(locator);
220
+ return new SemanticSnapshotTransformer(options).transform(snapshot);
225
221
  }
226
222
  /**
227
- * Creates an Element Snapshot of the specified element, returning the raw snapshot data for custom transformations
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 for the element to snapshot
230
- * @experimental
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 snapshotElementRaw(locator) {
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 snapshotElementRaw(locator), options);
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, snapshotElement, snapshotElementRaw };
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.14.1",
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.com/cronn/file-snapshots",
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.13 <25",
22
+ "version": ">=24.14.1 <25",
23
23
  "onFail": "error"
24
24
  },
25
25
  "packageManager": {
26
26
  "name": "pnpm",
27
- "version": ">=10.28.2 <11",
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.11.0",
51
- "@typescript/native-preview": "7.0.0-dev.20260302.1",
52
- "@vitest/coverage-v8": "4.0.18",
53
- "eslint": "10.0.2",
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.20.3",
60
- "typescript-eslint": "8.56.1",
61
- "vitest": "4.0.18",
62
- "@cronn/playwright-file-snapshots": "1.0.1",
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.1",
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"