@cronn/element-snapshot 0.15.0 → 0.17.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,52 +1,52 @@
1
- # Element Snapshots
1
+ # Playwright Element Snapshots
2
2
 
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.
3
+ Element snapshots for Playwright.
4
4
 
5
- Element Snapshots are designed to be used in combination with [playwright-file-snapshots](../playwright-file-snapshots/README.md) for writing Playwright tests.
5
+ [Read the full documentation](https://cronn.github.io/file-snapshots/playwright/element-snapshots/)
6
6
 
7
7
  > [!NOTE]
8
8
  > Element Snapshots are currently experimental. Not all HTML elements, ARIA roles and attributes are covered. Breaking changes to the snapshot format are possible.
9
9
 
10
10
  ## Getting Started
11
11
 
12
- ### Adding the library to your project
12
+ ### Installation
13
13
 
14
14
  ```shell
15
- npm install -D @cronn/element-snapshot
15
+ pnpm add -D @cronn/element-snapshot
16
16
  ```
17
17
 
18
18
  ```shell
19
- yarn add -D @cronn/element-snapshot
19
+ npm install -D @cronn/element-snapshot
20
20
  ```
21
21
 
22
22
  ```shell
23
- pnpm add -D @cronn/element-snapshot
23
+ yarn add -D @cronn/element-snapshot
24
24
  ```
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
-
34
- ### General-Purpose Snapshots
35
-
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.
37
-
38
28
  ```ts
39
- import { snapshotElement } from "@cronn/element-snapshot";
29
+ import { semanticSnapshot } from "@cronn/element-snapshot";
40
30
  import { defineValidationFileExpect } from "@cronn/playwright-file-snapshots";
41
31
 
42
32
  const expect = defineValidationFileExpect();
43
33
 
44
34
  test("matches element snapshot", async ({ page }) => {
45
- await expect(snapshotElement(page.getByRole("main"))).toMatchJsonFile();
35
+ await page.setContent(`
36
+ <main>
37
+ <h1>List</h1>
38
+ <ul>
39
+ <li>Apple</li>
40
+ <li>Peach</li>
41
+ </ul>
42
+ </main>
43
+ `);
44
+
45
+ await expect(semanticSnapshot(page.getByRole("main"))).toMatchJsonFile();
46
46
  });
47
47
  ```
48
48
 
49
- Element Snapshot Example:
49
+ **Output (`matches_element_snapshot.json`):**
50
50
 
51
51
  ```json
52
52
  {
@@ -70,62 +70,3 @@ Element Snapshot Example:
70
70
  ]
71
71
  }
72
72
  ```
73
-
74
- To improve the specificity of certain tests, `snapshotElement` can be called on certain areas of the page only:
75
-
76
- ```ts
77
- test("matches navigation snapshot", async ({ page }) => {
78
- await expect(snapshotElement(page.getByRole("navigation"))).toMatchJsonFile({
79
- name: "navigation",
80
- });
81
- });
82
- ```
83
-
84
- #### Snapshot Options
85
-
86
- Snapshot options can be passed when calling the snapshot function:
87
-
88
- ```ts
89
- await expect(
90
- snapshotElement(page.getByLabel("My select"), {
91
- filter: (element) => element.role === "heading",
92
- }),
93
- ).toMatchJsonFile({
94
- name: "headings",
95
- });
96
- ```
97
-
98
- | Option | Default Value | Description |
99
- | ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
- | `filter` | `() => true` | Include only elements in the snapshot for which the specified filter returns `true`. |
101
- | `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. |
102
- | `includeComboboxOptions` | `false` | Include combobox options in the snapshot. |
103
-
104
- ### Custom Snapshots
105
-
106
- 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.
107
-
108
- ```ts
109
- import { snapshotElementRaw } from "@cronn/element-snapshot";
110
-
111
- test("matches custom snapshot", async ({ page }) => {
112
- const tableSnapshot = await snapshotElementRaw(page.getByRole("table"));
113
- const markdownTable = transformToMarkdownTable(tableSnapshot);
114
-
115
- await expect(markdownTable).toMatchTextFile({ fileExtension: "md" });
116
- });
117
-
118
- function transformToMarkdownTable(snapshot: Array<NodeSnapshot>): string {
119
- // transform table snapshot to markdown table
120
- }
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. |
@@ -28,6 +28,14 @@
28
28
  name
29
29
  };
30
30
  }
31
+ function elementSnapshot(values) {
32
+ return {
33
+ role: values.role,
34
+ name: values.name,
35
+ attributes: values.attributes ?? {},
36
+ children: values.children ?? []
37
+ };
38
+ }
31
39
  //#endregion
32
40
  //#region src/utils/guards.ts
33
41
  function isTextSnapshot(snapshot) {
@@ -38,6 +46,14 @@
38
46
  return role.includes(snapshot.role);
39
47
  }
40
48
  //#endregion
49
+ //#region src/utils/predicates.ts
50
+ /**
51
+ * Include only elements with the specified role
52
+ */
53
+ function includeRole(role) {
54
+ return (snapshot) => hasRole(role, snapshot);
55
+ }
56
+ //#endregion
41
57
  //#region src/utils/filter.ts
42
58
  function filter(options) {
43
59
  const { predicate, snapshots, recurse = false } = options;
@@ -58,6 +74,16 @@
58
74
  };
59
75
  });
60
76
  }
77
+ /**
78
+ * Filters node snapshots by a specific role
79
+ */
80
+ function filterByRole(role, snapshots) {
81
+ return filter({
82
+ predicate: includeRole(role),
83
+ recurse: false,
84
+ snapshots
85
+ });
86
+ }
61
87
  function hasChildren(snapshot) {
62
88
  return "children" in snapshot && snapshot.children.length > 0;
63
89
  }
