@cronn/element-snapshot 0.9.0 → 0.10.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,4 +1,4 @@
1
- # Element Snapshot
1
+ # Element Snapshots
2
2
 
3
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.
4
4
 
@@ -82,13 +82,18 @@ Snapshot options can be passed when calling the snapshot function:
82
82
  ```ts
83
83
  await expect(
84
84
  snapshotElement(page.getByLabel("My select"), {
85
- includeComboboxOptions: true,
85
+ filter: (element) => element.role === "heading",
86
86
  }),
87
87
  ).toMatchJsonFile({
88
- name: "select options",
88
+ name: "headings",
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. |
96
+
92
97
  ### Custom Snapshots
93
98
 
94
99
  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.
@@ -66,6 +66,9 @@ interface HeadingAttributes {
66
66
  level?: number;
67
67
  }
68
68
 
69
+ interface ImageSnapshot extends GenericElementSnapshot<"img"> {
70
+ }
71
+
69
72
  interface LinkSnapshot extends GenericElementSnapshot<"link", LinkAttributes> {
70
73
  }
71
74
  interface LinkAttributes {
@@ -103,9 +106,9 @@ interface TextSnapshot {
103
106
 
104
107
  type SnapshotTargetElement = HTMLElement | SVGElement;
105
108
  type NodeRole = ElementRole | "text";
106
- type ElementRole = "heading" | "link" | "button" | "option" | "tab" | "menuitem" | "columnheader" | "group" | "radiogroup" | "progressbar" | ContainerRole | InputRole | DialogRole;
109
+ type ElementRole = "heading" | "link" | "button" | "option" | "tab" | "menuitem" | "columnheader" | "group" | "radiogroup" | "progressbar" | "img" | ContainerRole | InputRole | DialogRole;
107
110
  type NodeSnapshot = ElementSnapshot | TextSnapshot;
108
- type ElementSnapshot = ContainerSnapshot | HeadingSnapshot | LinkSnapshot | ButtonSnapshot | InputSnapshot | ComboboxSnapshot | OptionSnapshot | DialogSnapshot | TabSnapshot | MenuitemSnapshot | ColumnheaderSnapshot | GroupSnapshot | RadiogroupSnapshot | ProgressbarSnapshot;
111
+ type ElementSnapshot = ContainerSnapshot | HeadingSnapshot | LinkSnapshot | ButtonSnapshot | InputSnapshot | ComboboxSnapshot | OptionSnapshot | DialogSnapshot | TabSnapshot | MenuitemSnapshot | ColumnheaderSnapshot | GroupSnapshot | RadiogroupSnapshot | ProgressbarSnapshot | ImageSnapshot;
109
112
  interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes = unknown> {
110
113
  role: TRole;
111
114
  name?: string;
@@ -114,7 +117,7 @@ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes
114
117
  }
115
118
  type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
116
119
 
117
- 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">;
120
+ 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" | "separator">;
118
121
  interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {
119
122
  }
120
123
  type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
@@ -172,7 +172,8 @@
172
172
  "alert",
173
173
  "menu",
174
174
  "tablist",
175
- "tabpanel"
175
+ "tabpanel",
176
+ "separator"
176
177
  ]);
177
178
  function snapshotContainer(role, element) {
178
179
  const children = snapshotChildren(element);
@@ -242,7 +243,10 @@
242
243
  th: resolveTableHeaderCellRole,
243
244
  td: "cell",
244
245
  fieldset: "group",
245
- progress: "progressbar"
246
+ progress: "progressbar",
247
+ hr: "separator",
248
+ img: "img",
249
+ svg: "img"
246
250
  };
247
251
  var CONTEXT_DEPENDENT_ROLES = {
248
252
  banner: isTopLevelBanner,
@@ -606,6 +610,20 @@
606
610
  return numericAttribute(levelAsString);
607
611
  }
608
612
 
613
+ // src/browser/image.ts
614
+ function snapshotImage(element) {
615
+ return {
616
+ role: "img",
617
+ name: resolveImageName(element)
618
+ };
619
+ }
620
+ function resolveImageName(element) {
621
+ if (element instanceof HTMLImageElement) {
622
+ return stringAttribute(element.alt);
623
+ }
624
+ return resolveAccessibleName(element, true);
625
+ }
626
+
609
627
  // src/browser/link.ts
610
628
  function snapshotLink(element) {
611
629
  return {
@@ -696,7 +714,8 @@
696
714
  columnheader: snapshotColumnheader,
697
715
  group: snapshotGroup,
698
716
  radiogroup: snapshotRadiogroup,
699
- progressbar: snapshotProgressbar
717
+ progressbar: snapshotProgressbar,
718
+ img: snapshotImage
700
719
  };
701
720
  function snapshotElement(element) {
702
721
  return snapshotNodeRecursive(element);
@@ -732,7 +751,6 @@
732
751
  "style",
733
752
  "script",
734
753
  "noscript",
735
- "img",
736
754
  "picture",
737
755
  "audio",
738
756
  "video",
@@ -740,7 +758,7 @@
740
758
  "template"
741
759
  ]);
742
760
  function isSupportedElement(node) {
743
- if (!(node instanceof HTMLElement)) {
761
+ if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
744
762
  return false;
745
763
  }
746
764
  const tagName = getElementTagName(node);
package/dist/index.d.ts CHANGED
@@ -48,7 +48,7 @@ interface OptionAttributes {
48
48
  selected?: boolean;
49
49
  }
50
50
 
51
- 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">;
51
+ 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" | "separator">;
52
52
  interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {
53
53
  }
54
54
  type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
@@ -73,6 +73,9 @@ interface HeadingAttributes {
73
73
  level?: number;
74
74
  }
75
75
 
76
+ interface ImageSnapshot extends GenericElementSnapshot<"img"> {
77
+ }
78
+
76
79
  interface LinkSnapshot extends GenericElementSnapshot<"link", LinkAttributes> {
77
80
  }
78
81
  interface LinkAttributes {
@@ -109,9 +112,9 @@ interface TextSnapshot {
109
112
  }
110
113
 
111
114
  type NodeRole = ElementRole | "text";
112
- type ElementRole = "heading" | "link" | "button" | "option" | "tab" | "menuitem" | "columnheader" | "group" | "radiogroup" | "progressbar" | ContainerRole | InputRole | DialogRole;
115
+ type ElementRole = "heading" | "link" | "button" | "option" | "tab" | "menuitem" | "columnheader" | "group" | "radiogroup" | "progressbar" | "img" | ContainerRole | InputRole | DialogRole;
113
116
  type NodeSnapshot = ElementSnapshot | TextSnapshot;
114
- type ElementSnapshot = ContainerSnapshot | HeadingSnapshot | LinkSnapshot | ButtonSnapshot | InputSnapshot | ComboboxSnapshot | OptionSnapshot | DialogSnapshot | TabSnapshot | MenuitemSnapshot | ColumnheaderSnapshot | GroupSnapshot | RadiogroupSnapshot | ProgressbarSnapshot;
117
+ type ElementSnapshot = ContainerSnapshot | HeadingSnapshot | LinkSnapshot | ButtonSnapshot | InputSnapshot | ComboboxSnapshot | OptionSnapshot | DialogSnapshot | TabSnapshot | MenuitemSnapshot | ColumnheaderSnapshot | GroupSnapshot | RadiogroupSnapshot | ProgressbarSnapshot | ImageSnapshot;
115
118
  interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes = unknown> {
116
119
  role: TRole;
117
120
  name?: string;
@@ -120,7 +123,15 @@ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes
120
123
  }
121
124
  type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
122
125
 
126
+ type SnapshotFilter = (node: NodeSnapshot) => boolean;
127
+
123
128
  interface ElementSnapshotOptions {
129
+ /**
130
+ * Include only elements in the snapshot for which the specified filter returns `true`
131
+ *
132
+ * @default () => true
133
+ */
134
+ filter?: SnapshotFilter;
124
135
  /**
125
136
  * Include combobox options in the snapshot
126
137
  *
package/dist/index.js CHANGED
@@ -43,19 +43,30 @@ var ElementSnapshotProxy = class {
43
43
 
44
44
  // src/playwright/transformer.ts
45
45
  var ElementSnapshotTransformer = class {
46
+ filter;
46
47
  includeComboboxOptions;
47
48
  constructor(options = {}) {
49
+ this.filter = options.filter ?? (() => true);
48
50
  this.includeComboboxOptions = options.includeComboboxOptions ?? false;
49
51
  }
50
52
  transform(snapshots) {
51
- const transformedSnapshots = snapshots.map(
52
- (snapshot) => this.transformSnapshotRecursive(snapshot)
53
- );
53
+ const transformedSnapshots = this.filterAndTransformSnapshots(snapshots);
54
54
  if (transformedSnapshots.length === 1) {
55
55
  return transformedSnapshots.at(0);
56
56
  }
57
57
  return transformedSnapshots;
58
58
  }
59
+ filterAndTransformSnapshots(snapshots) {
60
+ return snapshots.flatMap((snapshot) => {
61
+ if (this.filter(snapshot)) {
62
+ return this.transformSnapshotRecursive(snapshot);
63
+ }
64
+ if ("children" in snapshot) {
65
+ return this.filterAndTransformSnapshots(snapshot.children ?? []);
66
+ }
67
+ return [];
68
+ });
69
+ }
59
70
  transformSnapshotRecursive(snapshot) {
60
71
  if (snapshot.role === "text") {
61
72
  return this.simplifyTextSnapshot(snapshot);
@@ -96,15 +107,15 @@ var ElementSnapshotTransformer = class {
96
107
  const sparseAttributes = this.removeUndefinedProperties(
97
108
  snapshot.attributes ?? {}
98
109
  );
99
- const transformedChildren = snapshot.children?.map(
100
- (childSnaphot) => this.transformSnapshotRecursive(childSnaphot)
110
+ const transformedChildren = this.filterAndTransformSnapshots(
111
+ snapshot?.children ?? []
101
112
  );
102
- const nameEqualsChildren = transformedChildren?.length === 1 && snapshot.name === transformedChildren.at(0);
113
+ const nameEqualsChildren = transformedChildren.length === 1 && snapshot.name === transformedChildren.at(0);
103
114
  return {
104
115
  role: snapshot.role,
105
116
  name: snapshot.name,
106
117
  attributes: sparseAttributes,
107
- children: nameEqualsChildren ? void 0 : transformedChildren
118
+ children: nameEqualsChildren ? void 0 : transformedChildren.length === 0 ? void 0 : transformedChildren
108
119
  };
109
120
  }
110
121
  simplifyComboboxSnapshot(snapshot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cronn/element-snapshot",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Element snapshots for Playwright",
5
5
  "author": "cronn",
6
6
  "license": "Apache-2.0",
@@ -16,17 +16,18 @@
16
16
  "devEngines": {
17
17
  "runtime": {
18
18
  "name": "node",
19
- "version": ">=24.11 <25",
19
+ "version": ">=24.13 <25",
20
20
  "onFail": "error"
21
21
  },
22
22
  "packageManager": {
23
23
  "name": "pnpm",
24
- "version": ">=10.27 <11",
24
+ "version": ">=10.28.2 <11",
25
25
  "onFail": "error"
26
26
  }
27
27
  },
28
28
  "type": "module",
29
29
  "exports": {
30
+ "./package.json": "./package.json",
30
31
  ".": {
31
32
  "types": "./dist/index.d.ts",
32
33
  "default": "./dist/index.js"
@@ -39,16 +40,16 @@
39
40
  "package-directory": "8.1.0"
40
41
  },
41
42
  "devDependencies": {
42
- "@trivago/prettier-plugin-sort-imports": "6.0.0",
43
- "@types/node": "24.10.4",
43
+ "@trivago/prettier-plugin-sort-imports": "6.0.2",
44
+ "@types/node": "24.10.9",
44
45
  "eslint": "9.39.2",
45
46
  "eslint-config-prettier": "10.1.8",
46
47
  "eslint-plugin-unused-imports": "4.3.0",
47
- "@playwright/test": "1.56.1",
48
- "prettier": "3.7.4",
48
+ "@playwright/test": "1.58.0",
49
+ "prettier": "3.8.1",
49
50
  "tsup": "8.5.1",
50
51
  "typescript": "5.9.3",
51
- "typescript-eslint": "8.50.1",
52
+ "typescript-eslint": "8.54.0",
52
53
  "@cronn/shared-configs": "0.0.0"
53
54
  },
54
55
  "peerDependencies": {