@cronn/element-snapshot 0.8.0 → 0.9.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 +109 -0
- package/dist/{types.d.ts → browser-lib.d.ts} +13 -6
- package/dist/{index.global.js → browser-lib.global.js} +23 -23
- package/dist/index.d.ts +35 -13
- package/dist/index.js +158 -0
- package/package.json +14 -10
- package/dist/types.js +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Element Snapshot
|
|
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.
|
|
4
|
+
|
|
5
|
+
Element Snapshots are designed to be used in combination with [playwright-file-snapshots](../playwright-file-snapshots/README.md) for writing Playwright tests.
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
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
|
+
|
|
10
|
+
## Getting Started
|
|
11
|
+
|
|
12
|
+
### Adding the library to your project
|
|
13
|
+
|
|
14
|
+
```shell
|
|
15
|
+
npm install -D @cronn/element-snapshot
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```shell
|
|
19
|
+
yarn add -D @cronn/element-snapshot
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```shell
|
|
23
|
+
pnpm add -D @cronn/element-snapshot
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Writing Tests
|
|
27
|
+
|
|
28
|
+
### General-Purpose Snapshots
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { snapshotElement } from "@cronn/element-snapshot";
|
|
34
|
+
import { defineValidationFileExpect } from "@cronn/playwright-file-snapshots";
|
|
35
|
+
|
|
36
|
+
const expect = defineValidationFileExpect();
|
|
37
|
+
|
|
38
|
+
test("matches element snapshot", async ({ page }) => {
|
|
39
|
+
await expect(snapshotElement(page.getByRole("main"))).toMatchJsonFile();
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Element Snapshot Example:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"main": [
|
|
48
|
+
{
|
|
49
|
+
"heading": {
|
|
50
|
+
"name": "List",
|
|
51
|
+
"level": 1
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"list": [
|
|
56
|
+
{
|
|
57
|
+
"listitem": "Apple"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"listitem": "Peach"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
To improve the specificity of certain tests, `snapshotElement` can be called on certain areas of the page only:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
test("matches navigation snapshot", async ({ page }) => {
|
|
72
|
+
await expect(snapshotElement(page.getByRole("navigation"))).toMatchJsonFile({
|
|
73
|
+
name: "navigation",
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Snapshot Options
|
|
79
|
+
|
|
80
|
+
Snapshot options can be passed when calling the snapshot function:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await expect(
|
|
84
|
+
snapshotElement(page.getByLabel("My select"), {
|
|
85
|
+
includeComboboxOptions: true,
|
|
86
|
+
}),
|
|
87
|
+
).toMatchJsonFile({
|
|
88
|
+
name: "select options",
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Custom Snapshots
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { snapshotElementRaw } from "@cronn/element-snapshot";
|
|
98
|
+
|
|
99
|
+
test("matches custom snapshot", async ({ page }) => {
|
|
100
|
+
const tableSnapshot = await snapshotElementRaw(page.getByRole("table"));
|
|
101
|
+
const markdownTable = transformToMarkdownTable(tableSnapshot);
|
|
102
|
+
|
|
103
|
+
await expect(markdownTable).toMatchTextFile({ fileExtension: "md" });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function transformToMarkdownTable(snapshot: Array<NodeSnapshot>): string {
|
|
107
|
+
// transform table snapshot to markdown table
|
|
108
|
+
}
|
|
109
|
+
```
|
|
@@ -46,11 +46,6 @@ interface OptionAttributes {
|
|
|
46
46
|
selected?: boolean;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
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">;
|
|
50
|
-
interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {
|
|
51
|
-
}
|
|
52
|
-
type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
|
|
53
|
-
|
|
54
49
|
interface DialogSnapshot extends GenericElementSnapshot<DialogRole, DialogAttributes> {
|
|
55
50
|
}
|
|
56
51
|
type DialogRole = "dialog" | "alertdialog";
|
|
@@ -106,6 +101,7 @@ interface TextSnapshot {
|
|
|
106
101
|
name: string;
|
|
107
102
|
}
|
|
108
103
|
|
|
104
|
+
type SnapshotTargetElement = HTMLElement | SVGElement;
|
|
109
105
|
type NodeRole = ElementRole | "text";
|
|
110
106
|
type ElementRole = "heading" | "link" | "button" | "option" | "tab" | "menuitem" | "columnheader" | "group" | "radiogroup" | "progressbar" | ContainerRole | InputRole | DialogRole;
|
|
111
107
|
type NodeSnapshot = ElementSnapshot | TextSnapshot;
|
|
@@ -118,4 +114,15 @@ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes
|
|
|
118
114
|
}
|
|
119
115
|
type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
|
|
120
116
|
|
|
121
|
-
|
|
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">;
|
|
118
|
+
interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {
|
|
119
|
+
}
|
|
120
|
+
type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
|
|
121
|
+
|
|
122
|
+
declare function snapshotElement(element: SnapshotTargetElement): Array<NodeSnapshot>;
|
|
123
|
+
|
|
124
|
+
declare global {
|
|
125
|
+
interface Window {
|
|
126
|
+
snapshotElement: typeof snapshotElement;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
-
// src/
|
|
3
|
+
// src/browser/attribute.ts
|
|
4
4
|
function stringAttribute(value) {
|
|
5
5
|
return value !== null && value !== "" ? value : void 0;
|
|
6
6
|
}
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
return element.ownerDocument.getElementById(referenceId);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// src/
|
|
34
|
+
// src/browser/children.ts
|
|
35
35
|
function snapshotChildren(element) {
|
|
36
36
|
const children = Array.from(element.childNodes).map(snapshotNodeRecursive).filter((childSnapshot) => childSnapshot !== null).flat();
|
|
37
37
|
return children.length > 0 ? children : void 0;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// src/
|
|
40
|
+
// src/browser/text.ts
|
|
41
41
|
function snapshotTextNode(textNode) {
|
|
42
42
|
const textContent = snapshotTextContent(textNode);
|
|
43
43
|
if (textContent === void 0) {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
return normalizedText !== "" ? normalizedText : void 0;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// src/
|
|
59
|
+
// src/browser/name.ts
|
|
60
60
|
function resolveAccessibleName(element, includeTextContent = true) {
|
|
61
61
|
const labelledByElement = resolveElementReference(element, "aria-labelledby");
|
|
62
62
|
if (labelledByElement !== null) {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
return void 0;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// src/
|
|
74
|
+
// src/browser/state.ts
|
|
75
75
|
function disableableAttributes(element) {
|
|
76
76
|
const supportsHtmlAttribute = isNativeInputElement(element) || element instanceof HTMLButtonElement;
|
|
77
77
|
const disabled = booleanAttribute(
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
return element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// src/
|
|
106
|
+
// src/browser/button.ts
|
|
107
107
|
var PRESSED_VALUES = /* @__PURE__ */ new Set(["true", "mixed", "false"]);
|
|
108
108
|
function snapshotButton(element) {
|
|
109
109
|
return {
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
return booleanAttribute(pressed === "true");
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
// src/
|
|
128
|
+
// src/browser/description.ts
|
|
129
129
|
function discribableAttributes(element) {
|
|
130
130
|
return {
|
|
131
131
|
description: resolveDescription(element)
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
return void 0;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
// src/
|
|
148
|
+
// src/browser/container.ts
|
|
149
149
|
var CONTAINER_ROLES = /* @__PURE__ */ new Set([
|
|
150
150
|
"paragraph",
|
|
151
151
|
"list",
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
return CONTAINER_ROLES.has(role);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
// src/
|
|
189
|
+
// src/browser/selector.ts
|
|
190
190
|
function selectorList(...selectors) {
|
|
191
191
|
return selectors.join(",");
|
|
192
192
|
}
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
return `[${name}='${value}']`;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// src/
|
|
200
|
+
// src/browser/utils.ts
|
|
201
201
|
function getElementTagName(element) {
|
|
202
202
|
return element.tagName.toLowerCase();
|
|
203
203
|
}
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
return element.closest(containerSelector) !== null;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
// src/
|
|
208
|
+
// src/browser/role.ts
|
|
209
209
|
var ELEMENT_ROLES = {
|
|
210
210
|
main: "main",
|
|
211
211
|
nav: "navigation",
|
|
@@ -394,7 +394,7 @@
|
|
|
394
394
|
return isWithinElement(element, roleSelector("tablist"));
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
// src/
|
|
397
|
+
// src/browser/input.ts
|
|
398
398
|
function snapshotInput(element) {
|
|
399
399
|
if (element instanceof HTMLInputElement) {
|
|
400
400
|
return snapshotInputElement(element);
|
|
@@ -477,7 +477,7 @@
|
|
|
477
477
|
return stringAttribute(element.placeholder);
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
// src/
|
|
480
|
+
// src/browser/combobox.ts
|
|
481
481
|
function snapshotCombobox(element) {
|
|
482
482
|
if (!isCombobox(element)) {
|
|
483
483
|
return null;
|
|
@@ -546,7 +546,7 @@
|
|
|
546
546
|
};
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
// src/
|
|
549
|
+
// src/browser/dialog.ts
|
|
550
550
|
function snapshotDialogWithRole(role) {
|
|
551
551
|
return (element) => snapshotDialog(role, element);
|
|
552
552
|
}
|
|
@@ -562,7 +562,7 @@
|
|
|
562
562
|
};
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
// src/
|
|
565
|
+
// src/browser/group.ts
|
|
566
566
|
function snapshotGroup(element) {
|
|
567
567
|
return {
|
|
568
568
|
role: "group",
|
|
@@ -586,7 +586,7 @@
|
|
|
586
586
|
};
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
-
// src/
|
|
589
|
+
// src/browser/heading.ts
|
|
590
590
|
var LEVEL_REGEXP = /^H([1-6])$/;
|
|
591
591
|
function snapshotHeading(element) {
|
|
592
592
|
if (!(element instanceof HTMLHeadingElement)) {
|
|
@@ -606,7 +606,7 @@
|
|
|
606
606
|
return numericAttribute(levelAsString);
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
-
// src/
|
|
609
|
+
// src/browser/link.ts
|
|
610
610
|
function snapshotLink(element) {
|
|
611
611
|
return {
|
|
612
612
|
role: "link",
|
|
@@ -619,7 +619,7 @@
|
|
|
619
619
|
return { url: stringAttribute(element.getAttribute("href")) };
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
-
// src/
|
|
622
|
+
// src/browser/list.ts
|
|
623
623
|
function snapshotMenuitem(element) {
|
|
624
624
|
return {
|
|
625
625
|
role: "menuitem",
|
|
@@ -632,7 +632,7 @@
|
|
|
632
632
|
};
|
|
633
633
|
}
|
|
634
634
|
|
|
635
|
-
// src/
|
|
635
|
+
// src/browser/progressbar.ts
|
|
636
636
|
function snapshotProgressbar(element) {
|
|
637
637
|
return {
|
|
638
638
|
role: "progressbar",
|
|
@@ -650,7 +650,7 @@
|
|
|
650
650
|
return numericAttribute(element.ariaValueNow);
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
-
// src/
|
|
653
|
+
// src/browser/tab.ts
|
|
654
654
|
function snapshotTab(element) {
|
|
655
655
|
return {
|
|
656
656
|
role: "tab",
|
|
@@ -663,7 +663,7 @@
|
|
|
663
663
|
};
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
-
// src/
|
|
666
|
+
// src/browser/table.ts
|
|
667
667
|
var SORT_ATTRIBUTES = /* @__PURE__ */ new Set(["ascending", "descending", "other"]);
|
|
668
668
|
function snapshotColumnheader(element) {
|
|
669
669
|
return {
|
|
@@ -676,7 +676,7 @@
|
|
|
676
676
|
};
|
|
677
677
|
}
|
|
678
678
|
|
|
679
|
-
// src/
|
|
679
|
+
// src/browser/element.ts
|
|
680
680
|
var ROLE_SNAPSHOTS = {
|
|
681
681
|
button: snapshotButton,
|
|
682
682
|
checkbox: snapshotInput,
|
|
@@ -761,6 +761,6 @@
|
|
|
761
761
|
return cssStyles.display === "none" || cssStyles.visibility === "hidden" || cssStyles.visibility === "collapse";
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
-
// src/
|
|
764
|
+
// src/browser-lib.ts
|
|
765
765
|
window.snapshotElement = snapshotElement;
|
|
766
766
|
})();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Locator } from '@playwright/test';
|
|
2
|
+
|
|
1
3
|
interface DisableableAttributes {
|
|
2
4
|
disabled?: boolean;
|
|
3
5
|
}
|
|
@@ -46,6 +48,11 @@ interface OptionAttributes {
|
|
|
46
48
|
selected?: boolean;
|
|
47
49
|
}
|
|
48
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">;
|
|
52
|
+
interface ContainerSnapshot extends GenericElementSnapshot<ContainerRole> {
|
|
53
|
+
}
|
|
54
|
+
type ContainerRole = SetValues<typeof CONTAINER_ROLES>;
|
|
55
|
+
|
|
49
56
|
interface DialogSnapshot extends GenericElementSnapshot<DialogRole, DialogAttributes> {
|
|
50
57
|
}
|
|
51
58
|
type DialogRole = "dialog" | "alertdialog";
|
|
@@ -101,7 +108,6 @@ interface TextSnapshot {
|
|
|
101
108
|
name: string;
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
type SnapshotTargetElement = HTMLElement | SVGElement;
|
|
105
111
|
type NodeRole = ElementRole | "text";
|
|
106
112
|
type ElementRole = "heading" | "link" | "button" | "option" | "tab" | "menuitem" | "columnheader" | "group" | "radiogroup" | "progressbar" | ContainerRole | InputRole | DialogRole;
|
|
107
113
|
type NodeSnapshot = ElementSnapshot | TextSnapshot;
|
|
@@ -114,15 +120,31 @@ interface GenericElementSnapshot<TRole extends NodeRole = NodeRole, TAttributes
|
|
|
114
120
|
}
|
|
115
121
|
type SetValues<TSet> = TSet extends Set<infer TValue> ? TValue : never;
|
|
116
122
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
interface ElementSnapshotOptions {
|
|
124
|
+
/**
|
|
125
|
+
* Include combobox options in the snapshot
|
|
126
|
+
*
|
|
127
|
+
* @default false
|
|
128
|
+
*/
|
|
129
|
+
includeComboboxOptions?: boolean;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Creates an Element Snapshot of the specified element
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* await expect(snapshotElement(page.getByRole("main"))).toMatchJsonSnapshot();
|
|
137
|
+
* ```
|
|
138
|
+
* @param locator Locator for the element to snapshot
|
|
139
|
+
* @experimental
|
|
140
|
+
*/
|
|
141
|
+
declare function snapshotElement(locator: Locator, options?: ElementSnapshotOptions): Promise<unknown>;
|
|
142
|
+
/**
|
|
143
|
+
* Creates an Element Snapshot of the specified element, returning the raw snapshot data for custom transformations
|
|
144
|
+
*
|
|
145
|
+
* @param locator Locator for the element to snapshot
|
|
146
|
+
* @experimental
|
|
147
|
+
*/
|
|
148
|
+
declare function snapshotElementRaw(locator: Locator): Promise<Array<NodeSnapshot>>;
|
|
149
|
+
|
|
150
|
+
export { type ButtonSnapshot, type ColumnheaderSnapshot, type ComboboxSnapshot, type ContainerSnapshot, type DialogSnapshot, type ElementRole, type ElementSnapshot, type GroupSnapshot, type HeadingSnapshot, type InputSnapshot, type LinkSnapshot, type MenuitemSnapshot, type NodeRole, type NodeSnapshot, type OptionSnapshot, type ProgressbarSnapshot, type RadiogroupSnapshot, type TabSnapshot, type TextSnapshot, snapshotElement, snapshotElementRaw };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/playwright/proxy.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { packageDirectory } from "package-directory";
|
|
4
|
+
var ElementSnapshotProxy = class {
|
|
5
|
+
page;
|
|
6
|
+
constructor(page) {
|
|
7
|
+
this.page = page;
|
|
8
|
+
}
|
|
9
|
+
async snapshotElement(locator) {
|
|
10
|
+
if (!await this.isLibraryLoaded()) {
|
|
11
|
+
await this.loadLibrary();
|
|
12
|
+
}
|
|
13
|
+
return await locator.evaluate((element) => {
|
|
14
|
+
return window.snapshotElement(element);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async isLibraryLoaded() {
|
|
18
|
+
return await this.page.evaluate(
|
|
19
|
+
() => typeof window.snapshotElement === "function"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
async loadLibrary() {
|
|
23
|
+
const packageRootDir = await this.resolvePackageRootDir();
|
|
24
|
+
const libPath = path.resolve(
|
|
25
|
+
packageRootDir,
|
|
26
|
+
"dist",
|
|
27
|
+
"browser-lib.global.js"
|
|
28
|
+
);
|
|
29
|
+
await this.page.addScriptTag({
|
|
30
|
+
path: libPath
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async resolvePackageRootDir() {
|
|
34
|
+
const packageDir = await packageDirectory({
|
|
35
|
+
cwd: import.meta.dirname
|
|
36
|
+
});
|
|
37
|
+
if (packageDir === void 0) {
|
|
38
|
+
throw new Error("Unable to resolve root directory of package");
|
|
39
|
+
}
|
|
40
|
+
return packageDir;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/playwright/transformer.ts
|
|
45
|
+
var ElementSnapshotTransformer = class {
|
|
46
|
+
includeComboboxOptions;
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.includeComboboxOptions = options.includeComboboxOptions ?? false;
|
|
49
|
+
}
|
|
50
|
+
transform(snapshots) {
|
|
51
|
+
const transformedSnapshots = snapshots.map(
|
|
52
|
+
(snapshot) => this.transformSnapshotRecursive(snapshot)
|
|
53
|
+
);
|
|
54
|
+
if (transformedSnapshots.length === 1) {
|
|
55
|
+
return transformedSnapshots.at(0);
|
|
56
|
+
}
|
|
57
|
+
return transformedSnapshots;
|
|
58
|
+
}
|
|
59
|
+
transformSnapshotRecursive(snapshot) {
|
|
60
|
+
if (snapshot.role === "text") {
|
|
61
|
+
return this.simplifyTextSnapshot(snapshot);
|
|
62
|
+
}
|
|
63
|
+
return this.simplifyElementSnapshot(snapshot);
|
|
64
|
+
}
|
|
65
|
+
simplifyTextSnapshot(snapshot) {
|
|
66
|
+
return snapshot.name;
|
|
67
|
+
}
|
|
68
|
+
simplifyElementSnapshot(snapshot) {
|
|
69
|
+
const normalizedSnapshot = this.normalizeElementSnapshot(snapshot);
|
|
70
|
+
if (this.isEmpty(normalizedSnapshot)) {
|
|
71
|
+
const { role: role2 } = normalizedSnapshot;
|
|
72
|
+
return this.transformedSnapshot(role2, "");
|
|
73
|
+
}
|
|
74
|
+
if (this.hasOnlyName(normalizedSnapshot)) {
|
|
75
|
+
const { role: role2, name: name2 } = normalizedSnapshot;
|
|
76
|
+
return this.transformedSnapshot(role2, name2);
|
|
77
|
+
}
|
|
78
|
+
if (this.hasOnlyChildren(normalizedSnapshot)) {
|
|
79
|
+
const { role: role2, children: children2 } = normalizedSnapshot;
|
|
80
|
+
return this.transformedSnapshot(
|
|
81
|
+
role2,
|
|
82
|
+
children2.length === 1 ? children2.at(0) : children2
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (snapshot.role === "combobox") {
|
|
86
|
+
return this.simplifyComboboxSnapshot(snapshot);
|
|
87
|
+
}
|
|
88
|
+
const { role, name, attributes, children } = normalizedSnapshot;
|
|
89
|
+
return this.transformedSnapshot(role, {
|
|
90
|
+
name,
|
|
91
|
+
...attributes,
|
|
92
|
+
children
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
normalizeElementSnapshot(snapshot) {
|
|
96
|
+
const sparseAttributes = this.removeUndefinedProperties(
|
|
97
|
+
snapshot.attributes ?? {}
|
|
98
|
+
);
|
|
99
|
+
const transformedChildren = snapshot.children?.map(
|
|
100
|
+
(childSnaphot) => this.transformSnapshotRecursive(childSnaphot)
|
|
101
|
+
);
|
|
102
|
+
const nameEqualsChildren = transformedChildren?.length === 1 && snapshot.name === transformedChildren.at(0);
|
|
103
|
+
return {
|
|
104
|
+
role: snapshot.role,
|
|
105
|
+
name: snapshot.name,
|
|
106
|
+
attributes: sparseAttributes,
|
|
107
|
+
children: nameEqualsChildren ? void 0 : transformedChildren
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
simplifyComboboxSnapshot(snapshot) {
|
|
111
|
+
const { role, name, attributes } = this.normalizeElementSnapshot(snapshot);
|
|
112
|
+
const options = snapshot.options ?? [];
|
|
113
|
+
const transformedOptions = options.map(
|
|
114
|
+
(optionSnapshot) => this.simplifyElementSnapshot(optionSnapshot)
|
|
115
|
+
);
|
|
116
|
+
return this.transformedSnapshot(role, {
|
|
117
|
+
name,
|
|
118
|
+
...attributes,
|
|
119
|
+
options: this.includeComboboxOptions && transformedOptions.length > 0 ? transformedOptions : void 0
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
isEmpty(snapshot) {
|
|
123
|
+
return snapshot.name === void 0 && (snapshot.attributes === void 0 || Object.keys(snapshot.attributes).length === 0) && (snapshot.children === void 0 || snapshot.children.length === 0);
|
|
124
|
+
}
|
|
125
|
+
hasOnlyName(snapshot) {
|
|
126
|
+
return snapshot.name !== void 0 && snapshot.attributes === void 0 && snapshot.children === void 0;
|
|
127
|
+
}
|
|
128
|
+
hasOnlyChildren(snapshot) {
|
|
129
|
+
return snapshot.name === void 0 && snapshot.attributes === void 0 && snapshot.children !== void 0;
|
|
130
|
+
}
|
|
131
|
+
removeUndefinedProperties(attributes) {
|
|
132
|
+
const sparseAttributes = {};
|
|
133
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
134
|
+
if (value !== void 0) {
|
|
135
|
+
sparseAttributes[key] = value;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return Object.keys(sparseAttributes).length > 0 ? sparseAttributes : void 0;
|
|
139
|
+
}
|
|
140
|
+
transformedSnapshot(role, content) {
|
|
141
|
+
return { [role]: content };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/playwright/snapshot.ts
|
|
146
|
+
async function snapshotElement(locator, options) {
|
|
147
|
+
const snapshot = await snapshotElementRaw(locator);
|
|
148
|
+
return new ElementSnapshotTransformer(options).transform(snapshot);
|
|
149
|
+
}
|
|
150
|
+
async function snapshotElementRaw(locator) {
|
|
151
|
+
return await new ElementSnapshotProxy(locator.page()).snapshotElement(
|
|
152
|
+
locator
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
snapshotElement,
|
|
157
|
+
snapshotElementRaw
|
|
158
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cronn/element-snapshot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Element snapshots for Playwright",
|
|
5
5
|
"author": "cronn",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"packageManager": {
|
|
23
23
|
"name": "pnpm",
|
|
24
|
-
"version": ">=10.
|
|
24
|
+
"version": ">=10.27 <11",
|
|
25
25
|
"onFail": "error"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
@@ -29,27 +29,31 @@
|
|
|
29
29
|
"exports": {
|
|
30
30
|
".": {
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
|
-
"default": "./dist/index.
|
|
33
|
-
},
|
|
34
|
-
"./types": {
|
|
35
|
-
"types": "./dist/types.d.ts",
|
|
36
|
-
"default": "./dist/types.js"
|
|
32
|
+
"default": "./dist/index.js"
|
|
37
33
|
}
|
|
38
34
|
},
|
|
39
35
|
"files": [
|
|
40
36
|
"dist"
|
|
41
37
|
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"package-directory": "8.1.0"
|
|
40
|
+
},
|
|
42
41
|
"devDependencies": {
|
|
43
42
|
"@trivago/prettier-plugin-sort-imports": "6.0.0",
|
|
44
|
-
"
|
|
43
|
+
"@types/node": "24.10.4",
|
|
44
|
+
"eslint": "9.39.2",
|
|
45
45
|
"eslint-config-prettier": "10.1.8",
|
|
46
46
|
"eslint-plugin-unused-imports": "4.3.0",
|
|
47
|
-
"
|
|
47
|
+
"@playwright/test": "1.56.1",
|
|
48
|
+
"prettier": "3.7.4",
|
|
48
49
|
"tsup": "8.5.1",
|
|
49
50
|
"typescript": "5.9.3",
|
|
50
|
-
"typescript-eslint": "8.
|
|
51
|
+
"typescript-eslint": "8.50.1",
|
|
51
52
|
"@cronn/shared-configs": "0.0.0"
|
|
52
53
|
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@playwright/test": "^1.55"
|
|
56
|
+
},
|
|
53
57
|
"scripts": {
|
|
54
58
|
"lint:check": "eslint src/ --max-warnings=0",
|
|
55
59
|
"lint:fix": "eslint src/ --max-warnings=0 --fix",
|
package/dist/types.js
DELETED
|
File without changes
|