@cronn/element-snapshot 0.28.0 → 0.29.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/dist/index.d.mts CHANGED
@@ -158,7 +158,7 @@ type NodeSnapshot = ElementSnapshot | TextSnapshot;
158
158
  type SnapshotByRole<TRole extends NodeRole> = NodeSnapshot & {
159
159
  role: TRole;
160
160
  };
161
- interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes = Record<string, unknown>> {
161
+ interface GenericElementSnapshot<TRole extends ElementRole = ElementRole, TAttributes extends object = object> {
162
162
  role: TRole;
163
163
  name?: string;
164
164
  attributes: TAttributes;
@@ -166,6 +166,15 @@ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes
166
166
  }
167
167
  type ElementSnapshot = ButtonSnapshot | CellSnapshot | ColumnHeaderSnapshot | ComboboxSnapshot | ContainerSnapshot | DialogSnapshot | GroupSnapshot | HeadingSnapshot | ImageSnapshot | InputSnapshot | LinkSnapshot | OptionSnapshot | ProgressbarSnapshot | RegionSnapshot | TabSnapshot | MenuItemSnapshot | RadioGroupSnapshot | SeparatorSnapshot | TreeItemSnapshot;
168
168
  //#endregion
169
+ //#region src/types/transformer.d.ts
170
+ type SnapshotTransformer = (snapshot: NodeSnapshot) => unknown;
171
+ type RoleBasedSnapshotTransformer<TRole extends NodeRole = NodeRole> = (snapshot: SnapshotByRole<TRole>, context: SnapshotTransformerContext) => unknown;
172
+ type DefaultSnapshotTransformer = (snapshot: NodeSnapshot, context: SnapshotTransformerContext) => unknown;
173
+ interface SnapshotTransformerContext {
174
+ transform: SnapshotTransformer;
175
+ }
176
+ type RoleBasedSnapshotTransformers = { [TRole in NodeRole]?: RoleBasedSnapshotTransformer<TRole>; };
177
+ //#endregion
169
178
  //#region src/utils/filter.d.ts
170
179
  type FilterPredicate = (snapshot: NodeSnapshot) => boolean;
171
180
  type GuardedFilterPredicate<TSnapshot extends NodeSnapshot> = (snapshot: NodeSnapshot) => snapshot is TSnapshot;
@@ -224,11 +233,29 @@ interface SemanticSnapshotOptions {
224
233
  */
225
234
  recurseFilter?: boolean;
226
235
  /**
227
- * Include combobox options in the snapshot
236
+ * Replace the default transformation for specific roles
228
237
  *
229
- * @default false
238
+ * Transformers are keyed by role and applied after the filter, but before
239
+ * the default serialization. A transformer may return any JSON-serializable
240
+ * value, which is written to the snapshot verbatim. The `transform` function
241
+ * of its context applies the default transformation to untransformed
242
+ * children.
243
+ *
244
+ * Entries override the built-in transformers for the same role.
245
+ *
246
+ * @default { combobox: comboboxTransformer() }
247
+ */
248
+ transformers?: RoleBasedSnapshotTransformers;
249
+ /**
250
+ * Replace the default transformation for all roles
251
+ *
252
+ * Applied to every snapshot passed to the default transformation, including
253
+ * the ones a transformer delegates via the `transform` function of its
254
+ * context. Transformers registered for a role take precedence.
255
+ *
256
+ * @default semanticSnapshotTransformer
230
257
  */
231
- includeComboboxOptions?: boolean;
258
+ defaultTransformer?: DefaultSnapshotTransformer;
232
259
  }
233
260
  /**
234
261
  * Captures a semantic snapshot of the specified locator's DOM structure.
@@ -269,6 +296,23 @@ interface MatchMarkdownTableSnapshotFileOptions extends Omit<PlaywrightMatchText
269
296
  */
270
297
  declare function defineElementSnapshotMatchers(config?: PlaywrightValidationFileMatcherConfig): Expect<ElementSnapshotMatchers>;
271
298
  //#endregion
299
+ //#region src/transformers/combobox-transformer.d.ts
300
+ interface ComboboxTransformerOptions {
301
+ /**
302
+ * Include combobox options in the snapshot
303
+ *
304
+ * @default false
305
+ */
306
+ includeOptions?: boolean;
307
+ }
308
+ /**
309
+ * Returns a transformer controlling whether the options of a combobox are
310
+ * included in the snapshot
311
+ *
312
+ * Empty option lists are always omitted.
313
+ */
314
+ declare function comboboxTransformer(options?: ComboboxTransformerOptions): RoleBasedSnapshotTransformer<"combobox">;
315
+ //#endregion
272
316
  //#region src/utils/predicates.d.ts
273
317
  /**
274
318
  * Include only elements with the specified role
@@ -285,4 +329,4 @@ declare function excludeRole<TRole extends NodeRole>(role: TRole | Array<TRole>)
285
329
  */
286
330
  declare function getTextContent(snapshots: Array<NodeSnapshot>): string;
287
331
  //#endregion
288
- export { type ButtonSnapshot, type CellSnapshot, type ColumnHeaderSnapshot, type ComboboxSnapshot, type ContainerSnapshot, type DialogSnapshot, type ElementRole, type ElementSnapshot, type ElementSnapshotMatchers, type GroupSnapshot, type HeadingSnapshot, type ImageSnapshot, type InputSnapshot, type LinkSnapshot, type MarkdownTableSnapshotOptions, type MatchMarkdownTableSnapshotFileOptions, type MatchSemanticSnapshotFileOptions, type MenuItemSnapshot, type NodeRole, type NodeSnapshot, type OptionSnapshot, type ProgressbarSnapshot, type RadioGroupSnapshot, type RegionSnapshot, type SemanticSnapshotOptions, type SeparatorSnapshot, type SnapshotByRole, type TabSnapshot, type TextSnapshot, type TreeItemSnapshot, defineElementSnapshotMatchers, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
332
+ export { type ButtonSnapshot, type CellSnapshot, type ColumnHeaderSnapshot, type ComboboxSnapshot, type ComboboxTransformerOptions, type ContainerSnapshot, type DefaultSnapshotTransformer, type DialogSnapshot, type ElementRole, type ElementSnapshot, type ElementSnapshotMatchers, type GroupSnapshot, type HeadingSnapshot, type ImageSnapshot, type InputSnapshot, type LinkSnapshot, type MarkdownTableSnapshotOptions, type MatchMarkdownTableSnapshotFileOptions, type MatchSemanticSnapshotFileOptions, type MenuItemSnapshot, type NodeRole, type NodeSnapshot, type OptionSnapshot, type ProgressbarSnapshot, type RadioGroupSnapshot, type RegionSnapshot, type RoleBasedSnapshotTransformer, type RoleBasedSnapshotTransformers, type SemanticSnapshotOptions, type SeparatorSnapshot, type SnapshotByRole, type SnapshotTransformer, type SnapshotTransformerContext, type TabSnapshot, type TextSnapshot, type TreeItemSnapshot, comboboxTransformer, defineElementSnapshotMatchers, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
package/dist/index.mjs CHANGED
@@ -91,19 +91,106 @@ var ElementSnapshotProxy = class {
91
91
  }
92
92
  };
93
93
  //#endregion
94
- //#region src/playwright/semantic-snapshot-transformer.ts
95
- var SemanticSnapshotTransformer = class {
94
+ //#region src/transformers/semantic-snapshot-transformer.ts
95
+ function semanticSnapshotTransformer(snapshot, context) {
96
+ if (snapshot.role === "text") return snapshot.name;
97
+ return transformElementSnapshot(snapshot, context.transform);
98
+ }
99
+ function transformElementSnapshot(snapshot, transformRecursive) {
100
+ const normalizedSnapshot = normalizeElementSnapshot(snapshot, transformRecursive);
101
+ if (hasOnlyRole(normalizedSnapshot)) {
102
+ const { role } = normalizedSnapshot;
103
+ return transformedSnapshot(role, "");
104
+ }
105
+ if (hasOnlyName(normalizedSnapshot)) {
106
+ const { role, name } = normalizedSnapshot;
107
+ return transformedSnapshot(role, name);
108
+ }
109
+ if (hasOnlyChildren(normalizedSnapshot)) {
110
+ const { role, children } = normalizedSnapshot;
111
+ return transformedSnapshot(role, children.length === 1 ? children.at(0) : children);
112
+ }
113
+ const { role, name, attributes, children } = normalizedSnapshot;
114
+ return transformedSnapshot(role, name, {
115
+ ...attributes,
116
+ children: children.length === 0 ? void 0 : children
117
+ });
118
+ }
119
+ function normalizeElementSnapshot(snapshot, transformRecursive) {
120
+ const normalizedName = snapshot.name ?? "";
121
+ const transformedAttributes = transformAttributes(snapshot.attributes ?? {});
122
+ const transformedChildren = (snapshot.children ?? []).map(transformRecursive);
123
+ const nameEqualsChildren = transformedChildren.length === 1 && normalizedName === transformedChildren.at(0);
124
+ return {
125
+ role: snapshot.role,
126
+ name: normalizedName,
127
+ attributes: transformedAttributes,
128
+ children: nameEqualsChildren ? [] : transformedChildren
129
+ };
130
+ }
131
+ function hasOnlyRole(snapshot) {
132
+ return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && isEmpty(snapshot.children);
133
+ }
134
+ function hasOnlyName(snapshot) {
135
+ return !isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && isEmpty(snapshot.children);
136
+ }
137
+ function hasOnlyChildren(snapshot) {
138
+ return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && !isEmpty(snapshot.children);
139
+ }
140
+ function transformAttributes(attributes) {
141
+ const filteredAttributes = {};
142
+ Object.entries(attributes).forEach(([attributeName, attributeValue]) => {
143
+ if (attributeValue === void 0) return;
144
+ filteredAttributes[attributeName] = attributeValue;
145
+ });
146
+ return filteredAttributes;
147
+ }
148
+ function transformedSnapshot(role, nameOrContent, additionalProperties) {
149
+ return {
150
+ [role]: nameOrContent,
151
+ ...additionalProperties
152
+ };
153
+ }
154
+ //#endregion
155
+ //#region src/transformers/combobox-transformer.ts
156
+ /**
157
+ * Returns a transformer controlling whether the options of a combobox are
158
+ * included in the snapshot
159
+ *
160
+ * Empty option lists are always omitted.
161
+ */
162
+ function comboboxTransformer(options = {}) {
163
+ const { includeOptions = false } = options;
164
+ return (snapshot, { transform }) => {
165
+ const { options: optionSnapshots, ...attributes } = snapshot.attributes;
166
+ const transformedOptions = optionSnapshots.map(transform);
167
+ return transformElementSnapshot({
168
+ ...snapshot,
169
+ attributes: includeOptions && transformedOptions.length > 0 ? {
170
+ ...attributes,
171
+ options: transformedOptions
172
+ } : attributes
173
+ }, transform);
174
+ };
175
+ }
176
+ //#endregion
177
+ //#region src/playwright/snapshot-serializer.ts
178
+ var SnapshotSerializer = class {
96
179
  filter;
97
180
  recurseFilter;
98
- includeComboboxOptions;
181
+ transformers;
182
+ defaultTransformer;
99
183
  constructor(options = {}) {
100
184
  this.filter = options.filter;
101
185
  this.recurseFilter = options.recurseFilter ?? false;
102
- this.includeComboboxOptions = options.includeComboboxOptions ?? false;
186
+ this.transformers = {
187
+ combobox: comboboxTransformer(),
188
+ ...options.transformers
189
+ };
190
+ this.defaultTransformer = options.defaultTransformer ?? semanticSnapshotTransformer;
103
191
  }
104
192
  transform(snapshots) {
105
- const filteredSnapshots = this.filterSnapshots(snapshots);
106
- const transformedSnapshots = this.transformSnapshots(filteredSnapshots);
193
+ const transformedSnapshots = this.filterSnapshots(snapshots).map(this.transformSnapshotRecursive);
107
194
  if (transformedSnapshots.length === 1) return transformedSnapshots.at(0);
108
195
  return transformedSnapshots;
109
196
  }
@@ -115,81 +202,12 @@ var SemanticSnapshotTransformer = class {
115
202
  recurse: this.recurseFilter
116
203
  });
117
204
  }
118
- transformSnapshots(snapshots) {
119
- return snapshots.map((snapshot) => this.transformSnapshotRecursive(snapshot));
120
- }
121
- transformSnapshotRecursive(snapshot) {
122
- if (isTextSnapshot(snapshot)) return this.simplifyTextSnapshot(snapshot);
123
- return this.simplifyElementSnapshot(snapshot);
124
- }
125
- simplifyTextSnapshot(snapshot) {
126
- return snapshot.name;
127
- }
128
- simplifyElementSnapshot(snapshot) {
129
- const normalizedSnapshot = this.normalizeElementSnapshot(snapshot);
130
- if (this.isEmpty(normalizedSnapshot)) {
131
- const { role } = normalizedSnapshot;
132
- return this.transformedSnapshot(role, "");
133
- }
134
- if (this.hasOnlyName(normalizedSnapshot)) {
135
- const { role, name } = normalizedSnapshot;
136
- return this.transformedSnapshot(role, name);
137
- }
138
- if (this.hasOnlyChildren(normalizedSnapshot)) {
139
- const { role, children } = normalizedSnapshot;
140
- return this.transformedSnapshot(role, children.length === 1 ? children.at(0) : children);
141
- }
142
- const { role, name, attributes, children } = normalizedSnapshot;
143
- return this.transformedSnapshot(role, name, {
144
- ...attributes,
145
- children: children.length === 0 ? void 0 : children
146
- });
147
- }
148
- normalizeElementSnapshot(snapshot) {
149
- const normalizedName = snapshot.name ?? "";
150
- const transformedAttributes = this.transformAttributes(snapshot);
151
- const transformedChildren = this.transformSnapshots(snapshot.children);
152
- const nameEqualsChildren = transformedChildren.length === 1 && normalizedName === transformedChildren.at(0);
153
- return {
154
- role: snapshot.role,
155
- name: normalizedName,
156
- attributes: transformedAttributes,
157
- children: nameEqualsChildren ? [] : transformedChildren
158
- };
159
- }
160
- isEmpty(snapshot) {
161
- return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && isEmpty(snapshot.children);
162
- }
163
- hasOnlyName(snapshot) {
164
- return !isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && isEmpty(snapshot.children);
165
- }
166
- hasOnlyChildren(snapshot) {
167
- return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && !isEmpty(snapshot.children);
168
- }
169
- transformAttributes(snapshot) {
170
- const { attributes } = snapshot;
171
- const filteredAttributes = {};
172
- Object.keys(attributes).forEach((attributeName) => {
173
- const transformedAttribute = this.transformAttribute(attributeName, snapshot);
174
- if (transformedAttribute === void 0) return;
175
- filteredAttributes[attributeName] = transformedAttribute;
176
- });
177
- return filteredAttributes;
178
- }
179
- transformAttribute(name, snapshot) {
180
- const { role, attributes } = snapshot;
181
- if (role === "combobox" && name === "options") {
182
- const transformedOptions = attributes.options.map((optionSnapshot) => this.simplifyElementSnapshot(optionSnapshot));
183
- return this.includeComboboxOptions && transformedOptions.length > 0 ? transformedOptions : void 0;
184
- }
185
- return attributes[name];
186
- }
187
- transformedSnapshot(role, nameOrContent, additionalProperties) {
188
- return {
189
- [role]: nameOrContent,
190
- ...additionalProperties
191
- };
192
- }
205
+ boundDefaultTransformer = (snapshot) => this.defaultTransformer(snapshot, { transform: this.transformSnapshotRecursive });
206
+ transformSnapshotRecursive = (snapshot) => {
207
+ const transformer = this.transformers[snapshot.role];
208
+ if (transformer !== void 0) return transformer(snapshot, { transform: this.boundDefaultTransformer });
209
+ return this.defaultTransformer(snapshot, { transform: this.transformSnapshotRecursive });
210
+ };
193
211
  };
194
212
  //#endregion
195
213
  //#region src/playwright/snapshot.ts
@@ -205,7 +223,7 @@ var SemanticSnapshotTransformer = class {
205
223
  */
206
224
  async function semanticSnapshot(target, options) {
207
225
  const snapshot = await rawSnapshot(target);
208
- return new SemanticSnapshotTransformer(options).transform(snapshot);
226
+ return new SnapshotSerializer(options).transform(snapshot);
209
227
  }
210
228
  /**
211
229
  * Captures a raw snapshot of the element matched by the provided locator.
@@ -323,4 +341,4 @@ function mergeTexts(texts) {
323
341
  return normalizeText(texts.map((textNode) => textNode.name).join(" "));
324
342
  }
325
343
  //#endregion
326
- export { defineElementSnapshotMatchers, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
344
+ export { comboboxTransformer, defineElementSnapshotMatchers, 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.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Element snapshots for Playwright",
5
5
  "homepage": "https://cronn.github.io/file-snapshots/playwright/element-snapshots/",
6
6
  "bugs": {
@@ -25,30 +25,30 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "package-directory": "8.2.0",
29
28
  "@cronn/lib-file-snapshots": "1.2.1",
30
- "@cronn/playwright-file-snapshots": "2.2.1"
29
+ "@cronn/playwright-file-snapshots": "2.2.1",
30
+ "package-directory": "8.2.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@arethetypeswrong/core": "0.18.5",
34
+ "@cronn/shared-configs": "0.0.0",
35
+ "@cronn/test-utils": "0.0.0",
36
+ "@cronn/vitest-file-snapshots": "2.1.2",
34
37
  "@playwright/test": "1.62.1",
35
38
  "@types/node": "24.13.3",
36
39
  "@typescript/native": "npm:typescript@^7.0.2",
37
- "@vitest/coverage-v8": "4.1.10",
38
- "eslint": "10.8.1",
40
+ "@vitest/coverage-v8": "4.1.11",
41
+ "eslint": "10.9.0",
39
42
  "eslint-config-prettier": "10.1.8",
40
43
  "eslint-plugin-check-file": "3.3.2",
41
44
  "eslint-plugin-unused-imports": "4.4.1",
42
- "publint": "0.3.23",
45
+ "publint": "0.3.24",
43
46
  "rimraf": "6.1.3",
44
47
  "tsdown": "0.22.14",
45
48
  "typescript": "npm:@typescript/typescript6@^6.0.2",
46
49
  "typescript-eslint": "8.67.0",
47
- "vitest": "4.1.10",
48
- "node": "runtime:24.19.0",
49
- "@cronn/shared-configs": "0.0.0",
50
- "@cronn/test-utils": "0.0.0",
51
- "@cronn/vitest-file-snapshots": "2.1.2"
50
+ "vitest": "4.1.11",
51
+ "node": "runtime:24.20.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@playwright/test": "^1.55"
@@ -56,12 +56,12 @@
56
56
  "devEngines": {
57
57
  "packageManager": {
58
58
  "name": "pnpm",
59
- "version": "11.22.0",
59
+ "version": "11.25.0",
60
60
  "onFail": "download"
61
61
  },
62
62
  "runtime": {
63
63
  "name": "node",
64
- "version": "24.19.0",
64
+ "version": "24.20.0",
65
65
  "onFail": "download"
66
66
  }
67
67
  },