-
Notifications
You must be signed in to change notification settings - Fork 336
Expand file tree
/
Copy pathcreate-element-selector.ts
More file actions
122 lines (100 loc) · 3.81 KB
/
Copy pathcreate-element-selector.ts
File metadata and controls
122 lines (100 loc) · 3.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import { isAcceptedAttr, findUniqueSelector } from "./find-unique-selector.js";
import { FINDER_TIMEOUT_MS, SELECTOR_ATTR_VALUE_MAX_LENGTH_CHARS } from "../constants.js";
const escapeCssIdentifier = (value: string): string => {
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
return CSS.escape(value);
}
return value.replace(/[^a-zA-Z0-9_-]/g, (character) => `\\${character}`);
};
const getFinderRoot = (element: Element): Element =>
element.ownerDocument.body ?? element.ownerDocument.documentElement;
const PREFERRED_SELECTOR_ATTRIBUTE_NAMES = new Set<string>([
"data-testid",
"data-test-id",
"data-test",
"data-cy",
"data-qa",
"aria-label",
"href",
"src",
"role",
"name",
"title",
"alt",
]);
const isPreferredAttributeValueSafe = (value: string): boolean =>
value.length > 0 && value.length <= SELECTOR_ATTR_VALUE_MAX_LENGTH_CHARS;
const isSelectorUniqueForElement = (element: Element, selector: string): boolean => {
try {
const matchingElements = element.ownerDocument.querySelectorAll(selector);
return matchingElements.length === 1 && matchingElements[0] === element;
} catch {
return false;
}
};
const createFastElementSelector = (element: Element): string | null => {
if (element instanceof HTMLElement && element.id) {
const idSelector = `#${escapeCssIdentifier(element.id)}`;
if (isSelectorUniqueForElement(element, idSelector)) return idSelector;
}
for (const attributeName of PREFERRED_SELECTOR_ATTRIBUTE_NAMES) {
const attributeValue = element.getAttribute(attributeName);
if (!attributeValue) continue;
if (!isPreferredAttributeValueSafe(attributeValue)) continue;
const quotedValue = JSON.stringify(attributeValue);
const attributeOnlySelector = `[${attributeName}=${quotedValue}]`;
if (isSelectorUniqueForElement(element, attributeOnlySelector)) {
return attributeOnlySelector;
}
const tagSelector = `${element.tagName.toLowerCase()}${attributeOnlySelector}`;
if (isSelectorUniqueForElement(element, tagSelector)) {
return tagSelector;
}
}
return null;
};
const createNthChildSelector = (element: Element): string => {
const segments: string[] = [];
const root = getFinderRoot(element);
let currentElement: Element | null = element;
while (currentElement) {
if (currentElement instanceof HTMLElement && currentElement.id) {
segments.unshift(`#${escapeCssIdentifier(currentElement.id)}`);
break;
}
const parentElement: HTMLElement | null = currentElement.parentElement;
if (!parentElement) {
segments.unshift(currentElement.tagName.toLowerCase());
break;
}
const siblings = Array.from(parentElement.children);
const siblingIndex = siblings.indexOf(currentElement);
const nthChild = siblingIndex >= 0 ? siblingIndex + 1 : 1;
segments.unshift(`${currentElement.tagName.toLowerCase()}:nth-child(${nthChild})`);
if (parentElement === root) {
segments.unshift(root.tagName.toLowerCase());
break;
}
currentElement = parentElement;
}
return segments.join(" > ");
};
export const createElementSelector = (element: Element): string => {
const fastSelector = createFastElementSelector(element);
if (fastSelector) return fastSelector;
try {
const selector = findUniqueSelector(
element,
getFinderRoot(element),
FINDER_TIMEOUT_MS,
(attributeName, attributeValue) =>
isAcceptedAttr(attributeName, attributeValue) ||
(PREFERRED_SELECTOR_ATTRIBUTE_NAMES.has(attributeName) &&
isPreferredAttributeValueSafe(attributeValue)),
);
if (selector) return selector;
// @medv/finder can throw on unusual DOM structures (SVG, web components,
// detached nodes), so we fall back to an nth-child selector instead.
} catch {}
return createNthChildSelector(element);
};