@dev-blinq/cucumber_client 1.0.1253-dev → 1.0.1255-dev
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/bin/assets/bundled_scripts/recorder.js +14132 -0
- package/bin/assets/scripts/aria_snapshot.js +235 -0
- package/bin/assets/scripts/dom_attr.js +372 -0
- package/bin/assets/scripts/dom_element.js +0 -0
- package/bin/assets/scripts/dom_parent.js +159 -0
- package/bin/assets/scripts/event_utils.js +105 -0
- package/bin/assets/scripts/pw.js +7886 -0
- package/bin/assets/scripts/recorder.js +1146 -0
- package/bin/assets/scripts/snapshot_capturer.js +155 -0
- package/bin/assets/scripts/unique_locators.js +552 -0
- package/bin/assets/scripts/yaml.js +4770 -0
- package/bin/client/recorderv3/bvt_recorder.js +23 -53
- package/package.json +8 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
class SnapshotCapturer {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
const {
|
|
4
|
+
inlineImages = true,
|
|
5
|
+
inlineStyles = true,
|
|
6
|
+
excludeSelectors = []
|
|
7
|
+
} = options;
|
|
8
|
+
// this.options = {
|
|
9
|
+
// inlineImages,
|
|
10
|
+
// inlineStyles,
|
|
11
|
+
// excludeSelectors
|
|
12
|
+
// };
|
|
13
|
+
this.inlineImages = inlineImages;
|
|
14
|
+
this.inlineStyles = inlineStyles;
|
|
15
|
+
this.excludeSelectors = excludeSelectors;
|
|
16
|
+
}
|
|
17
|
+
imageToDataURL(img, document) {
|
|
18
|
+
try {
|
|
19
|
+
// Create canvas to draw the image
|
|
20
|
+
const canvas = document.createElement('canvas');
|
|
21
|
+
canvas.width = img.naturalWidth || img.width;
|
|
22
|
+
canvas.height = img.naturalHeight || img.height;
|
|
23
|
+
const ctx = canvas.getContext('2d');
|
|
24
|
+
|
|
25
|
+
// Draw image to canvas and convert to data URL
|
|
26
|
+
ctx.drawImage(img, 0, 0);
|
|
27
|
+
return canvas.toDataURL('image/png');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.warn(`Failed to inline image: ${img.src}`, error);
|
|
30
|
+
return img.src; // Fall back to original source
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
processStyles(document) {
|
|
34
|
+
if (!this.inlineStyles) return;
|
|
35
|
+
|
|
36
|
+
const stylesheets = Array.from(document.styleSheets);
|
|
37
|
+
for (const sheet of stylesheets) {
|
|
38
|
+
try {
|
|
39
|
+
if (!sheet.href) continue; // Skip inline styles
|
|
40
|
+
const styleEl = document.createElement('style');
|
|
41
|
+
let text = ""
|
|
42
|
+
const cssRules = Array.from(sheet.cssRules || []);
|
|
43
|
+
for (const rule of cssRules) {
|
|
44
|
+
if (rule.cssText) {
|
|
45
|
+
text += rule.cssText + '\n';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
styleEl.textContent = text;
|
|
49
|
+
|
|
50
|
+
// Replace the link with our new style element
|
|
51
|
+
if (sheet.ownerNode && sheet.ownerNode.parentNode) {
|
|
52
|
+
sheet.ownerNode.parentNode.replaceChild(styleEl, sheet.ownerNode);
|
|
53
|
+
} else if (sheet.ownerNode) {
|
|
54
|
+
// If the owner node is not in the document, just append it
|
|
55
|
+
document.head.appendChild(styleEl);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn(`Error processing stylesheet: ${sheet.href}`, error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
processImages(document) {
|
|
64
|
+
if (!this.inlineImages) return;
|
|
65
|
+
|
|
66
|
+
const images = document.querySelectorAll('img');
|
|
67
|
+
images.forEach(img => {
|
|
68
|
+
// Skip SVGs and already data URLs
|
|
69
|
+
if (img.src.startsWith('data:') || img.src.endsWith('.svg')) return;
|
|
70
|
+
|
|
71
|
+
// Skip if the image is excluded
|
|
72
|
+
if (this.excludeSelectors.some(selector => img.matches(selector))) return;
|
|
73
|
+
|
|
74
|
+
// Only inline complete images
|
|
75
|
+
if (img.complete && img.naturalWidth !== 0) {
|
|
76
|
+
try {
|
|
77
|
+
img.setAttribute('src', this.imageToDataURL(img, document));
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn(`Failed to process image: ${img.src}`, e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
removeExcludedElements(document) {
|
|
85
|
+
this.excludeSelectors.forEach(selector => {
|
|
86
|
+
const elements = document.querySelectorAll(selector);
|
|
87
|
+
elements.forEach(el => {
|
|
88
|
+
el.parentNode?.removeChild(el);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
processComputedStyles(document) {
|
|
93
|
+
const elements = document.querySelectorAll('*');
|
|
94
|
+
|
|
95
|
+
elements.forEach(el => {
|
|
96
|
+
// Skip excluded elements
|
|
97
|
+
if (this.excludeSelectors.some(selector => el.matches(selector))) return;
|
|
98
|
+
|
|
99
|
+
// Get computed style
|
|
100
|
+
const style = window.getComputedStyle(el);
|
|
101
|
+
|
|
102
|
+
// Copy important styles to inline style
|
|
103
|
+
const importantStyles = [
|
|
104
|
+
'display', 'position', 'width', 'height', 'margin', 'padding',
|
|
105
|
+
'color', 'background-color', 'font-family', 'font-size',
|
|
106
|
+
'text-align', 'line-height', 'border', 'box-shadow', 'opacity'
|
|
107
|
+
// Add more styles as needed
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const inlineStyles = [];
|
|
111
|
+
|
|
112
|
+
importantStyles.forEach(prop => {
|
|
113
|
+
const value = style.getPropertyValue(prop);
|
|
114
|
+
if (value) {
|
|
115
|
+
inlineStyles.push(`${prop}: ${value}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Set inline style
|
|
120
|
+
if (inlineStyles.length > 0) {
|
|
121
|
+
const currentStyle = el.getAttribute('style') || '';
|
|
122
|
+
el.setAttribute('style', currentStyle + inlineStyles.join('; ') + ';');
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
createSnapshot() {
|
|
128
|
+
// Clone the document to avoid modifying the original
|
|
129
|
+
const docClone = window.document.cloneNode(true);
|
|
130
|
+
|
|
131
|
+
// Store the original document
|
|
132
|
+
const originalDoc = window.document;
|
|
133
|
+
|
|
134
|
+
// Temporarily "swap" the document for processing
|
|
135
|
+
// (We're just using this as a convention - it doesn't actually replace the global document)
|
|
136
|
+
const doc = docClone;
|
|
137
|
+
|
|
138
|
+
// Process the clone
|
|
139
|
+
this.processStyles(doc);
|
|
140
|
+
this.processImages(doc);
|
|
141
|
+
this.removeExcludedElements(doc);
|
|
142
|
+
this.processComputedStyles(doc);
|
|
143
|
+
|
|
144
|
+
// Generate HTML with doctype
|
|
145
|
+
const doctype = originalDoc.doctype ?
|
|
146
|
+
new XMLSerializer().serializeToString(originalDoc.doctype) : '<!DOCTYPE html>';
|
|
147
|
+
|
|
148
|
+
// Get the HTML content
|
|
149
|
+
const htmlContent = doc.documentElement.outerHTML;
|
|
150
|
+
|
|
151
|
+
// Combine doctype and HTML content
|
|
152
|
+
return `${doctype}${htmlContent}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export default SnapshotCapturer;
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
|
|
2
|
+
import DOM_Attr from "./dom_attr";
|
|
3
|
+
import DOM_Parent from "./dom_parent";
|
|
4
|
+
// import SnapshotCapturer from "./snapshot_capturer";
|
|
5
|
+
import { __PW } from "./pw"
|
|
6
|
+
|
|
7
|
+
class LocatorGenerator {
|
|
8
|
+
constructor(injectedScript, options = {}) {
|
|
9
|
+
this.locatorStrategies = {
|
|
10
|
+
text: "basic",
|
|
11
|
+
no_text: "no_text",
|
|
12
|
+
custom: "custom",
|
|
13
|
+
context: "context",
|
|
14
|
+
digitIgnore: "ignore_digit",
|
|
15
|
+
text_with_index: "text_with_index",
|
|
16
|
+
};
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.dom_Attr = new DOM_Attr();
|
|
19
|
+
this.dom_Parent = new DOM_Parent();
|
|
20
|
+
this.PW = __PW;
|
|
21
|
+
this.injectedScript = injectedScript;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getMatchingElements(selector, options = {}) {
|
|
25
|
+
const { root = window.document, prefix, visible = true } = options;
|
|
26
|
+
if (visible) {
|
|
27
|
+
selector = `${selector} >> visible=true`;
|
|
28
|
+
}
|
|
29
|
+
if (prefix) {
|
|
30
|
+
selector = `${prefix} >> ${selector}`;
|
|
31
|
+
}
|
|
32
|
+
return this.injectedScript.querySelectorAll(
|
|
33
|
+
this.injectedScript.parseSelector(selector),
|
|
34
|
+
root)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getLocatorStrategies() {
|
|
38
|
+
return this.locatorStrategies;
|
|
39
|
+
}
|
|
40
|
+
getTextLocators(element, options) {
|
|
41
|
+
const injectedScript = this.injectedScript;
|
|
42
|
+
const selectorPartLists = this.PW.selectorGenerator.buildTextCandidates(injectedScript, element, options);
|
|
43
|
+
const result = [];
|
|
44
|
+
for (const selectorPartList of selectorPartLists) {
|
|
45
|
+
let tScore = 0;
|
|
46
|
+
const tSelectorList = [];
|
|
47
|
+
for (const selectorPart of selectorPartList) {
|
|
48
|
+
const { engine, selector } = selectorPart;
|
|
49
|
+
if (engine === "css") {
|
|
50
|
+
tSelectorList.push(selector);
|
|
51
|
+
} else {
|
|
52
|
+
tSelectorList.push(`${engine}=${selector}`);
|
|
53
|
+
}
|
|
54
|
+
tScore += selectorPart.score;
|
|
55
|
+
}
|
|
56
|
+
const selector = tSelectorList.join(" >> ");
|
|
57
|
+
const score = tScore / selectorPartList.length;
|
|
58
|
+
result.push({
|
|
59
|
+
css: selector,
|
|
60
|
+
score,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
getNoTextLocators(element, options) {
|
|
66
|
+
const injectedScript = this.injectedScript;
|
|
67
|
+
const locators = this.PW.selectorGenerator.buildNoTextCandidates(injectedScript, element, options);
|
|
68
|
+
for (const locator of locators) {
|
|
69
|
+
if (locator.engine === "css") {
|
|
70
|
+
locator.css = locator.selector;
|
|
71
|
+
} else {
|
|
72
|
+
locator.css = `${locator.engine}=${locator.selector}`;
|
|
73
|
+
}
|
|
74
|
+
delete locator.engine; // remove engine to avoid memory leak
|
|
75
|
+
delete locator.selector; // remove selector to avoid memory leak
|
|
76
|
+
}
|
|
77
|
+
return locators;
|
|
78
|
+
}
|
|
79
|
+
getCustomLocators(element, options) {
|
|
80
|
+
const { customAttributes = [] } = this.options;
|
|
81
|
+
if (!customAttributes || !Array.isArray(customAttributes)) {
|
|
82
|
+
console.error("Custom attributes must be an array");
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = [];
|
|
87
|
+
let hasAttribures = []
|
|
88
|
+
for (const customAttribute of customAttributes) {
|
|
89
|
+
if (!customAttribute || typeof customAttribute !== "string") {
|
|
90
|
+
console.error("Custom attribute must be a string");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const val = element.getAttribute(customAttribute);
|
|
94
|
+
if (val !== null) {
|
|
95
|
+
hasAttribures.push(customAttribute);
|
|
96
|
+
result.push({
|
|
97
|
+
css: `[${customAttribute}="${val}"]`,
|
|
98
|
+
score: 1,
|
|
99
|
+
priority: 1,
|
|
100
|
+
customAttribute,
|
|
101
|
+
value: val
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
getContextLocators(locators) {
|
|
108
|
+
|
|
109
|
+
if (!locators || !Array.isArray(locators)) {
|
|
110
|
+
console.error("Locators must be an array");
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const result = [];
|
|
114
|
+
const textSet = new Set();
|
|
115
|
+
for (const locator of locators) {
|
|
116
|
+
const selector = locator.css;
|
|
117
|
+
if (!selector || typeof selector !== "string") {
|
|
118
|
+
console.error("Locator must have a valid css selector");
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const parseResult = this.injectedScript.parseSelector(selector);
|
|
122
|
+
const parts = parseResult.parts;
|
|
123
|
+
if (!parts || !Array.isArray(parts) || parts.length === 0) {
|
|
124
|
+
console.error("Locator must have a valid css selector");
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// ignore parts.length < 3
|
|
128
|
+
if (parts.length < 3) {
|
|
129
|
+
// console.warn("Locator must have at least 3 parts to be a context locator");
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const firstPart = parts[0];
|
|
133
|
+
if (firstPart.name !== "internal:text") {
|
|
134
|
+
// console.warn("Locator must have internal:text as the first part to be a context locator");
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const textBody = firstPart.body;
|
|
138
|
+
if (!textBody || typeof textBody !== "string" || textBody.length === 0) {
|
|
139
|
+
console.error("Locator must have a valid text in the first part to be a context locator");
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const secondPart = parts[1];
|
|
143
|
+
if (secondPart.name !== "xpath") {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const xpath = secondPart.body;
|
|
147
|
+
if (!xpath || typeof xpath !== "string" || xpath.length === 0) {
|
|
148
|
+
// console.error("Locator must have a valid xpath in the second part to be a context locator");
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const climbString = secondPart.body;
|
|
152
|
+
if (!climbString || typeof climbString !== "string" || climbString.length === 0) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const climbStringRegex = /(\.\.)(\/\.\.)*/;
|
|
156
|
+
try {
|
|
157
|
+
const match = climbStringRegex.test(climbString);
|
|
158
|
+
if (match) {
|
|
159
|
+
const climbCount = climbString.split("..").length - 1;
|
|
160
|
+
const lastIndex = selector.indexOf(climbString);
|
|
161
|
+
const restOfSelector = selector.substring(lastIndex + climbString.length + 3).trim();
|
|
162
|
+
if (restOfSelector.length === 0) {
|
|
163
|
+
// console.warn("Locator must have a valid rest of selector after the xpath part to
|
|
164
|
+
// be a context locator");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const textLocator = `internal:text=${textBody}`;
|
|
169
|
+
const elements = this.getMatchingElements(textLocator, {});
|
|
170
|
+
if (elements.length !== 1) {
|
|
171
|
+
throw new Error("Context locator must have exactly one element matching the text part");
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
const textElement = elements[0];
|
|
175
|
+
// const text = this.PW.selectorUtils.elementText(textElement);
|
|
176
|
+
const text = this.injectedScript.utils.elementText(new Map(), textElement).full;
|
|
177
|
+
if (!textSet.has(text)) {
|
|
178
|
+
textSet.add(text);
|
|
179
|
+
result.push({
|
|
180
|
+
css: restOfSelector,
|
|
181
|
+
climb: climbCount,
|
|
182
|
+
text,
|
|
183
|
+
priority: 1,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error("Error parsing climb string:", error);
|
|
192
|
+
continue;
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
// Sort by text length to prioritize shorter texts
|
|
198
|
+
result.sort((a, b) => a.text.length - b.text.length);
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
getDigitIgnoreLocators(locators) {
|
|
202
|
+
const result = [];
|
|
203
|
+
if (!locators || !Array.isArray(locators)) {
|
|
204
|
+
console.error("Locators must be an array");
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const locator of locators) {
|
|
209
|
+
const selector = locator.css;
|
|
210
|
+
if (!selector || typeof selector !== "string") {
|
|
211
|
+
console.error("Locator must have a valid css selector");
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const parseresult = this.injectedScript.parseSelector(selector);
|
|
215
|
+
const parts = parseresult.parts;
|
|
216
|
+
if (!parts || !Array.isArray(parts) || parts.length === 0) {
|
|
217
|
+
console.error("Locator must have a valid css selector");
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
let finalSelector = "";
|
|
221
|
+
let hasDigitsInText = false;
|
|
222
|
+
for (const part of parts) {
|
|
223
|
+
|
|
224
|
+
if (part.name !== "internal:text") {
|
|
225
|
+
finalSelector += `${part.name === "css" ? "" : part.name}=${part.source} >> `;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (typeof part.body !== "string" || part.body.length === 0) {
|
|
229
|
+
// console.error("Locator must have a valid text in the first part to be a digit ignore locator");
|
|
230
|
+
finalSelector += `${part.name === "css" ? "" : part.name}=${part.source} >> `;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const text = part.body;
|
|
234
|
+
const digitsRegex = /\d+/g;
|
|
235
|
+
hasDigitsInText = digitsRegex.test(text);
|
|
236
|
+
|
|
237
|
+
const regex = text.replace(/\d+/g, "\\d+");
|
|
238
|
+
|
|
239
|
+
finalSelector += `internal:text=${regex} >> `;
|
|
240
|
+
|
|
241
|
+
}
|
|
242
|
+
if (!hasDigitsInText) {
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (finalSelector.endsWith(` >> `)) {
|
|
246
|
+
finalSelector = finalSelector.slice(0, -4);
|
|
247
|
+
}
|
|
248
|
+
if (finalSelector) {
|
|
249
|
+
result.push({
|
|
250
|
+
css: finalSelector,
|
|
251
|
+
priority: locator.priority || 1,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
getTextwithIndexLocators(locators) {
|
|
258
|
+
if (!locators || !Array.isArray(locators)) {
|
|
259
|
+
console.error("Locators must be an array");
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
const result = [];
|
|
263
|
+
for (const locator of locators) {
|
|
264
|
+
if (!locator || !locator.css || typeof locator.css !== "string") {
|
|
265
|
+
console.error("Locator must have a valid css selector");
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const index = locator.index;
|
|
269
|
+
if (typeof index !== "number" || index < 0) {
|
|
270
|
+
console.error("Locator must have a valid index");
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
result.push(locator);
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
getXPathSelector(climb) {
|
|
279
|
+
if (typeof climb !== "number" || climb < 0) {
|
|
280
|
+
// throw new Error("Climb must be a non-negative integer");
|
|
281
|
+
console.error("Climb must be a non-negative integer");
|
|
282
|
+
return "";
|
|
283
|
+
}
|
|
284
|
+
if (climb === 0) return "";
|
|
285
|
+
let selector = "xpath=..";
|
|
286
|
+
if (climb === 1) {
|
|
287
|
+
return selector;
|
|
288
|
+
}
|
|
289
|
+
for (let i = 1; i < climb; i++) {
|
|
290
|
+
selector += "/..";
|
|
291
|
+
}
|
|
292
|
+
return selector;
|
|
293
|
+
|
|
294
|
+
}
|
|
295
|
+
categorizeLocators(element, locators, options) {
|
|
296
|
+
const unique = [];
|
|
297
|
+
const nonUnique = [];
|
|
298
|
+
for (const locator of locators) {
|
|
299
|
+
const elements = this.getMatchingElements(locator.css, options);
|
|
300
|
+
if (elements.length === 1) {
|
|
301
|
+
if (element === elements[0]) {
|
|
302
|
+
locator.priority = 1;
|
|
303
|
+
unique.push(locator);
|
|
304
|
+
} else if (element.contains(elements[0])) {
|
|
305
|
+
locator.priority = 1;
|
|
306
|
+
const climb = this.dom_Parent.getClimbCountToParent(elements[0], element);
|
|
307
|
+
const climbSelector = this.getXPathSelector(climb);
|
|
308
|
+
const newSelector = `${locator.css} >> ${climbSelector}`;
|
|
309
|
+
locator.css = newSelector;
|
|
310
|
+
unique.push(locator);
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
locator.priority = 2;
|
|
314
|
+
locator.elements = elements;
|
|
315
|
+
nonUnique.push(locator);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { unique, nonUnique };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
getUniqueLocators(element, locatorGenerator = this.getNoTextLocators, options = {}) {
|
|
325
|
+
try {
|
|
326
|
+
|
|
327
|
+
const { maxLocators = 5, root = window.document.body, next = "LCA" } = options;
|
|
328
|
+
|
|
329
|
+
if (!element) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
if (element === root) {
|
|
333
|
+
if (element === window.document.documentElement) {
|
|
334
|
+
return [{
|
|
335
|
+
css: "html",
|
|
336
|
+
score: 1,
|
|
337
|
+
priority: 1,
|
|
338
|
+
}, {
|
|
339
|
+
css: ":root",
|
|
340
|
+
score: 1,
|
|
341
|
+
priority: 1,
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
} else {
|
|
345
|
+
return [{
|
|
346
|
+
css: ":root",
|
|
347
|
+
score: 1,
|
|
348
|
+
priority: 1,
|
|
349
|
+
// }, {
|
|
350
|
+
// css: ":root",
|
|
351
|
+
// score: 1,
|
|
352
|
+
// priority: 1,
|
|
353
|
+
}
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log("Generating locators for element:", element);
|
|
359
|
+
const locators = locatorGenerator(element, options);
|
|
360
|
+
console.log("Generated locators:", locators);
|
|
361
|
+
if (!locators || !Array.isArray(locators)) {
|
|
362
|
+
// throw new Error("Locator generator did not return an array of locators");
|
|
363
|
+
console.error("Locator generator did not return an array of locators");
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log("Categorizing locators for element:", element);
|
|
368
|
+
const categorizedLocators = this.categorizeLocators(element, locators, options);
|
|
369
|
+
console.log("Categorized locators:", categorizedLocators);
|
|
370
|
+
// categorizedLocators.unique = limitLocators(categorizedLocators.unique, options);
|
|
371
|
+
// categorizedLocators.nonUnique = limitLocators(categorizedLocators.nonUnique, options);
|
|
372
|
+
|
|
373
|
+
const { unique, nonUnique } = categorizedLocators;
|
|
374
|
+
const result = [];
|
|
375
|
+
if (unique.length > 0) {
|
|
376
|
+
result.push(...unique);
|
|
377
|
+
}
|
|
378
|
+
if (result.length >= maxLocators) {
|
|
379
|
+
return result.slice(0, maxLocators);
|
|
380
|
+
}
|
|
381
|
+
let nextElement = null;
|
|
382
|
+
for (const locator of nonUnique) {
|
|
383
|
+
const selector = locator.css ?? locator.selector;
|
|
384
|
+
const elements = locator.elements || this.getMatchingElements(selector, options);
|
|
385
|
+
|
|
386
|
+
if (next === "parent") {
|
|
387
|
+
nextElement = this.dom_Parent.getActualParent(element);
|
|
388
|
+
} else {
|
|
389
|
+
|
|
390
|
+
// find the branching element the child of common parent that contains the element
|
|
391
|
+
const branchingParent = this.dom_Parent.findBranchingParent(elements, element)
|
|
392
|
+
nextElement = branchingParent;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (nextElement && nextElement !== element) {
|
|
396
|
+
if (root.contains(nextElement)) {
|
|
397
|
+
const _result = this.getUniqueLocators(nextElement, locatorGenerator, {
|
|
398
|
+
...options,
|
|
399
|
+
root,
|
|
400
|
+
});
|
|
401
|
+
for (const _locator of _result) {
|
|
402
|
+
|
|
403
|
+
if (result.length >= maxLocators) {
|
|
404
|
+
return result.slice(0, maxLocators);
|
|
405
|
+
}
|
|
406
|
+
const _selector = _locator.css ?? _locator.selector;
|
|
407
|
+
const fullSelector = `${_selector} >> ${selector}`;
|
|
408
|
+
const _elements = this.getMatchingElements(fullSelector, options);
|
|
409
|
+
const effectiveScore = (_locator.score + locator.score) / 2 + 100;
|
|
410
|
+
if (_elements.length === 1 && _elements[0] === element) {
|
|
411
|
+
_locator.css = fullSelector;
|
|
412
|
+
_locator.score = effectiveScore;
|
|
413
|
+
_locator.priority = 1; // unique locators have higher priority
|
|
414
|
+
result.push(_locator);
|
|
415
|
+
} else {
|
|
416
|
+
const index = _elements.indexOf(element);
|
|
417
|
+
if (index !== -1) {
|
|
418
|
+
// _locator.selector = fullSelector;
|
|
419
|
+
_locator.css = fullSelector;
|
|
420
|
+
_locator.index = index;
|
|
421
|
+
_locator.priority = 2; // non-unique locators have lower priority
|
|
422
|
+
_locator.score = effectiveScore;
|
|
423
|
+
result.push(_locator);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
} else {
|
|
430
|
+
const index = elements.indexOf(element);
|
|
431
|
+
if (index !== -1) {
|
|
432
|
+
locator.index = index
|
|
433
|
+
locator.priority = 2; // non-unique locators have lower priority
|
|
434
|
+
result.push(locator);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
} else {
|
|
439
|
+
const index = elements.indexOf(element);
|
|
440
|
+
if (index !== -1) {
|
|
441
|
+
locator.index = index
|
|
442
|
+
locator.priority = 2; // non-unique locators have lower priority
|
|
443
|
+
result.push(locator);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
delete locator.elements; // remove elements to avoid memory leak
|
|
448
|
+
delete locator.strategy;
|
|
449
|
+
delete locator.engine;
|
|
450
|
+
delete locator.selector;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (result.length === 0) {
|
|
454
|
+
const parent = this.dom_Parent.getActualParent(element);
|
|
455
|
+
const locs = this.getUniqueLocators(parent, locatorGenerator, {
|
|
456
|
+
...options,
|
|
457
|
+
root
|
|
458
|
+
});
|
|
459
|
+
result.push(...locs);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
result.sort((a, b) => a.score - b.score);
|
|
464
|
+
console.log("Final locators:", result, element);
|
|
465
|
+
console.groupEnd();
|
|
466
|
+
return result.slice(0, maxLocators);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
console.error("Error in getUniqueLocators:", error);
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
getElementLocators(element, options = {}) {
|
|
474
|
+
|
|
475
|
+
const { excludeText = false } = options;
|
|
476
|
+
|
|
477
|
+
const allStrategyLocators = {
|
|
478
|
+
// [LOCATOR_STRATEGIES.text]: basicLocators,
|
|
479
|
+
// [LOCATOR_STRATEGIES.no_text]: noTextLocators,
|
|
480
|
+
}
|
|
481
|
+
if (this.locatorStrategies.options?.customAttributes) {
|
|
482
|
+
console.groupCollapsed("Generating Custom locators for element:", element);
|
|
483
|
+
const customLocators = this.getUniqueLocators(element, this.getCustomLocators.bind(this), options);
|
|
484
|
+
if (customLocators.length > 0) {
|
|
485
|
+
allStrategyLocators[this.locatorStrategies.custom] = customLocators;
|
|
486
|
+
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
console.groupEnd();
|
|
490
|
+
if (!excludeText) {
|
|
491
|
+
console.groupCollapsed("Generating Text locators for element:", element);
|
|
492
|
+
const basicLocators = this.getUniqueLocators(element, this.getTextLocators.bind(this), options);
|
|
493
|
+
console.groupEnd();
|
|
494
|
+
if (basicLocators.length > 0) {
|
|
495
|
+
allStrategyLocators[this.locatorStrategies.text] = basicLocators;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const textWithIndexLocators = this.getTextwithIndexLocators(basicLocators);
|
|
499
|
+
if (textWithIndexLocators.length > 0) {
|
|
500
|
+
allStrategyLocators[this.locatorStrategies.text_with_index] = textWithIndexLocators;
|
|
501
|
+
}
|
|
502
|
+
const digitIgnoreLocators = this.getDigitIgnoreLocators(basicLocators);
|
|
503
|
+
if (digitIgnoreLocators.length > 0) {
|
|
504
|
+
allStrategyLocators[this.locatorStrategies.digitIgnore] = digitIgnoreLocators;
|
|
505
|
+
}
|
|
506
|
+
const contextLocators = this.getContextLocators(basicLocators);
|
|
507
|
+
if (contextLocators.length > 0) {
|
|
508
|
+
allStrategyLocators[this.locatorStrategies.context] = contextLocators;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
console.groupCollapsed("Generating No Text locators for element:", element);
|
|
512
|
+
const noTextLocators = this.getUniqueLocators(element, this.getNoTextLocators.bind(this), options);
|
|
513
|
+
if (noTextLocators.length > 0) {
|
|
514
|
+
allStrategyLocators[this.locatorStrategies.no_text] = noTextLocators;
|
|
515
|
+
}
|
|
516
|
+
console.groupEnd();
|
|
517
|
+
|
|
518
|
+
let bestStrategy = this.getBestStrategy(allStrategyLocators);
|
|
519
|
+
if (!bestStrategy) {
|
|
520
|
+
console.warn("No locators found for element:", element);
|
|
521
|
+
bestStrategy = this.locatorStrategies.no_text;
|
|
522
|
+
}
|
|
523
|
+
allStrategyLocators.strategy = bestStrategy;
|
|
524
|
+
|
|
525
|
+
const locators = allStrategyLocators[allStrategyLocators.strategy];
|
|
526
|
+
const result = {
|
|
527
|
+
allStrategyLocators,
|
|
528
|
+
locators,
|
|
529
|
+
element_name: ""
|
|
530
|
+
}
|
|
531
|
+
console.log("Generated locators:", result);
|
|
532
|
+
return result
|
|
533
|
+
}
|
|
534
|
+
getBestStrategy(allStrategyLocators) {
|
|
535
|
+
const orderedPriorities = [
|
|
536
|
+
this.locatorStrategies.custom,
|
|
537
|
+
this.locatorStrategies.context,
|
|
538
|
+
this.locatorStrategies.text,
|
|
539
|
+
this.locatorStrategies.text_with_index,
|
|
540
|
+
this.locatorStrategies.digitIgnore,
|
|
541
|
+
this.locatorStrategies.no_text
|
|
542
|
+
];
|
|
543
|
+
for (const strategy of orderedPriorities) {
|
|
544
|
+
if (allStrategyLocators[strategy] && allStrategyLocators[strategy].length > 0) {
|
|
545
|
+
return strategy;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export default LocatorGenerator;
|