@cypress/unique-selector 2.1.1 → 2.1.2
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 +152 -71
- package/lib/index.js +15 -0
- package/lib/isShadowRoot.js +15 -0
- package/package.json +5 -6
package/README.md
CHANGED
|
@@ -1,115 +1,196 @@
|
|
|
1
|
-
unique-selector
|
|
2
|
-
===============
|
|
3
|
-
|
|
4
|
-
[](https://travis-ci.org/ericclemmons/unique-selector)
|
|
5
|
-
[]()
|
|
6
|
-
[]()
|
|
1
|
+
# unique-selector
|
|
7
2
|
|
|
8
3
|
Given a DOM node, return a unique CSS selector matching only that element.
|
|
9
|
-
This is particularly useful when tracking in custom variables in analytics:
|
|
10
4
|
|
|
5
|
+
Selector uniqueness is determined based on the given element's root node. Elements rendered within Shadow DOM will derive a selector unique within the associated ShadowRoot context. Otherwise, a selector unique within an element's owning document will be derived.
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
var selector = unique(event.target);
|
|
7
|
+
## Installation
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
}, false);
|
|
9
|
+

|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npm install @cypress/unique-selector
|
|
13
|
+
```
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
------------
|
|
15
|
+
## API
|
|
22
16
|
|
|
23
|
-
|
|
17
|
+
### `unique(element, options)`
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
------------
|
|
27
|
-
e.g.1 DomElement = `<span id="test"></span>`
|
|
19
|
+
Generates a unique CSS selector for the given DOM element.
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
import unique from 'unique-selector';
|
|
21
|
+
#### Parameters
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
options
|
|
34
|
-
// Array of selector types based on which the unique selector will generate
|
|
35
|
-
selectorTypes : [ 'ID', 'Class', 'Tag', 'NthChild' ]
|
|
36
|
-
}
|
|
23
|
+
- **`element`** (Element) - The DOM element for which to generate a unique selector
|
|
24
|
+
- **`options`** (Object, optional) - Configuration options for selector generation
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
```
|
|
26
|
+
#### Options
|
|
40
27
|
|
|
41
|
-
|
|
28
|
+
- **`selectorTypes`** (String[], optional) - Array of selector types to use in order of preference. Default: `['id', 'name', 'class', 'tag', 'nth-child']`
|
|
29
|
+
|
|
30
|
+
Available selector types:
|
|
31
|
+
- `'id'` - Uses element's ID attribute (e.g., `#myId`)
|
|
32
|
+
- `'name'` - Uses element's name attribute (e.g., `[name="myName"]`)
|
|
33
|
+
- `'class'` - Uses element's class attributes (e.g., `.myClass`)
|
|
34
|
+
- `'tag'` - Uses element's tag name (e.g., `div`, `span`)
|
|
35
|
+
- `'nth-child'` - Uses nth-child position (e.g., `:nth-child(1)`)
|
|
36
|
+
- `'attributes'` - Uses all attributes except those in `attributesToIgnore`
|
|
37
|
+
- `'data-*'` - Uses specific data attributes (e.g., `data-foo`, `data-test-id`)
|
|
38
|
+
- `'attribute:*'` - Uses specific attributes (e.g., `attribute:role`, `attribute:aria-label`)
|
|
39
|
+
|
|
40
|
+
- **`attributesToIgnore`** (String[], optional) - Array of attribute names to ignore when generating selectors. Default: `['id', 'class', 'length']`
|
|
41
|
+
|
|
42
|
+
- **`filter`** (Function, optional) - A filter function to conditionally reject various traits when building selectors.
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
function filter(type, key, value) {
|
|
46
|
+
// type: 'attribute', 'class', 'tag', 'nth-child'
|
|
47
|
+
// key: attribute name (for attributes), class name (for classes), etc.
|
|
48
|
+
// value: attribute value, class name, tag name, nth-child position
|
|
49
|
+
|
|
50
|
+
// Return true to allow this trait, false to reject it
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
import unique from 'unique-selector';
|
|
55
|
+
- **`selectorCache`** (Map<Element, String>, optional) - Cache to improve performance of repeated selector generation. The caller is responsible for cache invalidation.
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
options = {
|
|
48
|
-
// Array of selector types based on which the unique selector will be generate
|
|
49
|
-
selectorTypes : [ 'Attributes' ]
|
|
50
|
-
}
|
|
57
|
+
- **`isUniqueCache`** (Map<String, Boolean>, optional) - Cache to improve performance of repeated uniqueness checks. The caller is responsible for cache invalidation.
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
```
|
|
59
|
+
#### Returns
|
|
54
60
|
|
|
61
|
+
- **String** - A unique CSS selector for the element, or `null` if no unique selector can be generated
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
-----
|
|
63
|
+
## Usage Examples
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
### Basic Usage
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-----
|
|
64
|
-
Feel free to open issues, make suggestions or send PRs.
|
|
65
|
-
This project adheres to the Contributor Covenant [code of conduct](http://contributor-covenant.org/). By participating, you are expected to uphold this code.
|
|
67
|
+
```javascript
|
|
68
|
+
import unique from '@cypress/unique-selector';
|
|
66
69
|
|
|
70
|
+
// Get a unique selector for an element
|
|
71
|
+
const element = document.querySelector('.my-button');
|
|
72
|
+
const selector = unique(element);
|
|
73
|
+
// Returns: '#my-button' or '.my-button' or 'button' etc.
|
|
74
|
+
```
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
-----
|
|
76
|
+
### Custom Selector Types
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
```javascript
|
|
79
|
+
// Use only ID and class selectors
|
|
80
|
+
const selector = unique(element, {
|
|
81
|
+
selectorTypes: ['id', 'class']
|
|
82
|
+
});
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
// Use only data attributes
|
|
85
|
+
const selector = unique(element, {
|
|
86
|
+
selectorTypes: ['data-test-id', 'data-cy']
|
|
87
|
+
});
|
|
74
88
|
|
|
89
|
+
// Use specific attributes
|
|
90
|
+
const selector = unique(element, {
|
|
91
|
+
selectorTypes: ['attribute:role', 'attribute:aria-label']
|
|
92
|
+
});
|
|
93
|
+
```
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
### Filtering Selectors
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// Filter out certain IDs
|
|
99
|
+
const selector = unique(element, {
|
|
100
|
+
filter: (type, key, value) => {
|
|
101
|
+
if (type === 'attribute' && key === 'id') {
|
|
102
|
+
// Only allow IDs that contain 'test'
|
|
103
|
+
return value.includes('test');
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Filter out certain classes
|
|
110
|
+
const selector = unique(element, {
|
|
111
|
+
filter: (type, key, value) => {
|
|
112
|
+
if (type === 'class') {
|
|
113
|
+
// Only allow classes that start with 'btn-'
|
|
114
|
+
return value.startsWith('btn-');
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
```
|
|
78
120
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
121
|
+
### Performance Optimization with Caching
|
|
82
122
|
|
|
83
|
-
|
|
123
|
+
```javascript
|
|
124
|
+
const selectorCache = new Map();
|
|
125
|
+
const isUniqueCache = new Map();
|
|
84
126
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
const selector = unique(element, {
|
|
128
|
+
selectorCache,
|
|
129
|
+
isUniqueCache
|
|
130
|
+
});
|
|
89
131
|
|
|
132
|
+
// Reuse the same caches for multiple calls
|
|
133
|
+
const selector2 = unique(anotherElement, {
|
|
134
|
+
selectorCache,
|
|
135
|
+
isUniqueCache
|
|
136
|
+
});
|
|
137
|
+
```
|
|
90
138
|
|
|
91
|
-
|
|
139
|
+
### Shadow DOM Support
|
|
92
140
|
|
|
93
|
-
|
|
141
|
+
```javascript
|
|
142
|
+
// Works with Shadow DOM elements
|
|
143
|
+
const shadowHost = document.querySelector('#shadow-host');
|
|
144
|
+
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
|
|
145
|
+
const shadowElement = shadowRoot.querySelector('.shadow-button');
|
|
94
146
|
|
|
95
|
-
|
|
147
|
+
const selector = unique(shadowElement);
|
|
148
|
+
// Returns a selector unique within the shadow root context
|
|
149
|
+
```
|
|
96
150
|
|
|
97
|
-
|
|
151
|
+
## Selector Generation Strategy
|
|
98
152
|
|
|
99
|
-
|
|
153
|
+
The library follows this strategy to generate unique selectors:
|
|
100
154
|
|
|
101
|
-
|
|
155
|
+
1. **Element-level uniqueness**: First tries to find a selector that uniquely identifies the element within its parent
|
|
156
|
+
2. **Parent traversal**: If element-level uniqueness fails, traverses up the DOM tree, building a path of selectors
|
|
157
|
+
3. **Selector type precedence**: Uses the `selectorTypes` array to determine which selector types to try first
|
|
158
|
+
4. **Combination fallback**: For classes and attributes, tries combinations of multiple selectors if single selectors aren't unique
|
|
159
|
+
5. **Nth-child fallback**: Uses nth-child positioning as a last resort
|
|
102
160
|
|
|
103
|
-
|
|
161
|
+
## Examples of Generated Selectors
|
|
104
162
|
|
|
105
|
-
|
|
163
|
+
```html
|
|
164
|
+
<!-- Element with ID -->
|
|
165
|
+
<div id="header">Header</div>
|
|
166
|
+
<!-- Selector: #header -->
|
|
106
167
|
|
|
168
|
+
<!-- Element with unique class -->
|
|
169
|
+
<button class="submit-btn">Submit</button>
|
|
170
|
+
<!-- Selector: .submit-btn -->
|
|
107
171
|
|
|
172
|
+
<!-- Element with multiple classes -->
|
|
173
|
+
<div class="card primary large">Card</div>
|
|
174
|
+
<!-- Selector: .card.primary.large -->
|
|
108
175
|
|
|
109
|
-
|
|
110
|
-
|
|
176
|
+
<!-- Element with data attributes -->
|
|
177
|
+
<div data-test-id="user-profile">Profile</div>
|
|
178
|
+
<!-- Selector: [data-test-id="user-profile"] -->
|
|
111
179
|
|
|
112
|
-
|
|
180
|
+
<!-- Element with custom attributes -->
|
|
181
|
+
<button role="button" aria-label="Close">×</button>
|
|
182
|
+
<!-- Selector: [role="button"] or [aria-label="Close"] -->
|
|
113
183
|
|
|
184
|
+
<!-- Element requiring nth-child -->
|
|
185
|
+
<div class="item">Item 1</div>
|
|
186
|
+
<div class="item">Item 2</div>
|
|
187
|
+
<!-- Selector: :nth-child(1) or :nth-child(2) -->
|
|
114
188
|
|
|
115
|
-
|
|
189
|
+
<!-- Complex nested element -->
|
|
190
|
+
<div class="container">
|
|
191
|
+
<div class="wrapper">
|
|
192
|
+
<span class="text">Hello</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<!-- Selector: .container > .wrapper > .text -->
|
|
196
|
+
```
|
package/lib/index.js
CHANGED
|
@@ -25,6 +25,8 @@ var _getParents = require('./getParents');
|
|
|
25
25
|
|
|
26
26
|
var _getAttribute = require('./getAttribute');
|
|
27
27
|
|
|
28
|
+
var _isShadowRoot = require('./isShadowRoot');
|
|
29
|
+
|
|
28
30
|
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } /**
|
|
29
31
|
* Expose `unique`
|
|
30
32
|
*/
|
|
@@ -297,6 +299,19 @@ function unique(el) {
|
|
|
297
299
|
if (isUniqueCache) {
|
|
298
300
|
isUniqueCache.set(maybeUniqueSelector, isUniqueSelector);
|
|
299
301
|
}
|
|
302
|
+
|
|
303
|
+
// If the selector is not unique but we detect we've reached a ShadowRoot attempt
|
|
304
|
+
// to prefix the selector with `:host >` to anchor against the root. This helps
|
|
305
|
+
// address the possibility of a unique path segment not existing before reaching
|
|
306
|
+
// the root.
|
|
307
|
+
if (!isUniqueSelector && (0, _isShadowRoot.isShadowRoot)(el.parentNode)) {
|
|
308
|
+
maybeUniqueSelector = ':host > ' + maybeUniqueSelector;
|
|
309
|
+
isUniqueSelector = (0, _isUnique.isUnique)(el, maybeUniqueSelector);
|
|
310
|
+
|
|
311
|
+
if (isUniqueCache) {
|
|
312
|
+
isUniqueCache.set(maybeUniqueSelector, isUniqueSelector);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
if (isUniqueSelector) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.isShadowRoot = isShadowRoot;
|
|
7
|
+
/**
|
|
8
|
+
* Checks if the supplied node is a ShadowRoot
|
|
9
|
+
*
|
|
10
|
+
* @param { Object } element
|
|
11
|
+
* @return { Boolean }
|
|
12
|
+
*/
|
|
13
|
+
function isShadowRoot(node) {
|
|
14
|
+
return Object.getPrototypeOf(node).constructor.name === 'ShadowRoot';
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cypress/unique-selector",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Given a DOM node, return a unique CSS selector matching only that element",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"jsnext:main": "./src/index.js",
|
|
@@ -39,12 +39,11 @@
|
|
|
39
39
|
"babel-register": "^6.26.0",
|
|
40
40
|
"chai": "^4.3.4",
|
|
41
41
|
"component": "~1.1.0",
|
|
42
|
-
"eslint": "^8.
|
|
43
|
-
"eslint-config-airbnb": "^
|
|
44
|
-
"eslint-
|
|
45
|
-
"eslint-plugin-react": "^7.26.1",
|
|
42
|
+
"eslint": "^8.57.1",
|
|
43
|
+
"eslint-config-airbnb": "^19.0.0",
|
|
44
|
+
"eslint-plugin-react": "^7.37.5",
|
|
46
45
|
"jquery": "^3.6.0",
|
|
47
|
-
"jsdom": "^
|
|
46
|
+
"jsdom": "^27.4.0",
|
|
48
47
|
"mocha": "~9.1.3",
|
|
49
48
|
"mocha-jsdom": "^2.0.0",
|
|
50
49
|
"mocha-phantomjs": "~4.1.0",
|