@cronn/element-snapshot 0.17.0 → 0.19.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
@@ -27,9 +27,9 @@ yarn add -D @cronn/element-snapshot
27
27
 
28
28
  ```ts
29
29
  import { semanticSnapshot } from "@cronn/element-snapshot";
30
- import { defineValidationFileExpect } from "@cronn/playwright-file-snapshots";
30
+ import { defineFileSnapshotMatchers } from "@cronn/playwright-file-snapshots";
31
31
 
32
- const expect = defineValidationFileExpect();
32
+ const expect = defineFileSnapshotMatchers();
33
33
 
34
34
  test("matches element snapshot", async ({ page }) => {
35
35
  await page.setContent(`
@@ -119,89 +119,6 @@
119
119
  }, []);
120
120
  }
121
121
  //#endregion
122
- //#region src/browser/text.ts
123
- function snapshotTextNode(textNode) {
124
- if (textNode.textContent === null) return null;
125
- const normalizedText = normalizeText(textNode.textContent);
126
- if (normalizedText.length === 0) return null;
127
- return textSnapshot(normalizedText);
128
- }
129
- function snapshotPresentationalChildren(node) {
130
- const accessibleTextContent = resolveAccessibleTextContent(node);
131
- if (accessibleTextContent === void 0 || accessibleTextContent.length === 0) return [];
132
- return [textSnapshot(accessibleTextContent)];
133
- }
134
- function resolveAccessibleTextContent(node, excludedNodes = []) {
135
- const children = snapshotChildren(node, excludedNodes);
136
- if (children === void 0) return;
137
- const textContent = getTextContent(children);
138
- return textContent.length > 0 ? textContent : void 0;
139
- }
140
- //#endregion
141
- //#region src/browser/name.ts
142
- function resolveAccessibleName(element, includeTextContent = true) {
143
- const labelledByElement = resolveElementReference(element, "aria-labelledby");
144
- if (labelledByElement !== null) return resolveAccessibleTextContent(labelledByElement);
145
- if (element.ariaLabel !== null) return element.ariaLabel;
146
- if (includeTextContent) return resolveAccessibleTextContent(element);
147
- }
148
- //#endregion
149
- //#region src/browser/state.ts
150
- function disableableAttributes(element) {
151
- return { disabled: booleanAttribute(isNativeInputElement(element) || element instanceof HTMLButtonElement || element instanceof HTMLOptionElement ? element.hasAttribute("disabled") : false) ?? booleanAttribute(element.ariaDisabled) };
152
- }
153
- function selectableAttributes(element) {
154
- return { selected: booleanAttribute(element instanceof HTMLOptionElement ? element.hasAttribute("selected") : false) ?? booleanAttribute(element.ariaSelected) };
155
- }
156
- function inputStateAttributes(element) {
157
- const supportsHtmlAttributes = isNativeInputElement(element);
158
- const readonly = booleanAttribute(supportsHtmlAttributes ? element.hasAttribute("readonly") : false);
159
- const required = booleanAttribute(supportsHtmlAttributes ? element.hasAttribute("required") : false);
160
- return {
161
- readonly: readonly ?? booleanAttribute(element.ariaReadOnly),
162
- ...disableableAttributes(element),
163
- required: required ?? booleanAttribute(element.ariaRequired),
164
- invalid: booleanAttribute(element.ariaInvalid)
165
- };
166
- }
167
- function isNativeInputElement(element) {
168
- return element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement;
169
- }
170
- //#endregion
171
- //#region src/browser/button.ts
172
- const PRESSED_VALUES = new Set([
173
- "true",
174
- "mixed",
175
- "false"
176
- ]);
177
- function snapshotButton(element) {
178
- return {
179
- role: "button",
180
- name: resolveAccessibleName(element),
181
- attributes: {
182
- ...disableableAttributes(element),
183
- expanded: booleanAttribute(element.ariaExpanded),
184
- pressed: resolvePressedAttribute(element)
185
- },
186
- children: snapshotChildren(element)
187
- };
188
- }
189
- function resolvePressedAttribute(element) {
190
- const pressed = enumAttribute(element.ariaPressed, PRESSED_VALUES);
191
- if (pressed === "mixed") return pressed;
192
- return booleanAttribute(pressed === "true");
193
- }
194
- //#endregion
195
- //#region src/browser/description.ts
196
- function discribableAttributes(element) {
197
- return { description: resolveDescription(element) };
198
- }
199
- function resolveDescription(element) {
200
- const describedByElement = resolveElementReference(element, "aria-describedby");
201
- if (describedByElement !== null) return resolveAccessibleTextContent(describedByElement);
202
- if (element.ariaDescription !== null) return element.ariaDescription;
203
- }
204
- //#endregion
205
122
  //#region src/types/elements/container.ts
206
123
  const CONTAINER_ROLES = new Set([
207
124
  "paragraph",
@@ -215,7 +132,6 @@
215
132
  "form",
216
133
  "main",
217
134
  "navigation",
218
- "region",
219
135
  "search",
220
136
  "descriptionlist",
221
137
  "term",
@@ -235,12 +151,11 @@
235
151
  ]);
