@cronn/element-snapshot 0.11.0 → 0.13.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
@@ -89,10 +89,11 @@ await expect(
89
89
  });
90
90
  ```
91
91
 
92
- | Option | Default Value | Description |
93
- | ------------------------ | ------------- | ------------------------------------------------------------------------------------ |
94
- | `filter` | `() => true` | Include only elements in the snapshot for which the specified filter returns `true`. |
95
- | `includeComboboxOptions` | `false` | Include combobox options in the snapshot. |
92
+ | Option | Default Value | Description |
93
+ | ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `filter` | `() => true` | Include only elements in the snapshot for which the specified filter returns `true`. |
95
+ | `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. |
96
+ | `includeComboboxOptions` | `false` | Include combobox options in the snapshot. |
96
97
 
97
98
  ### Custom Snapshots
98
99
 
@@ -26,24 +26,37 @@
26
26
  //#endregion
27
27
  //#region src/browser/children.ts
28
28
  function snapshotChildren(node, excludedNodes = []) {
29
- const children = Array.from(node.childNodes).map((node) => snapshotNodeRecursive(node, excludedNodes)).filter((childSnapshot) => childSnapshot !== null).flat();
30
- return children.length > 0 ? children : void 0;
29
+ return Array.from(node.childNodes).map((childNode) => snapshotNodeRecursive(childNode, excludedNodes)).filter((childSnapshot) => childSnapshot !== null).flat();
30
+ }
31
+
32
+ //#endregion
33
+ //#region src/utils/guards.ts
34
+ function isTextSnapshot(snapshot) {
35
+ return snapshot.role === "text";
31
36
  }
32
37
 
33
38
  //#endregion
34
39
  //#region src/utils/snapshot.ts
35
- function filterSnapshots(snapshots, filter) {
40
+ function filterSnapshots(options) {
41
+ const { filter, snapshots, recurse = false } = options;
36
42
  return snapshots.flatMap((snapshot) => {
37
- const filteredChildren = hasChildren(snapshot) ? filterSnapshots(snapshot.children ?? [], filter) : [];
38
- if (filter(snapshot)) return {
43
+ const isFilteredIn = filter(snapshot);
44
+ if (isFilteredIn && !recurse) return snapshot;
45
+ if (!isFilteredIn && recurse) return [];
46
+ const filteredChildren = hasChildren(snapshot) ? filterSnapshots({
47
+ filter,
48
+ snapshots: snapshot.children ?? [],
49
+ recurse
50
+ }) : [];
51
+ if (!isFilteredIn) return filteredChildren;
52
+ return {
39
53
  ...snapshot,
40
- children: filteredChildren.length > 0 ? filteredChildren : void 0
54
+ children: filteredChildren
41
55
  };
42
- return filteredChildren;
43
56
  });
44
57
  }
45
58
  function hasChildren(snapshot) {
46
- return "children" in snapshot && snapshot.children !== void 0 && snapshot.children.length > 0;
59
+ return "children" in snapshot && snapshot.children.length > 0;
47
60
  }
48
61
 
49
62
  //#endregion
@@ -59,7 +72,7 @@
59
72
  }
60
73
  function snapshotPresentationalChildren(node) {
61
74
  const accessibleTextContent = resolveAccessibleTextContent(node);
62
- if (accessibleTextContent === void 0 || accessibleTextContent.length === 0) return;
75
+ if (accessibleTextContent === void 0 || accessibleTextContent.length === 0) return [];
63
76
  return [{
64
77
  role: "text",
65
78
  name: accessibleTextContent
@@ -68,7 +81,10 @@
68
81
  function resolveAccessibleTextContent(node, excludedNodes = []) {
69
82
  const children = snapshotChildren(node, excludedNodes);
70
83
  if (children === void 0) return;
71
- const normalizedText = normalizeText(filterSnapshots(children, (element) => element.role === "text").map((textNode) => textNode.name).join(" "));
84
+ const normalizedText = normalizeText(filterSnapshots({
85
+ snapshots: children,
86
+ filter: isTextSnapshot
87
+ }).map((textNode) => textNode.name).join(" "));
72
88
  return normalizedText.length > 0 ? normalizedText : void 0;
73
89
  }
74
90
  function normalizeText(text) {
@@ -159,6 +175,7 @@
159
175
  "navigation",
160
176
  "region",
161
177
  "search",
178
+ "descriptionlist",
162
179
  "term",
163
180
  "definition",
164
181
  "table",
@@ -178,6 +195,7 @@
178
195
  return {
179
196
  role,
180
197
  name: resolveAccessibleName(element, false),
198
+ attributes: {},
181
199
  children
182
200
  };
183
201
  }
@@ -234,6 +252,7 @@
234
252
  textarea: "textbox",
235
253
  select: "combobox",
236
254
  option: "option",
255
+ dl: "descriptionlist",
237
256
  dt: "term",
238
257
  dd: "definition",
239
258
  table: "table",
@@ -246,8 +265,7 @@
246
265
  fieldset: "group",
247
266
  progress: "progressbar",
248
267
  hr: "separator",
249
- img: "img",
250
- svg: "img"
268
+ img: "img"
251
269
  };
252
270
  const CONTEXT_DEPENDENT_ROLES = {
253
271
  banner: isTopLevelBanner,
@@ -384,7 +402,8 @@
384
402
  value,
385
403
  checked: resolveChecked(element),
386
404
  ...snapshotCommonInputAttributes(element, value === void 0)
387
- }
405
+ },
406
+ children: []
388
407
  };
389
408
  }
