@cronn/element-snapshot 0.13.0 → 0.14.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
@@ -25,6 +25,12 @@ pnpm add -D @cronn/element-snapshot
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
+
28
34
  ### General-Purpose Snapshots
29
35
 
30
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.
@@ -113,3 +119,13 @@ function transformToMarkdownTable(snapshot: Array<NodeSnapshot>): string {
113
119
  // transform table snapshot to markdown table
114
120
  }
115
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. |
@@ -32,23 +32,28 @@
32
32
  //#endregion
33
33
  //#region src/utils/guards.ts
34
34
  function isTextSnapshot(snapshot) {
35
- return snapshot.role === "text";
35
+ return hasRole("text", snapshot);
36
+ }
37
+ function hasRole(role, snapshot) {
38
+ if (typeof role === "string") return role === snapshot.role;
39
+ return role.includes(snapshot.role);
36
40
  }
37
41
 
38
42
  //#endregion
39
- //#region src/utils/snapshot.ts
40
- function filterSnapshots(options) {
41
- const { filter, snapshots, recurse = false } = options;
43
+ //#region src/utils/filter.ts
44
+ function filter(options) {
45
+ const { predicate, snapshots, recurse = false } = options;
42
46
  return snapshots.flatMap((snapshot) => {
43
- const isFilteredIn = filter(snapshot);
47
+ const isFilteredIn = predicate(snapshot);
44
48
  if (isFilteredIn && !recurse) return snapshot;
45
49
  if (!isFilteredIn && recurse) return [];
46
- const filteredChildren = hasChildren(snapshot) ? filterSnapshots({
47
- filter,
50
+ const filteredChildren = hasChildren(snapshot) ? filter({
51
+ predicate,
48
52
  snapshots: snapshot.children ?? [],
49
53
  recurse
50
54
  }) : [];
51
55
  if (!isFilteredIn) return filteredChildren;
56
+ if (isTextSnapshot(snapshot)) return snapshot;
52
57
  return {
53
58
  ...snapshot,
54
59
  children: filteredChildren
@@ -59,6 +64,21 @@
59
64
  return "children" in snapshot && snapshot.children.length > 0;
60
65
  }
61
66
 
67
+ //#endregion
68
+ //#region src/utils/text.ts
69
+ /**
70
+ * Aggregates and normalizes the text content of all provided snapshots
71
+ */
72
+ function getTextContent(snapshots) {
73
+ return normalizeText(filter({
74
+ snapshots,
75
+ predicate: isTextSnapshot
76
+ }).map((textNode) => textNode.name).join(" "));
77
+ }
78
+ function normalizeText(text) {
79
+ return text.replaceAll(/\s+/g, " ").trim();
80
+ }
81
+
62
82
  //#endregion
63
83
  //#region src/browser/text.ts
64
84
  function snapshotTextNode(textNode) {
@@ -81,14 +101,8 @@
81
101
  function resolveAccessibleTextContent(node, excludedNodes = []) {
82
102
  const children = snapshotChildren(node, excludedNodes);
83
103
  if (children === void 0) return;
84
- const normalizedText = normalizeText(filterSnapshots({
85
- snapshots: children,
86
- filter: isTextSnapshot
87
- }).map((textNode) => textNode.name).join(" "));
88
- return normalizedText.length > 0 ? normalizedText : void 0;
89
- }
90
- function normalizeText(text) {
91
- return text.replaceAll(/\s+/g, " ").trim();
104
+ const textContent = getTextContent(children);
105
+ return textContent.length > 0 ? textContent : void 0;
92
106
  }
93
107
 
94
108
  //#endregion
@@ -641,7 +655,7 @@
641
655
 
642
656
  //#endregion
643
657
  //#region src/browser/table.ts
644
- const SORT_ATTRIBUTES = new Set([
658
+ const SORT_TYPES = new Set([
645
659
  "ascending",
646
660
  "descending",
647
661
  "other"
@@ -650,7 +664,7 @@
650
664
  return {
651
665
  role: "columnheader",
652
666
  name: resolveAccessibleName(element),
653
- attributes: { sort: enumAttribute(element.ariaSort, SORT_ATTRIBUTES) },
667
+ attributes: { sort: enumAttribute(element.ariaSort, SORT_TYPES) },
654
668
  children: snapshotChildren(element)
655
669
  };
656
670
  }
package/dist/index.d.mts CHANGED
@@ -102,10 +102,10 @@ interface TabAttributes extends DisableableAttributes, SelectableAttributes {}
102
102
  //#region src/browser/table.d.ts
103
103
  interface ColumnheaderSnapshot extends GenericElementSnapshot<"columnheader", ColumnheaderAttributes> {}
104
104
  interface ColumnheaderAttributes {
105
- sort?: SortAttribute;
105
+ sort?: SortType;
106
106
  }
107
- declare const SORT_ATTRIBUTES: Set<"ascending" | "descending" | "other">;
108
- type SortAttribute = SetValues<typeof SORT_ATTRIBUTES>;
107
+ declare const SORT_TYPES: Set<"ascending" | "descending" | "other">;
108
+ type SortType = SetValues<typeof SORT_TYPES>;
109
109
  //#endregion
110
110
  //#region src/browser/text.d.ts
111
111
  interface TextSnapshot {
@@ -126,21 +126,50 @@ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes
126
126
  }
127
127
  type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
128
128
  //#endregion
129
- //#region src/utils/snapshot.d.ts
130
- type SnapshotFilter = (snapshot: NodeSnapshot) => boolean;
131
- type GuardedSnapshotFilter<TSnapshot extends NodeSnapshot> = (snapshot: NodeSnapshot) => snapshot is TSnapshot;
132
- interface SnapshotFilterOptions {
133
- filter: SnapshotFilter;
134
- snapshots: Array<NodeSnapshot>;
135
- recurse: boolean;
129
+ //#region src/types/snapshot.d.ts
130
+ type SnapshotByRole<TRole extends NodeRole> = NodeSnapshot & {
131
+ role: TRole;
132
+ };
133
+ //#endregion
134
+ //#region src/utils/filter.d.ts
135
+ type FilterPredicate = (snapshot: NodeSnapshot) => boolean;
136
+ type GuardedFilterPredicate<TSnapshot extends NodeSnapshot> = (snapshot: NodeSnapshot) => snapshot is TSnapshot;
137
+ interface FilterOptions extends FilterBaseOptions {
138
+ /**
139
+ * Include only elements in the snapshot for which the specified predicate returns `true`
140
+ */
141
+ predicate: FilterPredicate;
142
+ }
143
+ interface GuardedFilterOptions<TSnapshot extends NodeSnapshot> extends FilterBaseOptions {
144
+ /**
145
+ * Include only elements in the snapshot for which the specified predicate returns `true`
146
+ */
147
+ predicate: GuardedFilterPredicate<TSnapshot>;
136
148
  }
137
- interface GuardedSnapshotFilterOptions<TSnapshot extends NodeSnapshot> {
138
- filter: GuardedSnapshotFilter<TSnapshot>;
149
+ interface FilterBaseOptions {
150
+ /**
151
+ * Array of snapshots to filter.
152
+ */
139
153
  snapshots: Array<NodeSnapshot>;
154
+ /**
155
+ * Recursively apply specified filter to children of filtered elements
156
+ *
157
+ * By default, recursion ends when the filter returns `true` for an element.
158
+ * Should be `true` for filters intended to remove specific elements recursively.
159
+ *
160
+ * @default false
161
+ */
140
162
  recurse?: boolean;
141
163
  }
142
- declare function filterSnapshots<TSnapshot extends NodeSnapshot = NodeSnapshot>(options: GuardedSnapshotFilterOptions<TSnapshot>): Array<TSnapshot>;
143
- declare function filterSnapshots(options: SnapshotFilterOptions): Array<NodeSnapshot>;
164
+ /**
165
+ * Filters node snapshots based on the provided predicate function
166
+ */
167
+ declare function filter<TSnapshot extends NodeSnapshot = NodeSnapshot>(options: GuardedFilterOptions<TSnapshot>): Array<TSnapshot>;
168
+ declare function filter(options: FilterOptions): Array<NodeSnapshot>;
169
+ /**
170
+ * Filters node snapshots by a specific role
171
+ */
172
+ declare function filterByRole<TRole extends NodeRole>(role: TRole, snapshots: Array<NodeSnapshot>): Array<SnapshotByRole<TRole>>;
144
173
  //#endregion
145
174
  //#region src/playwright/snapshot.d.ts
146
175
  interface ElementSnapshotOptions {
@@ -149,7 +178,7 @@ interface ElementSnapshotOptions {
149
178
  *
150
179
  * @default () => true
151
180
  */
152
- filter?: SnapshotFilter;
181
+ filter?: FilterPredicate;
153
182
  /**
154
183
  * Recursively apply specified filter to children of filtered elements
155
184
  *
@@ -185,4 +214,20 @@ declare function snapshotElement(locator: Locator, options?: ElementSnapshotOpti
185
214
  */
186
215
  declare function snapshotElementRaw(locator: Locator): Promise<Array<NodeSnapshot>>;
187
216
  //#endregion
188
- 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, filterSnapshots, snapshotElement, snapshotElementRaw };
217
+ //#region src/utils/predicates.d.ts
218
+ /**
219
+ * Include only elements with the specified role
220
+ */
221
+ declare function includeRole<TRole extends NodeRole>(role: TRole | Array<TRole>): GuardedFilterPredicate<SnapshotByRole<TRole>>;
222
+ /**
223
+ * Exclude elements with the specified role
224
+ */
225
+ declare function excludeRole<TRole extends NodeRole>(role: TRole | Array<TRole>): GuardedFilterPredicate<SnapshotByRole<TRole>>;
226
+ //#endregion
227
+ //#region src/utils/text.d.ts
228
+ /**
229
+ * Aggregates and normalizes the text content of all provided snapshots
230
+ */
231
+ declare function getTextContent(snapshots: Array<NodeSnapshot>): string;
232
+ //#endregion
233
+ 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, snapshotElement, snapshotElementRaw };
package/dist/index.mjs CHANGED
@@ -31,7 +31,11 @@ var ElementSnapshotProxy = class {
31
31
  //#endregion
32
32
  //#region src/utils/guards.ts
33
33
  function isTextSnapshot(snapshot) {
34
- return snapshot.role === "text";
34
+ return hasRole("text", snapshot);
35
+ }
36
+ function hasRole(role, snapshot) {
37
+ if (typeof role === "string") return role === snapshot.role;
38
+ return role.includes(snapshot.role);
35
39
  }
36
40
  function isEmpty(value) {
37
41
  if (typeof value === "string" || Array.isArray(value)) return value.length === 0;
@@ -39,25 +43,51 @@ function isEmpty(value) {
39
43
  }
40
44
 
41
45
  //#endregion
42
- //#region src/utils/snapshot.ts
43
- function filterSnapshots(options) {
44
- const { filter, snapshots, recurse = false } = options;
46
+ //#region src/utils/predicates.ts
47
+ /**
48
+ * Include only elements with the specified role
49
+ */
50
+ function includeRole(role) {
51
+ return (snapshot) => hasRole(role, snapshot);
52
+ }
53
+ /**
54
+ * Exclude elements with the specified role
55
+ */
56
+ function excludeRole(role) {
57
+ return (snapshot) => !hasRole(role, snapshot);
58
+ }
59
+
60
+ //#endregion
61
+ //#region src/utils/filter.ts
62
+ function filter(options) {
63
+ const { predicate, snapshots, recurse = false } = options;
45
64
  return snapshots.flatMap((snapshot) => {
46
- const isFilteredIn = filter(snapshot);
65
+ const isFilteredIn = predicate(snapshot);
47
66
  if (isFilteredIn && !recurse) return snapshot;
48
67
  if (!isFilteredIn && recurse) return [];
49
- const filteredChildren = hasChildren(snapshot) ? filterSnapshots({
50
- filter,
68
+ const filteredChildren = hasChildren(snapshot) ? filter({
69
+ predicate,
51
70
  snapshots: snapshot.children ?? [],
52
71
  recurse
53
72
  }) : [];
54
73
  if (!isFilteredIn) return filteredChildren;
74
+ if (isTextSnapshot(snapshot)) return snapshot;
55
75
  return {
56
76
  ...snapshot,
57
77
  children: filteredChildren
58
78
  };
59
79
  });
60
80
  }
81
+ /**
82
+ * Filters node snapshots by a specific role
83
+ */
84
+ function filterByRole(role, snapshots) {
85
+ return filter({
86
+ predicate: includeRole(role),
87
+ recurse: false,
88
+ snapshots
89
+ });
90
+ }
61
91
  function hasChildren(snapshot) {
62
92
  return "children" in snapshot && snapshot.children.length > 0;
63
93
  }
@@ -81,8 +111,8 @@ var ElementSnapshotTransformer = class {
81
111
  }
82
112
  filterSnapshots(snapshots) {
83
113
  if (this.filter === void 0) return snapshots;
84
- return filterSnapshots({
85
- filter: this.filter,
114
+ return filter({
115
+ predicate: this.filter,
86
116
  snapshots,
87
117
  recurse: this.recurseFilter
88
118
  });
@@ -188,4 +218,19 @@ async function snapshotElementRaw(locator) {
188
218
  }
189
219
 
190
220
  //#endregion
191
- export { filterSnapshots, snapshotElement, snapshotElementRaw };
221
+ //#region src/utils/text.ts
222
+ /**
223
+ * Aggregates and normalizes the text content of all provided snapshots
224
+ */
225
+ function getTextContent(snapshots) {
226
+ return normalizeText(filter({
227
+ snapshots,
228
+ predicate: isTextSnapshot
229
+ }).map((textNode) => textNode.name).join(" "));
230
+ }
231
+ function normalizeText(text) {
232
+ return text.replaceAll(/\s+/g, " ").trim();
233
+ }
234
+
235
+ //#endregion
236
+ export { excludeRole, filter, filterByRole, getTextContent, includeRole, snapshotElement, snapshotElementRaw };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cronn/element-snapshot",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Element snapshots for Playwright",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cronn/file-snapshots/issues"
@@ -40,26 +40,29 @@
40
40
  "dist"
41
41
  ],
42
42
  "dependencies": {
43
+ "markdown-table": "3.0.4",
43
44
  "package-directory": "8.2.0"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@arethetypeswrong/core": "0.18.2",
47
48
  "@playwright/test": "1.58.2",
48
49
  "@trivago/prettier-plugin-sort-imports": "6.0.2",
49
- "@typescript/native-preview": "7.0.0-dev.20260218.1",
50
- "@types/node": "24.10.13",
50
+ "@types/node": "24.11.0",
51
+ "@typescript/native-preview": "7.0.0-dev.20260302.1",
51
52
  "@vitest/coverage-v8": "4.0.18",
52
- "eslint": "10.0.1",
53
+ "eslint": "10.0.2",
53
54
  "eslint-config-prettier": "10.1.8",
55
+ "eslint-plugin-check-file": "3.3.1",
54
56
  "eslint-plugin-unused-imports": "4.4.1",
55
57
  "prettier": "3.8.1",
56
- "publint": "0.3.17",
58
+ "publint": "0.3.18",
57
59
  "tsdown": "0.20.3",
58
- "typescript-eslint": "8.56.0",
60
+ "typescript-eslint": "8.56.1",
59
61
  "vitest": "4.0.18",
60
- "@cronn/playwright-file-snapshots": "1.0.0",
61
62
  "@cronn/shared-configs": "0.0.0",
62
- "@cronn/test-utils": "0.0.0"
63
+ "@cronn/playwright-file-snapshots": "1.0.1",
64
+ "@cronn/test-utils": "0.0.0",
65
+ "@cronn/vitest-file-snapshots": "1.0.1"
63
66
  },
64
67
  "peerDependencies": {
65
68
  "@playwright/test": "^1.55"