236
152
  //#endregion
237
153
  //#region src/browser/container.ts
238
- const NAMEABLE_CONTAINER_ROLES = new Set(["label"]);
239
154
  function snapshotContainer(role, element) {
240
155
  const children = snapshotChildren(element);
241
156
  return elementSnapshot({
242
157
  role,
243
- name: resolveAccessibleName(element, NAMEABLE_CONTAINER_ROLES.has(role)),
158
+ name: resolveAccessibleName(element),
244
159
  children
245
160
  });
246
161
  }
@@ -319,7 +234,6 @@
319
234
  gridcell: isWithinGridRow,
320
235
  listitem: isWithinList,
321
236
  menuitem: isWithinMenu,
322
- region: hasAccessibleName,
323
237
  row: isWithinTableOrGrid,
324
238
  rowgroup: isWithinTableOrGrid,
325
239
  rowheader: isWithinTableRowOrGridRow,
@@ -345,7 +259,7 @@
345
259
  const resolvedElementRole = resolveElementRole(element);
346
260
  if (resolvedElementRole === void 0) return;
347
261
  const isValidInContext = CONTEXT_DEPENDENT_ROLES[resolvedElementRole];
348
- if (isValidInContext !== void 0 && !isValidInContext(element)) return;
262
+ if (isValidInContext !== void 0 && !isValidInContext(element, resolvedElementRole)) return;
349
263
  return resolvedElementRole;
350
264
  }
351
265
  function resolveElementRole(element) {
@@ -384,9 +298,6 @@
384
298
  function isTopLevelBanner(element) {
385
299
  return !isWithinElement(element, selectorList(...DISALLOWED_BANNER_CONTAINER_ELEMENTS, ...DISALLOWED_BANNER_CONTAINER_ROLES.map(roleSelector)));
386
300
  }
387
- function hasAccessibleName(element) {
388
- return resolveAccessibleName(element, false) !== void 0;
389
- }
390
301
  function isWithinDescriptionList(element) {
391
302
  return isWithinElement(element, "dl");
392
303
  }
@@ -428,6 +339,144 @@
428
339
  return isWithinElement(element, roleSelector("tablist"));
429
340
  }
430
341
  //#endregion