390
409
  function snapshotTextareaElement(element) {
@@ -395,7 +414,8 @@
395
414
  attributes: {
396
415
  value,
397
416
  ...snapshotCommonInputAttributes(element, value === void 0)
398
- }
417
+ },
418
+ children: []
399
419
  };
400
420
  }
401
421
  function snapshotCommonInputAttributes(element, isEmpty) {
@@ -442,6 +462,7 @@
442
462
  value,
443
463
  ...snapshotCommonInputAttributes(element, value === void 0)
444
464
  },
465
+ children: [],
445
466
  options
446
467
  };
447
468
  }
@@ -500,6 +521,7 @@
500
521
  return {
501
522
  role: "group",
502
523
  name: resolveGroupName(element),
524
+ attributes: {},
503
525
  children: snapshotChildren(element)
504
526
  };
505
527
  }
@@ -536,9 +558,13 @@
536
558
  //#endregion
537
559
  //#region src/browser/image.ts
538
560
  function snapshotImage(element) {
561
+ const imageName = resolveImageName(element);
562
+ if (imageName === void 0) return null;
539
563
  return {
540
564
  role: "img",
541
- name: resolveImageName(element)
565
+ name: imageName,
566
+ attributes: {},
567
+ children: []
542
568
  };
543
569
  }