@@ -98,18 +124,12 @@
98
124
  if (textNode.textContent === null) return null;
99
125
  const normalizedText = normalizeText(textNode.textContent);
100
126
  if (normalizedText.length === 0) return null;
101
- return {
102
- role: "text",
103
- name: normalizedText
104
- };
127
+ return textSnapshot(normalizedText);
105
128
  }
106
129
  function snapshotPresentationalChildren(node) {
107
130
  const accessibleTextContent = resolveAccessibleTextContent(node);
108
131
  if (accessibleTextContent === void 0 || accessibleTextContent.length === 0) return [];
109
- return [{
110
- role: "text",
111
- name: accessibleTextContent
112
- }];
132
+ return [textSnapshot(accessibleTextContent)];
113
133
  }
114
134
  function resolveAccessibleTextContent(node, excludedNodes = []) {
115
135
  const children = snapshotChildren(node, excludedNodes);
@@ -128,10 +148,10 @@
128
148
  //#endregion
129
149
  //#region src/browser/state.ts
130
150
  function disableableAttributes(element) {
131
- return { disabled: booleanAttribute(isNativeInputElement(element) || element instanceof HTMLButtonElement ? element.hasAttribute("disabled") : false) ?? booleanAttribute(element.ariaDisabled) };
151
+ return { disabled: booleanAttribute(isNativeInputElement(element) || element instanceof HTMLButtonElement || element instanceof HTMLOptionElement ? element.hasAttribute("disabled") : false) ?? booleanAttribute(element.ariaDisabled) };
132
152
  }
133
153
  function selectableAttributes(element) {
134
- return { selected: booleanAttribute(element.ariaSelected) };
154
+ return { selected: booleanAttribute(element instanceof HTMLOptionElement ? element.hasAttribute("selected") : false) ?? booleanAttribute(element.ariaSelected) };
135
155
  }
136
156
  function inputStateAttributes(element) {
137
157
  const supportsHtmlAttributes = isNativeInputElement(element);
@@ -182,7 +202,7 @@
182
202
  if (element.ariaDescription !== null) return element.ariaDescription;
183
203
  }
184
204
  //#endregion
185
- //#region src/browser/container.ts
205
+ //#region src/types/elements/container.ts
186
206
  const CONTAINER_ROLES = new Set([
187
207
  "paragraph",
188
208
  "list",
@@ -213,15 +233,16 @@
213
233
  "tabpanel",
214
234
  "label"
215
235
  ]);
236
+ //#endregion
237
+ //#region src/browser/container.ts
216
238
  const NAMEABLE_CONTAINER_ROLES = new Set(["label"]);
217
239
  function snapshotContainer(role, element) {
218
240
  const children = snapshotChildren(element);
219
- return {
241
+ return elementSnapshot({
220
242
  role,
221
243
  name: resolveAccessibleName(element, NAMEABLE_CONTAINER_ROLES.has(role)),
222
- attributes: {},
223
244
  children
224
- };
245
+ });
225
246
  }
226
247
  function isContainerRole(role) {
227
248
  return CONTAINER_ROLES.has(role);
@@ -248,62 +269,62 @@
248
269
  //#endregion
249
270
  //#region src/browser/role.ts
250
271
  const ELEMENT_ROLES = {
251
- main: "main",
252
- nav: "navigation",
253
- form: "form",
254
- header: "banner",
255
- section: "region",
272
+ a: "link",
256
273
  article: "article",
257
274
  aside: "complementary",
258
- search: "search",
275
+ button: "button",
276
+ dd: "definition",
277
+ dl: "descriptionlist",
278
+ dt: "term",
279
+ fieldset: "group",
259
280
  footer: "contentinfo",
260
- p: "paragraph",
281
+ form: "form",
261
282
  h1: "heading",
262
283
  h2: "heading",
263
284
  h3: "heading",
264
285
  h4: "heading",
265
286
  h5: "heading",
266
287
  h6: "heading",
267
- ul: "list",
268
- ol: "list",
269
- li: "listitem",
270
- a: "link",
271
- button: "button",
288
+ header: "banner",
289
+ hr: "separator",
290
+ img: "img",
272
291
  input: resolveInputRole,
273
- textarea: "textbox",
274
- select: "combobox",
292
+ label: "label",
293
+ legend: "label",
294
+ li: "listitem",
295
+ main: "main",
296
+ nav: "navigation",
297
+ ol: "list",
275
298
  option: "option",
276
- dl: "descriptionlist",
277
- dt: "term",
278
- dd: "definition",
299
+ p: "paragraph",
300
+ progress: "progressbar",
301
+ search: "search",
302
+ section: "region",
303
+ select: "combobox",
279
304
  table: "table",
280
- thead: "rowgroup",
281
305
  tbody: "rowgroup",
306
+ textarea: "textbox",
307
+ td: "cell",
282
308
  tfoot: "rowgroup",
283
- tr: "row",
284
309
  th: resolveTableHeaderCellRole,
285
- td: "cell",
286
- fieldset: "group",
287
- progress: "progressbar",
288
- hr: "separator",
289
- img: "img",
290
- label: "label",
291
- legend: "label"
310
+ thead: "rowgroup",
311
+ tr: "row",
312
+ ul: "list"
292
313
  };
293
314
  const CONTEXT_DEPENDENT_ROLES = {
294
315
  banner: isTopLevelBanner,
295
- region: hasAccessibleName,
296
- term: isWithinDescriptionList,
316
+ cell: isWithinTableRow,
317
+ columnheader: isWithinTableRowOrGridRow,
297
318
  definition: isWithinDescriptionList,
319
+ gridcell: isWithinGridRow,
298
320
  listitem: isWithinList,
299
321
  menuitem: isWithinMenu,
300
- rowgroup: isWithinTableOrGrid,
322
+ region: hasAccessibleName,
301
323
  row: isWithinTableOrGrid,
302
- columnheader: isWithinTableRowOrGridRow,
324
+ rowgroup: isWithinTableOrGrid,
303
325
  rowheader: isWithinTableRowOrGridRow,
304
- cell: isWithinTableRow,
305
- gridcell: isWithinGridRow,
306
- tab: isWithinTablist
326
+ tab: isWithinTablist,
327
+ term: isWithinDescriptionList
307
328
  };
308
329
  const INPUT_ROLES = {
309
330
  button: "button",
@@ -416,34 +437,30 @@
416
437
  function snapshotInputElement(element) {
417
438
  const inputRole = resolveInputRole(element);
418
439
  if (inputRole === void 0 || inputRole === "button" || inputRole === "combobox") return null;
419
- const value = resolveInputValue(element);
420
- return {
440
+ return elementSnapshot({
421
441
  role: inputRole,
422
442
  name: resolveInputLabel(element),
423
443
  attributes: {
424
- value,
444
+ value: resolveInputValue(element),
425
445
  checked: resolveChecked(element),
426
- ...snapshotCommonInputAttributes(element, value === void 0)
427
- },
428
- children: []
429
- };
446
+ ...snapshotCommonInputAttributes(element)
447
+ }
448
+ });
430
449
  }
431
450
  function snapshotTextareaElement(element) {
432
- const value = resolveInputValue(element);
433
- return {
451
+ return elementSnapshot({
434
452
  role: "textbox",
435
453
  name: resolveInputLabel(element),
436
454
  attributes: {
437
- value,
438
- ...snapshotCommonInputAttributes(element, value === void 0)
439
- },
440
- children: []
441
- };
455
+ value: resolveInputValue(element),
456
+ ...snapshotCommonInputAttributes(element)
457
+ }
458
+ });
442
459
  }
443
- function snapshotCommonInputAttributes(element, isEmpty) {
460
+ function snapshotCommonInputAttributes(element) {
444
461
  return {
445
462
  ...discribableAttributes(element),
446
- placeholder: isEmpty ? resolvePlaceholder(element) : void 0,
463
+ placeholder: resolvePlaceholder(element),
447
464
  ...inputStateAttributes(element)
448
465
  };
449
466
  }
@@ -475,16 +492,15 @@
475
492
  function snapshotCombobox(element) {
476
493
  if (!isCombobox(element)) return null;
477
494
  const options = snapshotOptions(element);
478
- const value = resolveValue$1(element);
479
495
  return {
480
496
  role: "combobox",
481
497
  name: resolveInputLabel(element),
482
498
  attributes: {
483
- value,
484
- ...snapshotCommonInputAttributes(element, value === void 0)
499
+ value: resolveValue$1(element),
500
+ ...snapshotCommonInputAttributes(element),
501
+ options
485
502
  },
486
- children: [],
487
- options
503
+ children: []
488
504
  };
489
505
  }
490
506
  function isCombobox(element) {
@@ -501,20 +517,24 @@
501
517
  return selectedLabels;
502
518
  }
503
519
  function snapshotOptions(element) {
504
- return resolveOptions(element).map(snapshotOption).filter((childSnapshot) => childSnapshot !== null);
520
+ const optionsContainer = resolveOptionsContainer(element);
521
+ if (optionsContainer === null) return [];
522
+ return filterByRole("option", snapshotChildren(optionsContainer));
505
523
  }
506
- function resolveOptions(element) {
507
- if (element instanceof HTMLSelectElement) return Array.from(element.options);
524
+ function resolveOptionsContainer(element) {
525
+ if (element instanceof HTMLSelectElement) return element;
508
526
  const controlledElement = resolveElementReference(element, "aria-controls");
509
- if (controlledElement === null) return [];
510
- const optionSelector = selectorList("option", roleSelector("option"));
511
- return Array.from(controlledElement.querySelectorAll(optionSelector));
527
+ if (controlledElement === null) return null;
528
+ return controlledElement;
512
529
  }
513
530
  function snapshotOption(element) {
514
531
  return {
515
532
  role: "option",
516
533
  name: resolveAccessibleName(element),
517
- attributes: { selected: element instanceof HTMLOptionElement ? booleanAttribute(element.selected) : booleanAttribute(element.ariaSelected) },
534
+ attributes: {
535
+ ...selectableAttributes(element),
536
+ ...disableableAttributes(element)
537
+ },
518
538
  children: snapshotChildren(element)
519
539
  };
520
540
  }
@@ -537,19 +557,18 @@
537
557
  //#endregion
538
558
  //#region src/browser/group.ts
539
559
  function snapshotGroup(element) {
540
- return {
560
+ return elementSnapshot({
541
561
  role: "group",
542
562
  name: resolveGroupName(element),
543
- attributes: {},
544
563
  children: snapshotChildren(element)
545
- };
564
+ });
546
565
  }
547
566
  function resolveGroupName(element) {
548
567
  const legendElement = element instanceof HTMLFieldSetElement ? element.querySelector("legend") : null;
549
568
  if (legendElement !== null) return resolveAccessibleTextContent(legendElement);
550
569
  return resolveAccessibleName(element, false);
551
570
  }
552
- function snapshotRadiogroup(element) {
571
+ function snapshotRadioGroup(element) {
553
572
  return {
554
573
  role: "radiogroup",
555
574
  name: resolveAccessibleName(element, false),
@@ -577,12 +596,10 @@
577
596
  function snapshotImage(element) {
578
597
  const imageName = resolveImageName(element);
579
598
  if (imageName === void 0) return null;
580
- return {
599
+ return elementSnapshot({
581
600
  role: "img",
582
- name: imageName,
583
- attributes: {},
584
- children: []
585
- };
601
+ name: imageName
602
+ });
586
603
  }
587
604
  function resolveImageName(element) {
588
605
  if (element instanceof HTMLImageElement) return stringAttribute(element.alt);
@@ -631,11 +648,10 @@
631
648
  //#endregion
632
649
  //#region src/browser/separator.ts
633
650
  function snapshotSeparator(element) {
634
- return {
651
+ return elementSnapshot({
635
652
  role: "separator",
636
- attributes: {},
637
653
  children: snapshotPresentationalChildren(element)
638
- };
654
+ });
639
655
  }
640
656
  //#endregion
641
657
  //#region src/browser/tab.ts
@@ -651,13 +667,15 @@
651
667
  };
652
668
  }
653
669
  //#endregion
654
- //#region src/browser/table.ts
670
+ //#region src/types/elements/table.ts
655
671
  const SORT_TYPES = new Set([
656
672
  "ascending",
657
673
  "descending",
658
674
  "other"
659
675
  ]);
660
- function snapshotColumnheader(element) {
676
+ //#endregion
677
+ //#region src/browser/table.ts
678
+ function snapshotColumnHeader(element) {
661
679
  return {
662
680
  role: "columnheader",
663
681
  name: resolveAccessibleName(element),
@@ -668,27 +686,27 @@
668
686
  //#endregion
669
687
  //#region src/browser/element.ts
670
688
  const ROLE_SNAPSHOTS = {
689
+ alertdialog: snapshotDialogWithRole("alertdialog"),
671
690
  button: snapshotButton,
672
691
  checkbox: snapshotInput,
692
+ columnheader: snapshotColumnHeader,
673
693
  combobox: snapshotCombobox,
674
- option: snapshotOption,
694
+ dialog: snapshotDialogWithRole("dialog"),
695
+ group: snapshotGroup,
675
696
  heading: snapshotHeading,
697
+ img: snapshotImage,
698
+ menuitem: snapshotMenuitem,
699
+ option: snapshotOption,
700
+ progressbar: snapshotProgressbar,
676
701
  link: snapshotLink,
677
702
  radio: snapshotInput,
703
+ radiogroup: snapshotRadioGroup,
678
704
  searchbox: snapshotInput,
705
+ separator: snapshotSeparator,
679
706
  slider: snapshotInput,
680
707
  spinbutton: snapshotInput,
681
- textbox: snapshotInput,
682
- dialog: snapshotDialogWithRole("dialog"),
683
- alertdialog: snapshotDialogWithRole("alertdialog"),
684
708
  tab: snapshotTab,
685
- menuitem: snapshotMenuitem,
686
- columnheader: snapshotColumnheader,
687
- group: snapshotGroup,
688
- radiogroup: snapshotRadiogroup,
689
- progressbar: snapshotProgressbar,
690
- img: snapshotImage,
691
- separator: snapshotSeparator
709
+ textbox: snapshotInput
692
710
  };
693
711
  function snapshotElement(element) {
694
712
  return snapshotNodeRecursive(element);
@@ -712,14 +730,14 @@
712
730
  return snapshotContainer(elementRole, node);
713
731
  }
714
732
  const UNSUPPORTED_ELEMENTS = new Set([
715
- "style",
716
- "script",
717
- "noscript",
718
- "picture",
719
733
  "audio",
720
- "video",
721
734
  "figure",
722
- "template"
735
+ "noscript",
736
+ "picture",
737
+ "script",
738
+ "style",
739
+ "template",
740
+ "video"
723
741
  ]);
724
742
  function isSupportedElement(node) {
725
743
  if (!(node instanceof HTMLElement || node instanceof SVGElement)) return false;
package/dist/index.d.mts CHANGED
@@ -1,19 +1,22 @@
1
1
  import { Locator } from "@playwright/test";
2
2
 
3
- //#region src/browser/state.d.ts
3
+ //#region src/types/attributes.d.ts
4
4
  interface DisableableAttributes {
5
5
  disabled?: boolean;
6
6
  }
7
7
  interface SelectableAttributes {
8
8
  selected?: boolean;
9
9
  }
10
+ interface DiscribableAttributes {
11
+ description?: string;
12
+ }
10
13
  interface InputStateAttributes extends DisableableAttributes {
11
14
  readonly?: boolean;
12
15
  required?: boolean;
13
16
  invalid?: boolean;
14
17
  }
15
18
  //#endregion
16
- //#region src/browser/button.d.ts
19
+ //#region src/types/elements/button.d.ts
17
20
  interface ButtonSnapshot extends GenericElementSnapshot<"button", ButtonAttributes> {}
18
21
  interface ButtonAttributes extends DisableableAttributes {
19
22
  expanded?: boolean;
@@ -21,110 +24,108 @@ interface ButtonAttributes extends DisableableAttributes {
21
24
  }
22
25
  type PressedValue = true | "mixed";
23
26
  //#endregion
24
- //#region src/browser/description.d.ts
25
- interface DiscribableAttributes {
26
- description?: string;
27
- }
28
- //#endregion
29
- //#region src/browser/input.d.ts
30
- interface InputSnapshot extends GenericElementSnapshot<CommonInputRole, InputAttributes> {}
31
- type InputRole = "button" | "combobox" | CommonInputRole;
32
- type CommonInputRole = "checkbox" | "radio" | "searchbox" | "slider" | "spinbutton" | "textbox";
33
- interface InputAttributes extends CommonInputAttributes {
34
- value?: string;
35
- checked?: boolean;
36
- }
37
- interface CommonInputAttributes extends InputStateAttributes, DiscribableAttributes {
38
- placeholder?: string;
39
- }
40
- //#endregion
41
- //#region src/browser/combobox.d.ts
42
- interface ComboboxSnapshot extends GenericElementSnapshot<"combobox", ComboboxAttributes> {
43
- options: Array<OptionSnapshot>;
44
- }
45
- interface ComboboxAttributes extends CommonInputAttributes {
46
- value?: string | Array<string>;
47
- }
48
- interface OptionSnapshot extends GenericElementSnapshot<"option", OptionAttributes> {}
49
- interface OptionAttributes {
50
- selected?: boolean;
51
- }
52
- //#endregion
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" | "descriptionlist" | "term" | "definition" | "table" | "grid" | "rowgroup" | "row" | "rowheader" | "cell" | "gridcell" | "alert" | "menu" | "tablist" | "tabpanel" | "label">;
55
- interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {}
56
- type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
57
- //#endregion
58
- //#region src/browser/dialog.d.ts
27
+ //#region src/types/elements/dialog.d.ts
59
28
  interface DialogSnapshot extends GenericElementSnapshot<DialogRole, DialogAttributes> {}
60
29
  type DialogRole = "dialog" | "alertdialog";
61
30
  interface DialogAttributes extends DiscribableAttributes {
62
31
  modal?: boolean;
63
32
  }
64
33
  //#endregion
65
- //#region src/browser/group.d.ts
34
+ //#region src/types/elements/group.d.ts
66
35
  interface GroupSnapshot extends GenericElementSnapshot<"group"> {}
67
- interface RadiogroupSnapshot extends GenericElementSnapshot<"radiogroup", RadiogroupAttributes> {}
36
+ interface RadioGroupSnapshot extends GenericElementSnapshot<"radiogroup", RadiogroupAttributes> {}
68
37
  interface RadiogroupAttributes extends DiscribableAttributes {}
69
38
  //#endregion
70
- //#region src/browser/heading.d.ts
39
+ //#region src/types/elements/heading.d.ts
71
40
  interface HeadingSnapshot extends GenericElementSnapshot<"heading", HeadingAttributes> {}
72
41
  interface HeadingAttributes {
73
42
  level?: number;
74
43
  }
75
44
  //#endregion
76
- //#region src/browser/image.d.ts
45
+ //#region src/types/elements/image.d.ts
77
46
  interface ImageSnapshot extends GenericElementSnapshot<"img"> {}
78
47
  //#endregion
79
- //#region src/browser/link.d.ts
48
+ //#region src/types/elements/input.d.ts
49
+ interface InputSnapshot extends GenericElementSnapshot<CommonInputRole, InputAttributes> {}
50
+ type InputRole = "combobox" | CommonInputRole;
51
+ type CommonInputRole = "button" | "checkbox" | "radio" | "searchbox" | "slider" | "spinbutton" | "textbox";
52
+ interface InputAttributes extends CommonInputAttributes {
53
+ value?: string;
54
+ checked?: boolean;
55
+ }
56
+ interface CommonInputAttributes extends InputStateAttributes, DiscribableAttributes {
57
+ placeholder?: string;
58
+ }
59
+ interface ComboboxSnapshot extends GenericElementSnapshot<"combobox", ComboboxAttributes> {}
60
+ interface ComboboxAttributes extends CommonInputAttributes {
61
+ value?: string | Array<string>;
62
+ options: Array<OptionSnapshot>;
63
+ }
64
+ interface OptionSnapshot extends GenericElementSnapshot<"option", OptionAttributes> {}
65
+ interface OptionAttributes extends SelectableAttributes, DisableableAttributes {}
66
+ //#endregion
67
+ //#region src/types/elements/link.d.ts
80
68
  interface LinkSnapshot extends GenericElementSnapshot<"link", LinkAttributes> {}
81
69
  interface LinkAttributes {
82
70
  url?: string;
83
71
  }
84
72
  //#endregion
85
- //#region src/browser/list.d.ts
86
- interface MenuitemSnapshot extends GenericElementSnapshot<"menuitem", MenuitemAttributes> {}
73
+ //#region src/types/elements/list.d.ts
74
+ interface MenuItemSnapshot extends GenericElementSnapshot<"menuitem", MenuitemAttributes> {}
87
75
  interface MenuitemAttributes extends LinkAttributes, DisableableAttributes {}
88
76
  //#endregion
89
- //#region src/browser/progressbar.d.ts
77
+ //#region src/types/elements/progressbar.d.ts
90
78
  interface ProgressbarSnapshot extends GenericElementSnapshot<"progressbar", ProgressbarAttributes> {}
91
79
  interface ProgressbarAttributes {
92
80
  value?: number;
93
81
  }
94
82
  //#endregion
95
- //#region src/browser/separator.d.ts
83
+ //#region src/types/elements/separator.d.ts
96
84
  interface SeparatorSnapshot extends GenericElementSnapshot<"separator"> {}
97
85
  //#endregion
98
- //#region src/browser/tab.d.ts
86
+ //#region src/types/elements/tab.d.ts
99
87
  interface TabSnapshot extends GenericElementSnapshot<"tab", TabAttributes> {}
100
88
  interface TabAttributes extends DisableableAttributes, SelectableAttributes {}
101
89
  //#endregion
102
- //#region src/browser/table.d.ts
103
- interface ColumnheaderSnapshot extends GenericElementSnapshot<"columnheader", ColumnheaderAttributes> {}
90
+ //#region src/types/utils.d.ts
91
+ type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
92
+ //#endregion
93
+ //#region src/types/elements/table.d.ts
94
+ declare const SORT_TYPES: Set<"ascending" | "descending" | "other">;
95
+ interface ColumnHeaderSnapshot extends GenericElementSnapshot<"columnheader", ColumnheaderAttributes> {}
104
96
  interface ColumnheaderAttributes {
105
97
  sort?: SortType;
106
98
  }
107
- declare const SORT_TYPES: Set<"ascending" | "descending" | "other">;
108
99
  type SortType = SetValues<typeof SORT_TYPES>;
109
100
  //#endregion
110
- //#region src/browser/text.d.ts
101
+ //#region src/types/elements/text.d.ts
111
102
  interface TextSnapshot {
112
103
  role: "text";
113
104
  name: string;
114
105
  }
115
106
  //#endregion
116
- //#region src/browser/types.d.ts
117
- type NodeRole = ElementRole | "text";
118
- type ElementRole = "heading" | "link" | "button" | "option" | "tab" | "menuitem" | "columnheader" | "group" | "radiogroup" | "progressbar" | "img" | "separator" | ContainerRole | InputRole | DialogRole;
107
+ //#region src/types/snapshot.d.ts
108
+ type NodeRole = ElementRole | TextRole;
119
109
  type NodeSnapshot = ElementSnapshot | TextSnapshot;
120
- type ElementSnapshot = ContainerSnapshot | HeadingSnapshot | LinkSnapshot | ButtonSnapshot | InputSnapshot | ComboboxSnapshot | OptionSnapshot | DialogSnapshot | TabSnapshot | MenuitemSnapshot | ColumnheaderSnapshot | GroupSnapshot | RadiogroupSnapshot | ProgressbarSnapshot | ImageSnapshot | SeparatorSnapshot;
110
+ type SnapshotByRole<TRole extends NodeRole> = NodeSnapshot & {
111
+ role: TRole;
112
+ };
121
113
  interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes = Record<string, unknown>> {
122
114
  role: TRole;
123
115
  name?: string;
124
116
  attributes: TAttributes;
125
117
  children: Array<NodeSnapshot>;
126
118
  }
127
- type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
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;
128
129
  //#endregion
129
130
  //#region src/playwright/markdown-table.d.ts
130
131
  interface MarkdownTableSnapshotOptions {
@@ -140,11 +141,6 @@ interface MarkdownTableSnapshotOptions {
140
141
  */
141
142
  declare function markdownTableSnapshot(locator: Locator, options?: MarkdownTableSnapshotOptions): Promise<string>;
142
143
  //#endregion
143
- //#region src/types/snapshot.d.ts
144
- type SnapshotByRole<TRole extends NodeRole> = NodeSnapshot & {
145
- role: TRole;
146
- };
147
- //#endregion
148
144
  //#region src/utils/filter.d.ts
149
145
  type FilterPredicate = (snapshot: NodeSnapshot) => boolean;
150
146
  type GuardedFilterPredicate<TSnapshot extends NodeSnapshot> = (snapshot: NodeSnapshot) => snapshot is TSnapshot;
@@ -186,7 +182,7 @@ declare function filter(options: FilterOptions): Array<NodeSnapshot>;
186
182
  declare function filterByRole<TRole extends NodeRole>(role: TRole, snapshots: Array<NodeSnapshot>): Array<SnapshotByRole<TRole>>;
187
183
  //#endregion
188
184
  //#region src/playwright/snapshot.d.ts
189
- interface ElementSnapshotOptions {
185
+ interface SemanticSnapshotOptions {
190
186
  /**
191
187
  * Include only elements in the snapshot for which the specified filter returns `true`
192
188
  *
@@ -210,23 +206,26 @@ interface ElementSnapshotOptions {
210
206
  includeComboboxOptions?: boolean;
211
207
  }
212
208
  /**
213
- * Creates an Element Snapshot of the specified element
209
+ * Captures a semantic snapshot of the specified locator's DOM structure.
214
210
  *
215
- * @example
216
- * ```ts
217
- * await expect(snapshotElement(page.getByRole("main"))).toMatchJsonSnapshot();
218
- * ```
219
- * @param locator Locator for the element to snapshot
220
- * @experimental
211
+ * This function creates a raw snapshot of the DOM element identified by the locator
212
+ * and transforms it into a semantic representation using the provided options.
213
+ *
214
+ * @param locator - The Locator instance that identifies the DOM element to snapshot
215
+ * @param options - Optional configuration settings for controlling the snapshot behavior
216
+ * @return A promise that resolves to the semantic snapshot
221
217
  */
222
- declare function snapshotElement(locator: Locator, options?: ElementSnapshotOptions): Promise<unknown>;
218
+ declare function semanticSnapshot(locator: Locator, options?: SemanticSnapshotOptions): Promise<unknown>;
223
219
  /**
224
- * Creates an Element Snapshot of the specified element, returning the raw snapshot data for custom transformations
220
+ * Captures a raw snapshot of the element matched by the provided locator.
221
+ *
222
+ * Returns an array of node snapshots representing the current state of the matched elements
223
+ * in the DOM, including their properties, attributes, and structure.
225
224
  *
226
- * @param locator Locator for the element to snapshot
227
- * @experimental
225
+ * @param locator - The Locator instance used to identify the target element to snapshot
226
+ * @return A promise that resolves to an array of NodeSnapshot objects representing the captured element
228
227
  */
229
- declare function snapshotElementRaw(locator: Locator): Promise<Array<NodeSnapshot>>;
228
+ declare function rawSnapshot(locator: Locator): Promise<Array<NodeSnapshot>>;
230
229
  //#endregion
231
230
  //#region src/utils/predicates.d.ts
232
231
  /**
@@ -244,4 +243,4 @@ declare function excludeRole<TRole extends NodeRole>(role: TRole | Array<TRole>)
244
243
  */
245
244
  declare function getTextContent(snapshots: Array<NodeSnapshot>): string;
246
245
  //#endregion
247
- 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 SeparatorSnapshot, type SnapshotByRole, type TabSnapshot, type TextSnapshot, excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, snapshotElement, snapshotElementRaw };
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 };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { markdownTable } from "markdown-table";
1
+ import { MarkdownTableSerializer } from "@cronn/lib-file-snapshots";
2
2
  import path from "node:path";
3
3
  import { packageDirectory } from "package-directory";
4
4
  //#region src/utils/guards.ts
@@ -106,8 +106,8 @@ var ElementSnapshotProxy = class {
106
106
  }
107
107
  };
108
108
  //#endregion
109
- //#region src/playwright/transformer.ts
110
- var ElementSnapshotTransformer = class {
109
+ //#region src/playwright/semantic-snapshot-transformer.ts
110
+ var SemanticSnapshotTransformer = class {
111
111
  filter;
112
112
  recurseFilter;
113
113
  includeComboboxOptions;
@@ -162,21 +162,21 @@ var ElementSnapshotTransformer = class {
162
162
  children: children.length === 0 ? void 0 : children
163
163
  });
164
164
  }
165
- normalizeElementSnapshot(snapshot) {
165
+ normalizeElementSnapshot(snapshot, excludeAttributes = []) {
166
166
  const normalizedName = snapshot.name ?? "";
167
- const sparseAttributes = this.removeUndefinedProperties(snapshot.attributes);
167
+ const filteredAttributes = this.filterAttributes(snapshot.attributes, excludeAttributes);
168
168
  const transformedChildren = this.transformSnapshots(snapshot.children);
169
169
  const nameEqualsChildren = transformedChildren.length === 1 && normalizedName === transformedChildren.at(0);
170
170
  return {
171
171
  role: snapshot.role,
172
172
  name: normalizedName,
173
- attributes: sparseAttributes,
173
+ attributes: filteredAttributes,
174
174
  children: nameEqualsChildren ? [] : transformedChildren
175
175
  };
176
176
  }
177
177
  simplifyComboboxSnapshot(snapshot) {
178
- const { role, name, attributes } = this.normalizeElementSnapshot(snapshot);
179
- const transformedOptions = snapshot.options.map((optionSnapshot) => this.simplifyElementSnapshot(optionSnapshot));
178
+ const { role, name, attributes } = this.normalizeElementSnapshot(snapshot, ["options"]);
179
+ const transformedOptions = snapshot.attributes.options.map((optionSnapshot) => this.simplifyElementSnapshot(optionSnapshot));
180
180
  return this.transformedSnapshot(role, {
181
181
  name,
182
182
  ...attributes,
@@ -192,12 +192,13 @@ var ElementSnapshotTransformer = class {
192
192
  hasOnlyChildren(snapshot) {
193
193
  return isEmpty(snapshot.name) && isEmpty(snapshot.attributes) && !isEmpty(snapshot.children);
194
194
  }
195
- removeUndefinedProperties(attributes) {
196
- const sparseAttributes = {};
195
+ filterAttributes(attributes, exclude) {
196
+ const filteredAttributes = {};
197
197
  Object.entries(attributes).forEach(([key, value]) => {
198
- if (value !== void 0) sparseAttributes[key] = value;
198
+ if (value === void 0 && !exclude.includes(key)) return;
199
+ filteredAttributes[key] = value;
199
200
  });
200
- return sparseAttributes;
201
+ return filteredAttributes;
201
202
  }
202
203
  transformedSnapshot(role, content) {
203
204
  return { [role]: content };
@@ -206,26 +207,29 @@ var ElementSnapshotTransformer = class {
206
207
  //#endregion
207
208
  //#region src/playwright/snapshot.ts
208
209
  /**
209
- * Creates an Element Snapshot of the specified element
210
+ * Captures a semantic snapshot of the specified locator's DOM structure.
210
211
  *
211
- * @example
212
- * ```ts
213
- * await expect(snapshotElement(page.getByRole("main"))).toMatchJsonSnapshot();
214
- * ```
215
- * @param locator Locator for the element to snapshot
216
- * @experimental
212
+ * This function creates a raw snapshot of the DOM element identified by the locator
213
+ * and transforms it into a semantic representation using the provided options.
214
+ *
215
+ * @param locator - The Locator instance that identifies the DOM element to snapshot
216
+ * @param options - Optional configuration settings for controlling the snapshot behavior
217
+ * @return A promise that resolves to the semantic snapshot
217
218
  */
218
- async function snapshotElement(locator, options) {
219
- const snapshot = await snapshotElementRaw(locator);
220
- return new ElementSnapshotTransformer(options).transform(snapshot);
219
+ async function semanticSnapshot(locator, options) {
220
+ const snapshot = await rawSnapshot(locator);
221
+ return new SemanticSnapshotTransformer(options).transform(snapshot);
221
222
  }
222
223
  /**
223
- * Creates an Element Snapshot of the specified element, returning the raw snapshot data for custom transformations
224
+ * Captures a raw snapshot of the element matched by the provided locator.
225
+ *
226
+ * Returns an array of node snapshots representing the current state of the matched elements
227
+ * in the DOM, including their properties, attributes, and structure.
224
228
  *
225
- * @param locator Locator for the element to snapshot
226
- * @experimental
229
+ * @param locator - The Locator instance used to identify the target element to snapshot
230
+ * @return A promise that resolves to an array of NodeSnapshot objects representing the captured element
227
231
  */
228
- async function snapshotElementRaw(locator) {
232
+ async function rawSnapshot(locator) {
229
233
  return await new ElementSnapshotProxy(locator.page()).snapshotElement(locator);
230
234
  }
231
235
  //#endregion
@@ -245,8 +249,8 @@ const sortIndicators = {
245
249
  * Creates a Markdown table snapshot from a table or grid element
246
250
  */
247
251
  async function markdownTableSnapshot(locator, options = {}) {
248
- const { columnHeaders, rows } = parseTable(await snapshotElementRaw(locator), options);
249
- return markdownTable([columnHeaders, ...rows]);
252
+ const parsedTable = parseTable(await rawSnapshot(locator), options);
253
+ return new MarkdownTableSerializer().serialize(parsedTable);
250
254
  }
251
255
  function parseTable(snapshot, options) {
252
256
  const { showSortIndicator = true } = options;
@@ -262,7 +266,7 @@ function parseTable(snapshot, options) {
262
266
  const columnHeaders = filterByRole("columnheader", headerRow.children);
263
267
  if (columnHeaders.length === 0) throw new Error("No column headers found");
264
268
  return {
265
- columnHeaders: columnHeaders.map((columnHeader) => getColumnHeaderText(columnHeader, showSortIndicator)),
269
+ columns: columnHeaders.map((columnHeader) => getColumnHeaderText(columnHeader, showSortIndicator)),
266
270
  rows: bodyRows.map(getCellTexts)
267
271
  };
268
272
  }
@@ -279,4 +283,4 @@ function getCellTexts(row) {
279
283
  }).map((cell) => getTextContent(cell.children));
280
284
  }
281
285
  //#endregion
282
- export { excludeRole, filter, filterByRole, getTextContent, includeRole, markdownTableSnapshot, snapshotElement, snapshotElementRaw };
286
+ export { 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.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Element snapshots for Playwright",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cronn/file-snapshots/issues"
@@ -12,19 +12,19 @@
12
12
  "url": "git+https://github.com/cronn/file-snapshots.git",
13
13
  "directory": "packages/element-snapshot"
14
14
  },
15
- "homepage": "https://github.com/cronn/file-snapshots",
15
+ "homepage": "https://cronn.github.io/file-snapshots/playwright/element-snapshots/",
16
16
  "engines": {
17
17
  "node": "^20 || ^22 || >=24"
18
18
  },
19
19
  "devEngines": {
20
20
  "runtime": {
21
21
  "name": "node",
22
- "version": ">=24.13 <25",
22
+ "version": ">=24.14.1 <25",
23
23
  "onFail": "error"
24
24
  },
25
25
  "packageManager": {
26
26
  "name": "pnpm",
27
- "version": ">=10.28.2 <11",
27
+ "version": ">=10.33.0 <11",
28
28
  "onFail": "error"
29
29
  }
30
30
  },
@@ -40,29 +40,30 @@
40
40
  "dist"
41
41
  ],
42
42
  "dependencies": {
43
- "markdown-table": "3.0.4",
44
- "package-directory": "8.2.0"
43
+ "package-directory": "8.2.0",
44
+ "@cronn/lib-file-snapshots": "1.1.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@arethetypeswrong/core": "0.18.2",
48
- "@playwright/test": "1.58.2",
48
+ "@playwright/test": "1.59.1",
49
49
  "@trivago/prettier-plugin-sort-imports": "6.0.2",
50
- "@types/node": "24.12.0",
51
- "@typescript/native-preview": "7.0.0-dev.20260309.1",
52
- "@vitest/coverage-v8": "4.0.18",
53
- "eslint": "10.0.3",
50
+ "@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",
54
54
  "eslint-config-prettier": "10.1.8",
55
55
  "eslint-plugin-check-file": "3.3.1",
56
56
  "eslint-plugin-unused-imports": "4.4.1",
57
57
  "prettier": "3.8.1",
58
58
  "publint": "0.3.18",
59
- "tsdown": "0.21.1",
60
- "typescript-eslint": "8.56.1",
61
- "vitest": "4.0.18",
62
- "@cronn/playwright-file-snapshots": "1.0.1",
59
+ "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",
63
64
  "@cronn/shared-configs": "0.0.0",
64
65
  "@cronn/test-utils": "0.0.0",
65
- "@cronn/vitest-file-snapshots": "1.0.1"
66
+ "@cronn/vitest-file-snapshots": "1.1.0"
66
67
  },
67
68
  "peerDependencies": {
68
69
  "@playwright/test": "^1.55"
@@ -75,6 +76,7 @@
75
76
  "test:integration": "playwright test",
76
77
  "playwright:install": "playwright install chromium",
77
78
  "compile": "tsgo",
78
- "build": "tsdown"
79
+ "build": "tsdown",
80
+ "clean": "rimraf .turbo dist playwright-report test-results coverage"
79
81
  }
80
82
  }