342
+ //#region src/browser/text.ts
343
+ function snapshotTextNode(textNode) {
344
+ if (textNode.textContent === null) return null;
345
+ const normalizedText = normalizeText(textNode.textContent);
346
+ if (normalizedText.length === 0) return null;
347
+ return textSnapshot(normalizedText);
348
+ }
349
+ function snapshotPresentationalChildren(node) {
350
+ const accessibleTextContent = resolveAccessibleTextContent(node);
351
+ if (accessibleTextContent === void 0 || accessibleTextContent.length === 0) return [];
352
+ return [textSnapshot(accessibleTextContent)];
353
+ }
354
+ function resolveAccessibleTextContent(node, excludedNodes = []) {
355
+ const children = snapshotChildren(node, excludedNodes);
356
+ if (children === void 0) return;
357
+ const textContent = getTextContent(children);
358
+ return textContent.length > 0 ? textContent : void 0;
359
+ }
360
+ //#endregion
361
+ //#region src/browser/name.ts
362
+ const prohibitsNaming = new Set([
363
+ "label",
364
+ "listitem",
365
+ "paragraph",
366
+ "rowgroup",
367
+ "term"
368
+ ]);
369
+ const supportsNamingByLabel = new Set([
370
+ "checkbox",
371
+ "combobox",
372
+ "progressbar",
373
+ "radio",
374
+ "searchbox",
375
+ "slider",
376
+ "spinbutton",
377
+ "textbox"
378
+ ]);
379
+ const supportsNamingFromChildContent = new Set([
380
+ "button",
381
+ "cell",
382
+ "columnheader",
383
+ "gridcell",
384
+ "heading",
385
+ "link",
386
+ "menuitem",
387
+ "option",
388
+ "rowheader",
389
+ "tab"
390
+ ]);
391
+ const contextBasedNameResolvers = [resolveImageName, resolveLegendName];
392
+ function resolveAccessibleName(element) {
393
+ const role = parseElementRole(element);
394
+ if (role === void 0 || prohibitsNaming.has(role)) return;
395
+ const labelledByElement = resolveElementReference(element, "aria-labelledby");
396
+ if (labelledByElement !== null) return resolveAccessibleTextContent(labelledByElement);
397
+ if (element.ariaLabel !== null) return element.ariaLabel;
398
+ for (const resolveName of contextBasedNameResolvers) {
399
+ const name = resolveName(element, role);
400
+ if (name !== void 0) return name;
401
+ }
402
+ if (supportsNamingByLabel.has(role)) return resolveLabelName(element);
403
+ if (supportsNamingFromChildContent.has(role)) return resolveAccessibleTextContent(element);
404
+ }
405
+ function resolveLabelName(element) {
406
+ if (element.id !== null) {
407
+ const referencedElement = element.ownerDocument.querySelector(attributeSelector("for", element.id));
408
+ if (referencedElement !== null) return resolveAccessibleTextContent(referencedElement);
409
+ }
410
+ const closestLabel = element.closest("label");
411
+ if (closestLabel !== null) return resolveAccessibleTextContent(closestLabel, [element]);
412
+ }
413
+ function resolveLegendName(element) {
414
+ if (!(element instanceof HTMLFieldSetElement)) return;
415
+ const legendElement = element.querySelector("legend");
416
+ if (legendElement === null) return;
417
+ return resolveAccessibleTextContent(legendElement);
418
+ }
419
+ function resolveImageName(element, role) {
420
+ if (element instanceof HTMLImageElement) return stringAttribute(element.alt);
421
+ if (role === "img") return resolveAccessibleTextContent(element);
422
+ }
423
+ //#endregion
424
+ //#region src/browser/state.ts
425
+ function disableableAttributes(element) {
426
+ return { disabled: booleanAttribute(isNativeInputElement(element) || element instanceof HTMLButtonElement || element instanceof HTMLOptionElement ? element.hasAttribute("disabled") : false) ?? booleanAttribute(element.ariaDisabled) };
427
+ }
428
+ function selectableAttributes(element) {
429
+ return { selected: booleanAttribute(element instanceof HTMLOptionElement ? element.hasAttribute("selected") : false) ?? booleanAttribute(element.ariaSelected) };
430
+ }
431
+ function inputStateAttributes(element) {
432
+ const supportsHtmlAttributes = isNativeInputElement(element);
433
+ const readonly = booleanAttribute(supportsHtmlAttributes ? element.hasAttribute("readonly") : false);
434
+ const required = booleanAttribute(supportsHtmlAttributes ? element.hasAttribute("required") : false);
435
+ return {
436
+ readonly: readonly ?? booleanAttribute(element.ariaReadOnly),
437
+ ...disableableAttributes(element),
438
+ required: required ?? booleanAttribute(element.ariaRequired),
439
+ invalid: booleanAttribute(element.ariaInvalid)
440
+ };
441
+ }
442
+ function isNativeInputElement(element) {
443
+ return element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement;
444
+ }
445
+ //#endregion
446
+ //#region src/browser/button.ts
447
+ const PRESSED_VALUES = new Set([
448
+ "true",
449
+ "mixed",
450
+ "false"
451
+ ]);
452
+ function snapshotButton(element) {
453
+ return {
454
+ role: "button",
455
+ name: resolveAccessibleName(element),
456
+ attributes: {
457
+ ...disableableAttributes(element),
458
+ expanded: booleanAttribute(element.ariaExpanded),
459
+ pressed: resolvePressedAttribute(element)
460
+ },
461
+ children: snapshotChildren(element)
462
+ };
463
+ }
464
+ function resolvePressedAttribute(element) {
465
+ const pressed = enumAttribute(element.ariaPressed, PRESSED_VALUES);
466
+ if (pressed === "mixed") return pressed;
467
+ return booleanAttribute(pressed === "true");
468
+ }
469
+ //#endregion
470
+ //#region src/browser/description.ts
471
+ function discribableAttributes(element) {
472
+ return { description: resolveDescription(element) };
473
+ }
474
+ function resolveDescription(element) {
475
+ const describedByElement = resolveElementReference(element, "aria-describedby");
476
+ if (describedByElement !== null) return resolveAccessibleTextContent(describedByElement);
477
+ if (element.ariaDescription !== null) return element.ariaDescription;
478
+ }
479
+ //#endregion
431
480
  //#region src/browser/input.ts