544
570
  function resolveImageName(element) {
@@ -594,6 +620,7 @@
594
620
  function snapshotSeparator(element) {
595
621
  return {
596
622
  role: "separator",
623
+ attributes: {},
597
624
  children: snapshotPresentationalChildren(element)
598
625
  };
599
626
  }
package/dist/index.d.mts CHANGED
@@ -40,7 +40,7 @@ interface CommonInputAttributes extends InputStateAttributes, DiscribableAttribu
40
40
  //#endregion
41
41
  //#region src/browser/combobox.d.ts
42
42
  interface ComboboxSnapshot extends GenericElementSnapshot<"combobox", ComboboxAttributes> {
43
- options?: Array<OptionSnapshot>;
43
+ options: Array<OptionSnapshot>;
44
44
  }
45
45
  interface ComboboxAttributes extends CommonInputAttributes {
46
46
  value?: string | Array<string>;
@@ -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" | "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">;
55
55
  interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {}
56
56
  type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
57
57
  //#endregion
@@ -118,16 +118,29 @@ type NodeRole = ElementRole | "text";
118
118
  type ElementRole = "heading" | "link" | "button" | "option" | "tab" | "menuitem" | "columnheader" | "group" | "radiogroup" | "progressbar" | "img" | "separator" | ContainerRole | InputRole | DialogRole;
119
119
  type NodeSnapshot = ElementSnapshot | TextSnapshot;
120
120
  type ElementSnapshot = ContainerSnapshot | HeadingSnapshot | LinkSnapshot | ButtonSnapshot | InputSnapshot | ComboboxSnapshot | OptionSnapshot | DialogSnapshot | TabSnapshot | MenuitemSnapshot | ColumnheaderSnapshot | GroupSnapshot | RadiogroupSnapshot | ProgressbarSnapshot | ImageSnapshot | SeparatorSnapshot;
121
- interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes = unknown> {
121
+ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes = Record<string, unknown>> {
122
122
  role: TRole;
123
123
  name?: string;
124
- attributes?: TAttributes;
125
- children?: Array<NodeSnapshot>;
124
+ attributes: TAttributes;
125
+ children: Array<NodeSnapshot>;
126
126
  }
127
127
  type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
128
128
  //#endregion
129
129
  //#region src/utils/snapshot.d.ts
130
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;
136
+ }
137
+ interface GuardedSnapshotFilterOptions<TSnapshot extends NodeSnapshot> {
138
+ filter: GuardedSnapshotFilter<TSnapshot>;
139
+ snapshots: Array<NodeSnapshot>;
140
+ recurse?: boolean;
141
+ }
142
+ declare function filterSnapshots<TSnapshot extends NodeSnapshot = NodeSnapshot>(options: GuardedSnapshotFilterOptions<TSnapshot>): Array<TSnapshot>;
143
+ declare function filterSnapshots(options: SnapshotFilterOptions): Array<NodeSnapshot>;
131
144
  //#endregion
132
145
  //#region src/playwright/snapshot.d.ts
133
146
  interface ElementSnapshotOptions {
@@ -137,6 +150,15 @@ interface ElementSnapshotOptions {
137
150
  * @default () => true
138
151
  */
139
152
  filter?: SnapshotFilter;
153
+ /**
154
+ * Recursively apply specified filter to children of filtered elements
155
+ *
156
+ * By default, recursion ends when the filter returns `true` for an element.
157
+ * Should be `true` for filters intended to remove specific elements recursively.
158
+ *
159
+ * @default false
160
+ */
161
+ recurseFilter?: boolean;
140
162
  /**
141
163
  * Include combobox options in the snapshot
142
164
  *
@@ -163,4 +185,4 @@ declare function snapshotElement(locator: Locator, options?: ElementSnapshotOpti
163
185
  */
164
186
  declare function snapshotElementRaw(locator: Locator): Promise<Array<NodeSnapshot>>;
165
187
  //#endregion
166
- export { type ButtonSnapshot, type ColumnheaderSnapshot, type ComboboxSnapshot, type ContainerSnapshot, type DialogSnapshot, type ElementRole, type ElementSnapshot, type GroupSnapshot, type HeadingSnapshot, type InputSnapshot, type LinkSnapshot, type MenuitemSnapshot, type NodeRole, type NodeSnapshot, type OptionSnapshot, type ProgressbarSnapshot, type RadiogroupSnapshot, type TabSnapshot, type TextSnapshot, snapshotElement, snapshotElementRaw };
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 };
package/dist/index.mjs CHANGED
@@ -28,29 +28,49 @@ var ElementSnapshotProxy = class {
28
28
  }
29
29
  };
30
30
 
31
+ //#endregion
32
+ //#region src/utils/guards.ts
33
+ function isTextSnapshot(snapshot) {
34
+ return snapshot.role === "text";
35
+ }
36
+ function isEmpty(value) {
37
+ if (typeof value === "string" || Array.isArray(value)) return value.length === 0;
38
+ return Object.keys(value).length === 0;
39
+ }
40
+
31
41
  //#endregion
32
42
  //#region src/utils/snapshot.ts
33
- function filterSnapshots(snapshots, filter) {
43
+ function filterSnapshots(options) {
44
+ const { filter, snapshots, recurse = false } = options;
34
45
  return snapshots.flatMap((snapshot) => {
35
- const filteredChildren = hasChildren(snapshot) ? filterSnapshots(snapshot.children ?? [], filter) : [];
36
- if (filter(snapshot)) return {
46
+ const isFilteredIn = filter(snapshot);
47
+ if (isFilteredIn && !recurse) return snapshot;
48
+ if (!isFilteredIn && recurse) return [];
49
+ const filteredChildren = hasChildren(snapshot) ? filterSnapshots({
50
+ filter,
51
+ snapshots: snapshot.children ?? [],
52
+ recurse
53
+ }) : [];
54
+ if (!isFilteredIn) return filteredChildren;
55
+ return {
37
56
  ...snapshot,
38
- children: filteredChildren.length > 0 ? filteredChildren : void 0
57
+ children: filteredChildren
39
58
  };
40
- return filteredChildren;
41
59
  });
42
60
  }
43
61
  function hasChildren(snapshot) {
44
- return "children" in snapshot && snapshot.children !== void 0 && snapshot.children.length > 0;
62
+ return "children" in snapshot && snapshot.children.length > 0;
45
63
  }
46
64
 
47
65
  //#endregion
48
66
  //#region src/playwright/transformer.ts
49
67
  var ElementSnapshotTransformer = class {
50
68
  filter;
69
+ recurseFilter;
51
70
  includeComboboxOptions;
52
71
  constructor(options = {}) {
53
72
  this.filter = options.filter;
73
+ this.recurseFilter = options.recurseFilter ?? false;
54
74
  this.includeComboboxOptions = options.includeComboboxOptions ?? false;
55
75
  }
56
76
  transform(snapshots) {
@@ -61,13 +81,17 @@ var ElementSnapshotTransformer = class {
61
81
  }
62
82
  filterSnapshots(snapshots) {
63
83
  if (this.filter === void 0) return snapshots;
64
- return filterSnapshots(snapshots, this.filter);
84
+ return filterSnapshots({
85
+ filter: this.filter,
86
+ snapshots,
87
+ recurse: this.recurseFilter
88
+ });
65
89
  }
66
90
  transformSnapshots(snapshots) {
67
91
  return snapshots.map((snapshot) => this.transformSnapshotRecursive(snapshot));
68
92
  }
69
93
  transformSnapshotRecursive(snapshot) {
70
- if (snapshot.role === "text") return this.simplifyTextSnapshot(snapshot);
94
+ if (isTextSnapshot(snapshot)) return this.simplifyTextSnapshot(snapshot);
71
95
  return this.simplifyElementSnapshot(snapshot);
72
96
  }
73
97
  simplifyTextSnapshot(snapshot) {
@@ -90,25 +114,26 @@ var ElementSnapshotTransformer = class {
90
114
  if (snapshot.role === "combobox") return this.simplifyComboboxSnapshot(snapshot);
91
115
  const { role, name, attributes, children } = normalizedSnapshot;
92
116
  return this.transformedSnapshot(role, {
93
- name,
117
+ name: name.length === 0 ? void 0 : name,
94
118
  ...attributes,
95
- children
119
+ children: children.length === 0 ? void 0 : children
96
120
  });
97
121
  }
98
122
  normalizeElementSnapshot(snapshot) {
99
- const sparseAttributes = this.removeUndefinedProperties(snapshot.attributes ?? {});
100
- const transformedChildren = this.transformSnapshots(snapshot?.children ?? []);
101
- const nameEqualsChildren = transformedChildren.length === 1 && snapshot.name === transformedChildren.at(0);
123
+ const normalizedName = snapshot.name ?? "";
124
+ const sparseAttributes = this.removeUndefinedProperties(snapshot.attributes);
125
+ const transformedChildren = this.transformSnapshots(snapshot.children);
126
+ const nameEqualsChildren = transformedChildren.length === 1 && normalizedName === transformedChildren.at(0);
102
127
  return {
103
128
  role: snapshot.role,
104
- name: snapshot.name,
129
+ name: normalizedName,
105
130
  attributes: sparseAttributes,
106
- children: nameEqualsChildren ? void 0 : transformedChildren.length === 0 ? void 0 : transformedChildren
131
+ children: nameEqualsChildren ? [] : transformedChildren
107
132
  };
108
133
  }
109
134
  simplifyComboboxSnapshot(snapshot) {
110
135
  const { role, name, attributes } = this.normalizeElementSnapshot(snapshot);
111
- const transformedOptions = (snapshot.options ?? []).map((optionSnapshot) => this.simplifyElementSnapshot(optionSnapshot));
136
+ const transformedOptions = snapshot.options.map((optionSnapshot) => this.simplifyElementSnapshot(optionSnapshot));
112
137
  return this.transformedSnapshot(role, {
113
138
  name,
114
139
  ...attributes,
@@ -116,20 +141,20 @@ var ElementSnapshotTransformer = class {
116
141
  });
117
142
  }
118
143
  isEmpty(snapshot) {
119
- return snapshot.name === void 0 && (snapshot.attributes === void 0 || Object.keys(snapshot.attributes).length === 0) && (snapshot.children === void 0 || snapshot.children.length === 0);
144
+ return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && isEmpty(snapshot.children);
120
145
  }
121
146
  hasOnlyName(snapshot) {
122
- return snapshot.name !== void 0 && snapshot.attributes === void 0 && snapshot.children === void 0;
147
+ return !isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && isEmpty(snapshot.children);
123
148
  }
124
149
  hasOnlyChildren(snapshot) {
125
- return snapshot.name === void 0 && snapshot.attributes === void 0 && snapshot.children !== void 0;
150
+ return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && !isEmpty(snapshot.children);
126
151
  }
127
152
  removeUndefinedProperties(attributes) {
128
153
  const sparseAttributes = {};
129
154
  Object.entries(attributes).forEach(([key, value]) => {
130
155
  if (value !== void 0) sparseAttributes[key] = value;
131
156
  });
132
- return Object.keys(sparseAttributes).length > 0 ? sparseAttributes : void 0;
157
+ return sparseAttributes;
133
158
  }
134
159
  transformedSnapshot(role, content) {
135
160
  return { [role]: content };
@@ -163,4 +188,4 @@ async function snapshotElementRaw(locator) {
163
188
  }
164
189
 
165
190
  //#endregion
166
- export { snapshotElement, snapshotElementRaw };
191
+ export { filterSnapshots, snapshotElement, snapshotElementRaw };
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@cronn/element-snapshot",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Element snapshots for Playwright",
5
+ "bugs": {
6
+ "url": "https://github.com/cronn/file-snapshots/issues"
7
+ },
5
8
  "author": "cronn",
6
9
  "license": "Apache-2.0",
7
10
  "repository": {
8
11
  "type": "git",
9
- "url": "https://github.com/cronn/file-snapshots.git",
12
+ "url": "git+https://github.com/cronn/file-snapshots.git",
10
13
  "directory": "packages/element-snapshot"
11
14
  },
12
15
  "homepage": "https://github.com/cronn/file-snapshots",
@@ -40,33 +43,35 @@
40
43
  "package-directory": "8.2.0"
41
44
  },
42
45
  "devDependencies": {
46
+ "@arethetypeswrong/core": "0.18.2",
43
47
  "@playwright/test": "1.58.2",
44
48
  "@trivago/prettier-plugin-sort-imports": "6.0.2",
45
- "@types/node": "24.10.12",
49
+ "@typescript/native-preview": "7.0.0-dev.20260218.1",
50
+ "@types/node": "24.10.13",
46
51
  "@vitest/coverage-v8": "4.0.18",
47
- "eslint": "9.39.2",
52
+ "eslint": "10.0.1",
48
53
  "eslint-config-prettier": "10.1.8",
49
- "eslint-plugin-unused-imports": "4.3.0",
54
+ "eslint-plugin-unused-imports": "4.4.1",
50
55
  "prettier": "3.8.1",
56
+ "publint": "0.3.17",
51
57
  "tsdown": "0.20.3",
52
- "typescript": "5.9.3",
53
- "typescript-eslint": "8.54.0",
58
+ "typescript-eslint": "8.56.0",
54
59
  "vitest": "4.0.18",
55
- "@cronn/playwright-file-snapshots": "0.38.1",
56
- "@cronn/test-utils": "0.0.0",
57
- "@cronn/shared-configs": "0.0.0"
60
+ "@cronn/playwright-file-snapshots": "1.0.0",
61
+ "@cronn/shared-configs": "0.0.0",
62
+ "@cronn/test-utils": "0.0.0"
58
63
  },
59
64
  "peerDependencies": {
60
65
  "@playwright/test": "^1.55"
61
66
  },
62
67
  "scripts": {
63
- "lint:check": "eslint src/ --max-warnings=0",
64
- "lint:fix": "eslint src/ --max-warnings=0 --fix",
68
+ "lint:check": "eslint src/ tests/ --max-warnings=0",
69
+ "lint:fix": "eslint src/ tests/ --max-warnings=0 --fix",
65
70
  "test:unit": "vitest run",
66
71
  "test:unit:coverage": "vitest run --coverage",
67
72
  "test:integration": "playwright test",
68
73
  "playwright:install": "playwright install chromium",
69
- "compile": "tsc",
74
+ "compile": "tsgo",
70
75
  "build": "tsdown"
71
76
  }
72
77
  }