432
481
  function snapshotInput(element) {
433
482
  if (element instanceof HTMLInputElement) return snapshotInputElement(element);
@@ -439,7 +488,7 @@
439
488
  if (inputRole === void 0 || inputRole === "button" || inputRole === "combobox") return null;
440
489
  return elementSnapshot({
441
490
  role: inputRole,
442
- name: resolveInputLabel(element),
491
+ name: resolveAccessibleName(element),
443
492
  attributes: {
444
493
  value: resolveInputValue(element),
445
494
  checked: resolveChecked(element),
@@ -450,7 +499,7 @@
450
499
  function snapshotTextareaElement(element) {
451
500
  return elementSnapshot({
452
501
  role: "textbox",
453
- name: resolveInputLabel(element),
502
+ name: resolveAccessibleName(element),
454
503
  attributes: {
455
504
  value: resolveInputValue(element),
456
505
  ...snapshotCommonInputAttributes(element)
@@ -473,16 +522,6 @@
473
522
  if (!(element instanceof HTMLInputElement)) return;
474
523
  return element.checked ? true : void 0;
475
524
  }
476
- function resolveInputLabel(element) {
477
- const accessibleName = resolveAccessibleName(element, false);
478
- if (accessibleName !== void 0) return accessibleName;
479
- if (element.id !== null) {
480
- const referencedElement = element.ownerDocument.querySelector(`[for='${element.id}']`);
481
- if (referencedElement !== null) return resolveAccessibleTextContent(referencedElement);
482
- }
483
- const closestLabel = element.closest("label");
484
- if (closestLabel !== null) return resolveAccessibleTextContent(closestLabel, [element]);
485
- }
486
525
  function resolvePlaceholder(element) {
487
526
  if (element instanceof HTMLSelectElement || element instanceof HTMLButtonElement) return;
488
527
  return stringAttribute(element.placeholder);
@@ -494,7 +533,7 @@
494
533
  const options = snapshotOptions(element);
495
534
  return {
496
535
  role: "combobox",
497
- name: resolveInputLabel(element),
536
+ name: resolveAccessibleName(element),
498
537
  attributes: {
499
538
  value: resolveValue$1(element),
500
539
  ...snapshotCommonInputAttributes(element),
@@ -546,7 +585,7 @@
546
585
  function snapshotDialog(role, element) {
547
586
  return {
548
587
  role,
549
- name: resolveAccessibleName(element, false),
588
+ name: resolveAccessibleName(element),
550
589
  attributes: {
551
590
  ...discribableAttributes(element),
552
591
  modal: booleanAttribute(element.ariaModal)
@@ -559,19 +598,14 @@
559
598
  function snapshotGroup(element) {
560
599
  return elementSnapshot({
561
600
  role: "group",
562
- name: resolveGroupName(element),
601
+ name: resolveAccessibleName(element),
563
602
  children: snapshotChildren(element)
564
603
  });
565
604
  }
566
- function resolveGroupName(element) {
567
- const legendElement = element instanceof HTMLFieldSetElement ? element.querySelector("legend") : null;
568
- if (legendElement !== null) return resolveAccessibleTextContent(legendElement);
569
- return resolveAccessibleName(element, false);
570
- }
571
605
  function snapshotRadioGroup(element) {
572
606
  return {
573
607
  role: "radiogroup",
574
- name: resolveAccessibleName(element, false),
608
+ name: resolveAccessibleName(element),
575
609
  attributes: discribableAttributes(element),
576
610
  children: snapshotChildren(element)
577
611
  };
@@ -594,17 +628,13 @@
594
628
  //#endregion
595
629
  //#region src/browser/image.ts
596
630
  function snapshotImage(element) {
597
- const imageName = resolveImageName(element);
631
+ const imageName = resolveAccessibleName(element);
598
632
  if (imageName === void 0) return null;
599
633
  return elementSnapshot({
600
634
  role: "img",
601
635
  name: imageName
602
636
  });
603
637
  }
604
- function resolveImageName(element) {
605
- if (element instanceof HTMLImageElement) return stringAttribute(element.alt);
606
- return resolveAccessibleName(element, true);
607
- }
608
638
  //#endregion
609
639
  //#region src/browser/link.ts
610
640
  function snapshotLink(element) {
@@ -636,7 +666,7 @@
636
666
  function snapshotProgressbar(element) {
637
667
  return {
638
668
  role: "progressbar",
639
- name: resolveAccessibleName(element, false),
669
+ name: resolveAccessibleName(element),
640
670
  attributes: { value: resolveValue(element) },
641
671
  children: snapshotPresentationalChildren(element)
642
672
  };
@@ -646,6 +676,18 @@
646
676
  return numericAttribute(element.ariaValueNow);
647
677
  }
648
678
  //#endregion
679
+ //#region src/browser/region.ts
680
+ function snapshotRegion(element) {
681
+ const name = resolveAccessibleName(element);
682
+ const children = snapshotChildren(element);
683
+ if (name === void 0) return children;
684
+ return elementSnapshot({
685
+ role: "region",
686
+ name,
687
+ children
688
+ });
689
+ }
690
+ //#endregion
649
691
  //#region src/browser/separator.ts
650
692
  function snapshotSeparator(element) {
651
693
  return elementSnapshot({
@@ -701,6 +743,7 @@
701
743
  link: snapshotLink,
702
744
  radio: snapshotInput,
703
745
  radiogroup: snapshotRadioGroup,
746
+ region: snapshotRegion,
704
747
  searchbox: snapshotInput,
705
748
  separator: snapshotSeparator,
706
749
  slider: snapshotInput,
package/dist/index.d.mts CHANGED
@@ -1,5 +1,20 @@
1
- import { Locator } from "@playwright/test";
1
+ import { Expect, Locator, MatcherReturnType } from "@playwright/test";
2
+ import { PlaywrightMatchJsonFileOptions, PlaywrightMatchTextFileOptions, PlaywrightValidationFileMatcherConfig } from "@cronn/playwright-file-snapshots";
2
3
 
4
+ //#region src/playwright/markdown-table.d.ts
5
+ interface MarkdownTableSnapshotOptions {
6
+ /**
7
+ * Show sort indicator for column headers
8
+ *
9
+ * @default true
10
+ */
11
+ showSortIndicator?: boolean;
12
+ }
13
+ /**
14
+ * Creates a Markdown table snapshot from a table or grid element
15
+ */
16
+ declare function markdownTableSnapshot(locator: Locator, options?: MarkdownTableSnapshotOptions): Promise<string>;
17
+ //#endregion
3
18
  //#region src/types/attributes.d.ts
4
19
  interface DisableableAttributes {
5
20
  disabled?: boolean;
@@ -24,6 +39,15 @@ interface ButtonAttributes extends DisableableAttributes {
24
39
  }
25
40
  type PressedValue = true | "mixed";
26
41
  //#endregion
42
+ //#region src/types/utils.d.ts
43
+ type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
44
+ //#endregion
45
+ //#region src/types/elements/container.d.ts
46
+ declare const CONTAINER_ROLES: Set<"paragraph" | "list" | "listitem" | "listbox" | "article" | "banner" | "complementary" | "contentinfo" | "form" | "main" | "navigation" | "search" | "descriptionlist" | "term" | "definition" | "table" | "grid" | "rowgroup" | "row" | "rowheader" | "cell" | "gridcell" | "alert" | "menu" | "tablist" | "tabpanel" | "label">;
47
+ interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {}
48
+ type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
49
+ interface RegionSnapshot extends GenericElementSnapshot<"region"> {}
50
+ //#endregion
27
51
  //#region src/types/elements/dialog.d.ts
28
52
  interface DialogSnapshot extends GenericElementSnapshot<DialogRole, DialogAttributes> {}
29
53
  type DialogRole = "dialog" | "alertdialog";
@@ -87,9 +111,6 @@ interface SeparatorSnapshot extends GenericElementSnapshot<"separator"> {}
87
111
  interface TabSnapshot extends GenericElementSnapshot<"tab", TabAttributes> {}
88
112
  interface TabAttributes extends DisableableAttributes, SelectableAttributes {}
89
113
  //#endregion
90
- //#region src/types/utils.d.ts
91
- type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
92
- //#endregion
93
114
  //#region src/types/elements/table.d.ts
94
115
  declare const SORT_TYPES: Set<"ascending" | "descending" | "other">;
95
116
  interface ColumnHeaderSnapshot extends GenericElementSnapshot<"columnheader", ColumnheaderAttributes> {}
@@ -104,6 +125,10 @@ interface TextSnapshot {
104
125
  name: string;
105
126
  }
106
127
  //#endregion
128
+ //#region src/types/role.d.ts
129
+ type TextRole = "text";
130
+ type ElementRole = "button" | "columnheader" | "group" | "heading" | "img" | "link" | "menuitem" | "option" | "progressbar" | "radiogroup" | "region" | "separator" | "tab" | ContainerRole | DialogRole | InputRole;
131
+ //#endregion
107
132
  //#region src/types/snapshot.d.ts
108
133
  type NodeRole = ElementRole | TextRole;
109
134
  type NodeSnapshot = ElementSnapshot | TextSnapshot;
@@ -116,30 +141,7 @@ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes
116
141
  attributes: TAttributes;
117
142
  children: Array<NodeSnapshot>;
118
143
  }
119
- type ElementSnapshot = ButtonSnapshot | ColumnHeaderSnapshot | ComboboxSnapshot | ContainerSnapshot | DialogSnapshot | GroupSnapshot | HeadingSnapshot | ImageSnapshot | InputSnapshot | LinkSnapshot | OptionSnapshot | ProgressbarSnapshot | TabSnapshot | MenuItemSnapshot | RadioGroupSnapshot | SeparatorSnapshot;
120
- //#endregion
121
- //#region src/types/elements/container.d.ts
122
- 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">;
123
- interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {}
124
- type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
125
- //#endregion
126
- //#region src/types/role.d.ts
127
- type TextRole = "text";
128
- type ElementRole = "button" | "columnheader" | "group" | "heading" | "img" | "link" | "menuitem" | "option" | "progressbar" | "radiogroup" | "separator" | "tab" | ContainerRole | DialogRole | InputRole;
129
- //#endregion
130
- //#region src/playwright/markdown-table.d.ts
131
- interface MarkdownTableSnapshotOptions {
132
- /**
133
- * Show sort indicator for column headers
134
- *
135
- * @default true
136
- */
137
- showSortIndicator?: boolean;
138
- }
139
- /**
140
- * Creates a Markdown table snapshot from a table or grid element
141
- */
142
- declare function markdownTableSnapshot(locator: Locator, options?: MarkdownTableSnapshotOptions): Promise<string>;
144
+ type ElementSnapshot = ButtonSnapshot | ColumnHeaderSnapshot | ComboboxSnapshot | ContainerSnapshot | DialogSnapshot | GroupSnapshot | HeadingSnapshot | ImageSnapshot | InputSnapshot | LinkSnapshot | OptionSnapshot | ProgressbarSnapshot | RegionSnapshot | TabSnapshot | MenuItemSnapshot | RadioGroupSnapshot | SeparatorSnapshot;
143
145
  //#endregion
144
146
  //#region src/utils/filter.d.ts
145
147
  type FilterPredicate = (snapshot: NodeSnapshot) => boolean;
@@ -227,6 +229,23 @@ declare function semanticSnapshot(locator: Locator, options?: SemanticSnapshotOp
227
229
  */
228
230
  declare function rawSnapshot(locator: Locator): Promise<Array<NodeSnapshot>>;
229
231
  //#endregion
232
+ //#region src/types/matchers.d.ts
233
+ interface ElementSnapshotMatchers {
234
+ toMatchSemanticSnapshotFile: (actual: Locator, options?: MatchSemanticSnapshotFileOptions) => Promise<MatcherReturnType>;
235
+ toMatchMarkdownTableSnapshotFile: (actual: Locator, options?: MatchMarkdownTableSnapshotFileOptions) => Promise<MatcherReturnType>;
236
+ }
237
+ interface MatchSemanticSnapshotFileOptions extends PlaywrightMatchJsonFileOptions, SemanticSnapshotOptions {}
238
+ interface MatchMarkdownTableSnapshotFileOptions extends Omit<PlaywrightMatchTextFileOptions, "fileExtension">, MarkdownTableSnapshotOptions {}
239
+ //#endregion
240
+ //#region src/playwright/matchers.d.ts
241
+ /**
242
+ * Defines a custom Playwright expect instance with element snapshot matchers for validating DOM elements against stored snapshots.
243
+ *
244
+ * @param {PlaywrightValidationFileMatcherConfig} config - Configuration options for file matching behavior
245
+ * @return {Expect<ElementSnapshotMatchers>} An extended expect instance that includes matcher methods for element snapshot validation.
246
+ */
247
+ declare function defineElementSnapshotMatchers(config?: PlaywrightValidationFileMatcherConfig): Expect<ElementSnapshotMatchers>;
248
+ //#endregion
230
249
  //#region src/utils/predicates.d.ts
231
250
  /**
232
251
  * Include only elements with the specified role
@@ -243,4 +262,4 @@ declare function excludeRole<TRole extends NodeRole>(role: TRole | Array<TRole>)
243
262
  */
244
263
  declare function getTextContent(snapshots: Array<NodeSnapshot>): string;
245
264
  //#endregion
246
- 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 SemanticSnapshotOptions, type SeparatorSnapshot, type SnapshotByRole, type TabSnapshot, type TextSnapshot, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
265
+ export { type ButtonSnapshot, 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, defineElementSnapshotMatchers, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
package/dist/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { expect } from "@playwright/test";
2
+ import { defineJsonFileMatcher, defineTextFileMatcher } from "@cronn/playwright-file-snapshots";
1
3
  import { MarkdownTableSerializer } from "@cronn/lib-file-snapshots";
2
4
  import path from "node:path";
3
5
  import { packageDirectory } from "package-directory";
@@ -62,23 +64,6 @@ function hasChildren(snapshot) {
62
64
  return "children" in snapshot && snapshot.children.length > 0;
63
65
  }
64
66
  //#endregion
65
- //#region src/utils/text.ts
66
- /**
67
- * Aggregates and normalizes the text content of all provided snapshots
68
- */
69
- function getTextContent(snapshots) {
70
- return mergeTexts(filter({
71
- snapshots,
72
- predicate: isTextSnapshot
73
- }));
74
- }
75
- function normalizeText(text) {
76
- return text.replaceAll(/\s+/g, " ").trim();
77
- }
78
- function mergeTexts(texts) {
79
- return normalizeText(texts.map((textNode) => textNode.name).join(" "));
80
- }
81
- //#endregion
82
67
  //#region src/playwright/proxy.ts
83
68
  var ElementSnapshotProxy = class {
84
69
  page;
@@ -154,7 +139,6 @@ var SemanticSnapshotTransformer = class {
154
139
  const { role, children } = normalizedSnapshot;
155
140
  return this.transformedSnapshot(role, children.length === 1 ? children.at(0) : children);
156
141
  }
157
- if (snapshot.role === "combobox") return this.simplifyComboboxSnapshot(snapshot);
158
142
  const { role, name, attributes, children } = normalizedSnapshot;
159
143
  return this.transformedSnapshot(role, {
160
144
  name: name.length === 0 ? void 0 : name,
@@ -162,27 +146,18 @@ var SemanticSnapshotTransformer = class {
162
146
  children: children.length === 0 ? void 0 : children
163
147
  });
164
148
  }
165
- normalizeElementSnapshot(snapshot, excludeAttributes = []) {
149
+ normalizeElementSnapshot(snapshot) {
166
150
  const normalizedName = snapshot.name ?? "";
167
- const filteredAttributes = this.filterAttributes(snapshot.attributes, excludeAttributes);
151
+ const transformedAttributes = this.transformAttributes(snapshot);
168
152
  const transformedChildren = this.transformSnapshots(snapshot.children);
169
153
  const nameEqualsChildren = transformedChildren.length === 1 && normalizedName === transformedChildren.at(0);
170
154
  return {
171
155
  role: snapshot.role,
172
156
  name: normalizedName,
173
- attributes: filteredAttributes,
157
+ attributes: transformedAttributes,
174
158
  children: nameEqualsChildren ? [] : transformedChildren
175
159
  };
176
160
  }
177
- simplifyComboboxSnapshot(snapshot) {
178
- const { role, name, attributes } = this.normalizeElementSnapshot(snapshot, ["options"]);
179
- const transformedOptions = snapshot.attributes.options.map((optionSnapshot) => this.simplifyElementSnapshot(optionSnapshot));
180
- return this.transformedSnapshot(role, {
181
- name,
182
- ...attributes,
183
- options: this.includeComboboxOptions && transformedOptions.length > 0 ? transformedOptions : void 0
184
- });
185
- }
186
161
  isEmpty(snapshot) {
187
162
  return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && isEmpty(snapshot.children);
188
163
  }
@@ -192,14 +167,24 @@ var SemanticSnapshotTransformer = class {
192
167
  hasOnlyChildren(snapshot) {
193
168
  return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && !isEmpty(snapshot.children);
194
169
  }
195
- filterAttributes(attributes, exclude) {
170
+ transformAttributes(snapshot) {
171
+ const { attributes } = snapshot;
196
172
  const filteredAttributes = {};
197
- Object.entries(attributes).forEach(([key, value]) => {
198
- if (value === void 0 && !exclude.includes(key)) return;
199
- filteredAttributes[key] = value;
173
+ Object.keys(attributes).forEach((attributeName) => {
174
+ const transformedAttribute = this.transformAttribute(attributeName, snapshot);
175
+ if (transformedAttribute === void 0) return;
176
+ filteredAttributes[attributeName] = transformedAttribute;
200
177
  });
201
178
  return filteredAttributes;
202
179
  }
180
+ transformAttribute(name, snapshot) {
181
+ const { role, attributes } = snapshot;
182
+ if (role === "combobox" && name === "options") {
183
+ const transformedOptions = attributes.options.map((optionSnapshot) => this.simplifyElementSnapshot(optionSnapshot));
184
+ return this.includeComboboxOptions && transformedOptions.length > 0 ? transformedOptions : void 0;
185
+ }
186
+ return attributes[name];
187
+ }
203
188
  transformedSnapshot(role, content) {
204
189
  return { [role]: content };
205
190
  }
@@ -271,7 +256,7 @@ function parseTable(snapshot, options) {
271
256
  };
272
257
  }
273
258
  function getColumnHeaderText(columnHeader, showSortIndicator) {
274
- const headerText = getTextContent([columnHeader]);
259
+ const headerText = getNameOrDefault(columnHeader);
275
260
  const sortType = columnHeader.attributes.sort;
276
261
  if (!showSortIndicator || sortType === void 0) return headerText;
277
262
  return `${headerText} ${sortIndicators[sortType]}`;
@@ -280,7 +265,50 @@ function getCellTexts(row) {
280
265
  return filter({
281
266
  predicate: includeRole(cellRoles),
282
267
  snapshots: row.children
283
- }).map((cell) => getTextContent(cell.children));
268
+ }).map(getNameOrDefault);
269
+ }
270
+ function getNameOrDefault(snapshot) {
271
+ return snapshot.name ?? "";
272
+ }
273
+ //#endregion
274
+ //#region src/playwright/matchers.ts
275
+ /**
276
+ * Defines a custom Playwright expect instance with element snapshot matchers for validating DOM elements against stored snapshots.
277
+ *
278
+ * @param {PlaywrightValidationFileMatcherConfig} config - Configuration options for file matching behavior
279
+ * @return {Expect<ElementSnapshotMatchers>} An extended expect instance that includes matcher methods for element snapshot validation.
280
+ */
281
+ function defineElementSnapshotMatchers(config = {}) {
282
+ async function toMatchSemanticSnapshotFile(actual, options = {}) {
283
+ return await defineJsonFileMatcher("toMatchSemanticSnapshotFile", config).bind(this)(() => semanticSnapshot(actual, options), options);
284
+ }
285
+ async function toMatchMarkdownTableSnapshotFile(actual, options = {}) {
286
+ return await defineTextFileMatcher("toMatchMarkdownTableSnapshotFile", config).bind(this)(() => markdownTableSnapshot(actual, options), {
287
+ ...options,
288
+ fileExtension: "md"
289
+ });
290
+ }
291
+ return expect.extend({
292
+ toMatchSemanticSnapshotFile,
293
+ toMatchMarkdownTableSnapshotFile
294
+ });
295
+ }
296
+ //#endregion
297
+ //#region src/utils/text.ts
298
+ /**
299
+ * Aggregates and normalizes the text content of all provided snapshots
300
+ */
301
+ function getTextContent(snapshots) {
302
+ return mergeTexts(filter({
303
+ snapshots,
304
+ predicate: isTextSnapshot
305
+ }));
306
+ }
307
+ function normalizeText(text) {
308
+ return text.replaceAll(/\s+/g, " ").trim();
309
+ }
310
+ function mergeTexts(texts) {
311
+ return normalizeText(texts.map((textNode) => textNode.name).join(" "));
284
312
  }
285
313
  //#endregion
286
- export { excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
314
+ export { defineElementSnapshotMatchers, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, rawSnapshot, semanticSnapshot };
package/package.json CHANGED
@@ -1,33 +1,21 @@
1
1
  {
2
2
  "name": "@cronn/element-snapshot",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Element snapshots for Playwright",
5
+ "homepage": "https://cronn.github.io/file-snapshots/playwright/element-snapshots/",
5
6
  "bugs": {
6
7
  "url": "https://github.com/cronn/file-snapshots/issues"
7
8
  },
8
- "author": "cronn",
9
9
  "license": "Apache-2.0",
10
+ "author": "cronn",
10
11
  "repository": {
11
12
  "type": "git",
12
13
  "url": "git+https://github.com/cronn/file-snapshots.git",
13
14
  "directory": "packages/element-snapshot"
14
15
  },
15
- "homepage": "https://cronn.github.io/file-snapshots/playwright/element-snapshots/",
16
- "engines": {
17
- "node": "^20 || ^22 || >=24"
18
- },
19
- "devEngines": {
20
- "runtime": {
21
- "name": "node",
22
- "version": ">=24.14.1 <25",
23
- "onFail": "error"
24
- },
25
- "packageManager": {
26
- "name": "pnpm",
27
- "version": ">=10.33.0 <11",
28
- "onFail": "error"
29
- }
30
- },
16
+ "files": [
17
+ "dist"
18
+ ],
31
19
  "type": "module",
32
20
  "exports": {
33
21
  "./package.json": "./package.json",
@@ -36,47 +24,58 @@
36
24
  "default": "./dist/index.mjs"
37
25
  }
38
26
  },
39
- "files": [
40
- "dist"
41
- ],
42
27
  "dependencies": {
43
28
  "package-directory": "8.2.0",
44
- "@cronn/lib-file-snapshots": "1.1.0"
29
+ "@cronn/lib-file-snapshots": "1.1.1",
30
+ "@cronn/playwright-file-snapshots": "2.0.0"
45
31
  },
46
32
  "devDependencies": {
47
33
  "@arethetypeswrong/core": "0.18.2",
48
34
  "@playwright/test": "1.59.1",
49
- "@trivago/prettier-plugin-sort-imports": "6.0.2",
50
35
  "@types/node": "24.12.2",
51
- "@typescript/native-preview": "7.0.0-dev.20260406.1",
52
- "@vitest/coverage-v8": "4.1.4",
53
- "eslint": "10.2.0",
36
+ "@typescript/native-preview": "7.0.0-dev.20260427.1",
37
+ "@vitest/coverage-v8": "4.1.5",
38
+ "eslint": "10.2.1",
54
39
  "eslint-config-prettier": "10.1.8",
55
40
  "eslint-plugin-check-file": "3.3.1",
56
41
  "eslint-plugin-unused-imports": "4.4.1",
57
- "prettier": "3.8.1",
58
42
  "publint": "0.3.18",
59
43
  "rimraf": "6.1.3",
60
- "tsdown": "0.21.7",
61
- "typescript-eslint": "8.58.0",
62
- "vitest": "4.1.4",
63
- "@cronn/playwright-file-snapshots": "1.0.3",
44
+ "tsdown": "0.21.10",
45
+ "typescript-eslint": "8.59.0",
46
+ "vitest": "4.1.5",
47
+ "node": "runtime:>=24.14.1 <25",
64
48
  "@cronn/shared-configs": "0.0.0",
65
49
  "@cronn/test-utils": "0.0.0",
66
- "@cronn/vitest-file-snapshots": "1.1.0"
50
+ "@cronn/vitest-file-snapshots": "2.0.0"
67
51
  },
68
52
  "peerDependencies": {
69
53
  "@playwright/test": "^1.55"
70
54
  },
55
+ "devEngines": {
56
+ "packageManager": {
57
+ "name": "pnpm",
58
+ "version": "11.0.7",
59
+ "onFail": "download"
60
+ },
61
+ "runtime": {
62
+ "name": "node",
63
+ "version": ">=24.14.1 <25",
64
+ "onFail": "download"
65
+ }
66
+ },
67
+ "engines": {
68
+ "node": "^20 || ^22 || >=24"
69
+ },
71
70
  "scripts": {
71
+ "build": "tsdown",
72
+ "clean": "rimraf .turbo dist playwright-report test-results coverage",
73
+ "compile": "tsgo",
72
74
  "lint:check": "eslint src/ tests/ --max-warnings=0",
73
75
  "lint:fix": "eslint src/ tests/ --max-warnings=0 --fix",
74
- "test:unit": "vitest run",
75
- "test:unit:coverage": "vitest run --coverage",
76
- "test:integration": "playwright test",
77
76
  "playwright:install": "playwright install chromium",
78
- "compile": "tsgo",
79
- "build": "tsdown",
80
- "clean": "rimraf .turbo dist playwright-report test-results coverage"
77
+ "test:integration": "playwright test",
78
+ "test:unit": "vitest run",
79
+ "test:unit:coverage": "vitest run --coverage"
81
80
  }
82
81
  }