@dev-blinq/cucumber_client 1.0.1339-dev → 1.0.1339-stage

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.
Files changed (53) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +110 -110
  2. package/bin/assets/preload/css_gen.js +10 -10
  3. package/bin/assets/preload/recorderv3.js +3 -1
  4. package/bin/assets/preload/toolbar.js +27 -29
  5. package/bin/assets/preload/unique_locators.js +1 -1
  6. package/bin/assets/preload/yaml.js +288 -275
  7. package/bin/assets/scripts/aria_snapshot.js +223 -220
  8. package/bin/assets/scripts/dom_attr.js +329 -329
  9. package/bin/assets/scripts/dom_parent.js +169 -174
  10. package/bin/assets/scripts/event_utils.js +94 -94
  11. package/bin/assets/scripts/pw.js +2050 -1949
  12. package/bin/assets/scripts/recorder.js +5 -17
  13. package/bin/assets/scripts/snapshot_capturer.js +153 -146
  14. package/bin/assets/scripts/unique_locators.js +940 -815
  15. package/bin/assets/scripts/yaml.js +796 -783
  16. package/bin/assets/templates/_hooks_template.txt +41 -0
  17. package/bin/assets/templates/utils_template.txt +2 -45
  18. package/bin/client/apiTest/apiTest.js +6 -0
  19. package/bin/client/cli_helpers.js +11 -13
  20. package/bin/client/code_cleanup/utils.js +5 -1
  21. package/bin/client/code_gen/api_codegen.js +2 -2
  22. package/bin/client/code_gen/code_inversion.js +112 -4
  23. package/bin/client/code_gen/page_reflection.js +839 -906
  24. package/bin/client/code_gen/playwright_codeget.js +26 -18
  25. package/bin/client/cucumber/feature.js +89 -27
  26. package/bin/client/cucumber/feature_data.js +2 -2
  27. package/bin/client/cucumber/project_to_document.js +9 -3
  28. package/bin/client/cucumber/steps_definitions.js +6 -3
  29. package/bin/client/cucumber_selector.js +17 -1
  30. package/bin/client/local_agent.js +6 -5
  31. package/bin/client/parse_feature_file.js +23 -26
  32. package/bin/client/playground/projects/env.json +2 -2
  33. package/bin/client/project.js +186 -196
  34. package/bin/client/recorderv3/bvt_recorder.js +136 -81
  35. package/bin/client/recorderv3/implemented_steps.js +24 -14
  36. package/bin/client/recorderv3/index.js +59 -54
  37. package/bin/client/recorderv3/network.js +22 -5
  38. package/bin/client/recorderv3/scriptTest.js +1 -1
  39. package/bin/client/recorderv3/services.js +4 -16
  40. package/bin/client/recorderv3/step_runner.js +318 -209
  41. package/bin/client/recorderv3/step_utils.js +476 -17
  42. package/bin/client/recorderv3/update_feature.js +32 -30
  43. package/bin/client/recording.js +1 -0
  44. package/bin/client/run_cucumber.js +1 -1
  45. package/bin/client/scenario_report.js +0 -5
  46. package/bin/client/test_scenario.js +0 -1
  47. package/bin/client/upload-service.js +3 -2
  48. package/bin/client/utils/socket_logger.js +132 -0
  49. package/bin/index.js +1 -0
  50. package/bin/logger.js +3 -2
  51. package/bin/min/consoleApi.min.cjs +2 -3
  52. package/bin/min/injectedScript.min.cjs +16 -16
  53. package/package.json +21 -12
@@ -1,834 +1,959 @@
1
1
  import DOM_Parent from "./dom_parent";
2
2
  import { __PW } from "./pw";
3
3
  const candidateAttributes = [
4
- "slot",
5
- "alt",
6
- "name",
7
- "placeholder",
8
- "title",
9
- "aria-label",
10
- "aria-placeholder",
11
- "role",
12
- "data-testid",
13
- "data-cy",
4
+ "slot",
5
+ "alt",
6
+ "name",
7
+ "placeholder",
8
+ "title",
9
+ "aria-label",
10
+ "aria-placeholder",
11
+ "role",
12
+ "data-testid",
13
+ "data-cy",
14
14
  ];
15
15
  function quoteCSSAttributeValue(text) {
16
- return `"${text.replace(/["\\]/g, (char) => "\\" + char)}"`;
16
+ return `"${text.replace(/["\\]/g, (char) => "\\" + char)}"`;
17
17
  }
18
18
  function makeSelectorForId(id) {
19
- return /^[a-zA-Z][a-zA-Z0-9\-\_]+$/.test(id) ? "#" + id : `[id=${quoteCSSAttributeValue(id)}]`;
19
+ return /^[a-zA-Z][a-zA-Z0-9\-\_]+$/.test(id) ? "#" + id : `[id=${quoteCSSAttributeValue(id)}]`;
20
20
  }
21
21
 
22
22
  function escapeClassName(className) {
23
- let result = "";
24
- for (let i = 0; i < className.length; i++) result += cssEscapeCharacter(className, i);
25
- return result;
23
+ let result = "";
24
+ for (let i = 0; i < className.length; i++) result += cssEscapeCharacter(className, i);
25
+ return result;
26
26
  }
27
27
  function cssEscapeCharacter(s, i) {
28
- const c = s.charCodeAt(i);
29
- if (c === 0) return "\uFFFD";
30
- if ((c >= 1 && c <= 31) || (c >= 48 && c <= 57 && (i === 0 || (i === 1 && s.charCodeAt(0) === 45))))
31
- return "\\" + c.toString(16) + " ";
32
- if (i === 0 && c === 45 && s.length === 1) return "\\" + s.charAt(i);
33
- if (c >= 128 || c === 45 || c === 95 || (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122))
34
- return s.charAt(i);
35
- return "\\" + s.charAt(i);
28
+ const c = s.charCodeAt(i);
29
+ if (c === 0) return "\uFFFD";
30
+ if ((c >= 1 && c <= 31) || (c >= 48 && c <= 57 && (i === 0 || (i === 1 && s.charCodeAt(0) === 45))))
31
+ return "\\" + c.toString(16) + " ";
32
+ if (i === 0 && c === 45 && s.length === 1) return "\\" + s.charAt(i);
33
+ if (c >= 128 || c === 45 || c === 95 || (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122))
34
+ return s.charAt(i);
35
+ return "\\" + s.charAt(i);
36
36
  }
37
37
  function escapeRegexForSelector(re) {
38
- if (re.unicode || re.unicodeSets) return String(re);
39
- return String(re)
40
- .replace(/(^|[^\\])(\\\\)*(["'`])/g, "$1$2\\$3")
41
- .replace(/>>/g, "\\>\\>");
38
+ if (re.unicode || re.unicodeSets) return String(re);
39
+ return String(re)
40
+ .replace(/(^|[^\\])(\\\\)*(["'`])/g, "$1$2\\$3")
41
+ .replace(/>>/g, "\\>\\>");
42
42
  }
43
43
  class LocatorGenerator {
44
- constructor(injectedScript, options = {}) {
45
- this.locatorStrategies = {
46
- text: "basic",
47
- no_text: "no_text",
48
- custom: "custom",
49
- context: "context",
50
- digitIgnore: "ignore_digit",
51
- text_with_index: "text_with_index",
52
- };
53
- this.options = options;
54
- this.dom_Parent = new DOM_Parent();
55
- this.PW = __PW;
56
- this.injectedScript = injectedScript;
57
- this.cache = new Map();
58
- }
59
-
60
- generateUniqueCSSSelector(element, options) {
61
- const root = options?.root || window.document;
62
- const separator = options?.separator || " > ";
63
- const isUnique = options?.isunique || ((selector) => this.getMatchingElements(selector, options).length === 1);
64
- const noCSSId = options?.noCSSId || false;
65
-
66
- if (!(element instanceof Element)) return "";
67
-
68
- if (!root.contains(element)) return "";
69
-
70
- let selector = "";
71
- const id = element.getAttribute("id");
72
- if (id && !/\d/.test(id) && !noCSSId) {
73
- selector = makeSelectorForId(id);
74
- if (isUnique(selector)) return selector;
75
- }
76
- if (element.tagName) {
77
- selector = element.tagName.toLowerCase() + selector;
78
- if (isUnique(selector)) return selector;
79
- }
80
-
81
- const classList = Array.from(element.classList).filter((c) => !/\d/.test(c));
82
- if (classList.length) {
83
- selector += classList.map((c) => "." + escapeClassName(c)).join("");
84
- if (isUnique(selector)) return selector;
85
- }
86
-
87
- for (let attr of candidateAttributes) {
88
- if (element.hasAttribute(attr)) {
89
- let value = element.getAttribute(attr);
90
- if (value === "") continue;
91
- if (/\d/.test(value)) continue;
92
- // Escape special characters in attribute value
93
- value = value.replace(/[!"#$%&'()*+,./:;<=>?@[\]^`{|}~]/g, "\\$&");
94
- selector += `[${attr}=${this.PW.stringUtils.escapeForAttributeSelector(value)}]`;
95
- }
96
- }
97
- if (isUnique(selector)) return selector;
98
-
99
- if (element === root) return selector;
100
-
101
- let parentElement = element.parentElement;
102
- if (!parentElement) {
103
- // if element is shadowRoot
104
- if (element.parentNode instanceof ShadowRoot) {
105
- const parentElement = element.parentNode.host;
106
- if (parentElement && parentElement !== root) {
107
- const parentSelector = this.generateUniqueCSSSelector(parentElement, options);
108
- selector = parentSelector + " >>> " + selector;
109
- if (isUnique(selector)) return selector;
110
- }
111
- }
112
- } else if (parentElement !== root) {
113
- // if is a slotted element
114
- if (element.assignedSlot) {
115
- parentElement = element.assignedSlot.parentElement ?? element.assignedSlot.parentNode.host;
116
- }
117
-
118
- if (parentElement && parentElement !== root) {
119
- const parentSelector = this.generateUniqueCSSSelector(parentElement, options);
120
-
121
- selector = parentSelector + separator + selector;
122
- if (isUnique(selector)) return selector;
123
- }
124
- }
125
-
126
- const siblings = element.parentElement?.children;
127
- if (siblings) {
128
- for (let i = 0; i < siblings.length; i++) {
129
- if (siblings[i] === element) {
130
- return selector + `:nth-child(${i + 1})`;
131
- }
132
- }
133
- }
134
- return "";
135
- }
136
-
137
- getMatchingElements(selector, options = {}) {
138
- const { root = window.document, prefix, visible = true } = options;
139
- if (visible) {
140
- selector = `${selector} >> visible=true`;
141
- }
142
- if (prefix) {
143
- selector = `${prefix} >> ${selector}`;
144
- }
145
- return this.injectedScript.querySelectorAll(this.injectedScript.parseSelector(selector), root);
146
- }
147
-
148
- getLocatorStrategies() {
149
- return this.locatorStrategies;
150
- }
151
- getTextLocators(element, options) {
152
- const injectedScript = this.injectedScript;
153
- const selectorPartLists = this.PW.selectorGenerator.buildTextCandidates(injectedScript, element, options);
154
- const result = [];
155
- for (const selectorPartList of selectorPartLists) {
156
- let tScore = 0;
157
- const tSelectorList = [];
158
- for (const selectorPart of selectorPartList) {
159
- const { engine, selector } = selectorPart;
160
- if (engine === "css") {
161
- tSelectorList.push(selector);
162
- } else {
163
- tSelectorList.push(`${engine}=${selector}`);
164
- }
165
- tScore += selectorPart.score;
166
- }
167
- const selector = tSelectorList.join(" >> ");
168
- const score = tScore / selectorPartList.length;
169
- result.push({
170
- css: selector,
171
- score,
172
- });
173
- }
174
- return result;
175
- }
176
- getNoTextLocators(element, options) {
177
- const injectedScript = this.injectedScript;
178
- const locators = this.PW.selectorGenerator.buildNoTextCandidates(injectedScript, element, options);
179
- for (const locator of locators) {
180
- if (locator.engine === "css") {
181
- locator.css = locator.selector;
182
- } else {
183
- locator.css = `${locator.engine}=${locator.selector}`;
184
- }
185
- delete locator.engine; // remove engine to avoid memory leak
186
- delete locator.selector; // remove selector to avoid memory leak
187
- }
188
- return locators;
189
- }
190
- getCustomLocators(element, options) {
191
- const { customAttributes = [] } = this.options;
192
- if (!customAttributes || !Array.isArray(customAttributes)) {
193
- console.error("Custom attributes must be an array");
194
- return [];
195
- }
196
-
197
- const result = [];
198
- let hasAttribures = [];
199
- for (const customAttribute of customAttributes) {
200
- if (!customAttribute || typeof customAttribute !== "string") {
201
- console.error("Custom attribute must be a string");
202
- continue;
203
- }
204
- const val = element.getAttribute(customAttribute);
205
- if (val !== null) {
206
- hasAttribures.push(customAttribute);
207
- result.push({
208
- css: `[${customAttribute}=${this.PW.stringUtils.escapeForAttributeSelector(val)}]`,
209
- score: 1,
210
- priority: 1,
211
- });
212
- }
213
- }
214
- return result;
215
- }
216
- getContextLocators(element, locators) {
217
- if (!locators || !Array.isArray(locators)) {
218
- console.error("Locators must be an array");
219
- return [];
220
- }
221
- const result = [];
222
- try {
223
- const textSet = new Set();
224
- for (const locator of locators) {
225
- const selector = locator.css;
226
- if (!selector || typeof selector !== "string") {
227
- console.error("Locator must have a valid css selector");
228
- continue;
229
- }
230
- const parseResult = this.injectedScript.parseSelector(selector);
231
- const parts = parseResult.parts;
232
- if (!parts || !Array.isArray(parts) || parts.length === 0) {
233
- console.error("Locator must have a valid css selector");
234
- continue;
235
- }
236
- // ignore parts.length < 3
237
- if (parts.length < 3) {
238
- // console.warn("Locator must have at least 3 parts to be a context locator");
239
- continue;
240
- }
241
- const firstPart = parts[0];
242
- if (firstPart.name !== "internal:text") {
243
- // console.warn("Locator must have internal:text as the first part to be a context locator");
244
- continue;
245
- }
246
- const textBody = firstPart.body;
247
- if (!textBody || typeof textBody !== "string" || textBody.length === 0) {
248
- console.error("Locator must have a valid text in the first part to be a context locator");
249
- continue;
250
- }
251
- const secondPart = parts[1];
252
- if (secondPart.name !== "xpath") {
253
- continue;
254
- }
255
- const xpath = secondPart.body;
256
- if (!xpath || typeof xpath !== "string" || xpath.length === 0) {
257
- // console.error("Locator must have a valid xpath in the second part to be a context locator");
258
- continue;
259
- }
260
- const climbString = secondPart.body;
261
- if (!climbString || typeof climbString !== "string" || climbString.length === 0) {
262
- continue;
263
- }
264
- const climbStringRegex = /(\.\.)(\/\.\.)*/;
265
- try {
266
- const match = climbStringRegex.test(climbString);
267
- if (match) {
268
- const climbCount = climbString.split("..").length - 1;
269
- const lastIndex = selector.indexOf(climbString);
270
- const restOfSelector = selector.substring(lastIndex + climbString.length + 3).trim();
271
- if (restOfSelector.length === 0) {
272
- // console.warn("Locator must have a valid rest of selector after the xpath part to
273
- // be a context locator");
274
- continue;
275
- }
276
-
277
- const textLocator = `internal:text=${textBody}`;
278
- const elements = this.getMatchingElements(textLocator, {});
279
- if (elements.length !== 1) {
280
- // throw new Error("Context locator must have exactly one element matching the text part");
281
- console.error("Context locator must have exactly one element matching the text part");
282
- continue;
283
- }
284
- const textElement = elements[0];
285
- // const text = this.PW.selectorUtils.elementText(textElement);
286
- const text = this.injectedScript.utils.elementText(new Map(), textElement).full;
287
-
288
- const fullSelector = `${textLocator} >> xpath=${xpath} >> ${restOfSelector}`;
289
- const fullElements = this.getMatchingElements(fullSelector, {});
290
- if (fullElements.length !== 1) {
291
- // throw new Error("Context locator must have exactly one element matching the full selector");
292
- console.error("Context locator must have exactly one element matching the full selector");
293
- continue;
294
- }
295
- const fullElement = fullElements[0];
296
- if (fullElement !== element) {
297
- // throw new Error("Context locator must have the text element as the full element");
298
- console.error("Context locator must have the text element as the full element");
299
- continue;
300
- }
301
- if (!textSet.has(text)) {
302
- textSet.add(text);
303
- const loc = {
304
- css: restOfSelector,
305
- climb: climbCount,
306
- text,
307
- priority: 1,
308
- };
309
- if (locator.index !== undefined) {
310
- loc.index = locator.index;
311
- }
312
- result.push(loc);
313
- }
314
- }
315
- } catch (error) {
316
- console.error("Error parsing climb string:", error);
317
- continue;
318
- }
319
- }
320
- } catch (error) {
321
- console.error("Error generating context locators:", error);
322
- }
323
- // Sort by text length to prioritize shorter texts
324
- result.sort((a, b) => a.text.length - b.text.length);
325
- return result;
326
- }
327
- getDigitIgnoreLocators(element, locators) {
328
- const result = [];
329
- try {
330
- if (!locators || !Array.isArray(locators)) {
331
- console.error("Locators must be an array");
332
- return [];
333
- }
334
-
335
- for (const locator of locators) {
336
- const selector = locator.css;
337
- if (!selector || typeof selector !== "string") {
338
- console.error("Locator must have a valid css selector");
339
- continue;
340
- }
341
- const parseresult = this.injectedScript.parseSelector(selector);
342
- const parts = parseresult.parts;
343
- if (!parts || !Array.isArray(parts) || parts.length === 0) {
344
- console.error("Locator must have a valid css selector");
345
- continue;
346
- }
347
- let finalSelector = "";
348
- let hasDigitsInText = false;
349
- for (const part of parts) {
350
- if (part.name !== "internal:text") {
351
- finalSelector += `${part.name === "css" ? "" : part.name + "="}${part.source} >> `;
352
- continue;
353
- }
354
- if (typeof part.body !== "string" || part.body.length === 0) {
355
- // console.error("Locator must have a valid text in the first part to be a digit ignore locator");
356
- finalSelector += `${part.name === "css" ? "" : part.name + "="}${part.source} >> `;
357
- continue;
358
- }
359
- const text = part.body;
360
- const digitsRegex = /\d+/g;
361
- hasDigitsInText = digitsRegex.test(text);
362
-
363
- let pattern = this.PW.stringUtils.escapeRegExp(text.substring(1, text.length - 2));
364
- const re = new RegExp(pattern);
365
-
366
- finalSelector += `internal:text=${escapeRegexForSelector(re).replace(digitsRegex, "\\d+")} >> `;
367
- }
368
- if (!hasDigitsInText) {
369
- continue;
370
- }
371
- if (finalSelector.endsWith(` >> `)) {
372
- finalSelector = finalSelector.slice(0, -4);
373
- }
374
- if (finalSelector) {
375
- const elements = this.getMatchingElements(finalSelector, {});
376
- if (elements.length !== 1) {
377
- console.error("Digit ignore locator must have exactly one element matching the final selector");
378
- continue;
379
- }
380
- if (elements[0] !== element) {
381
- console.error("Digit ignore locator must match the original element");
382
- continue;
383
- }
384
- result.push({
385
- css: finalSelector,
386
- priority: locator.priority || 1,
387
- });
388
- }
389
- }
390
- } catch (error) {
391
- console.error("Error generating digit ignore locators:", error);
392
- }
393
-
394
- return result;
395
- }
396
- getTextwithIndexLocators(locators) {
397
- if (!locators || !Array.isArray(locators)) {
398
- console.error("Locators must be an array");
399
- return [];
400
- }
401
- const result = [];
402
- try {
403
- for (const locator of locators) {
404
- if (!locator || !locator.css || typeof locator.css !== "string") {
405
- console.error("Locator must have a valid css selector");
406
- continue;
407
- }
408
- const index = locator.index;
409
- if (typeof index !== "number" || index < 0) {
410
- // console.error("Locator must have a valid index");
411
- continue;
412
- }
413
- result.push(locator);
414
- }
415
- } catch (error) {
416
- console.error("Error getting text with index locators:", error);
417
- }
418
- return result;
419
- }
420
-
421
- getXPathSelector(climb) {
422
- if (typeof climb !== "number" || climb < 0) {
423
- // throw new Error("Climb must be a non-negative integer");
424
- console.error("Climb must be a non-negative integer");
425
- return "";
426
- }
427
- if (climb === 0) return "";
428
- let selector = "xpath=..";
429
- if (climb === 1) {
430
- return selector;
431
- }
432
- for (let i = 1; i < climb; i++) {
433
- selector += "/..";
434
- }
435
- return selector;
436
- }
437
- categorizeLocators(element, locators, options) {
438
- const unique = [];
439
- const nonUnique = [];
440
- try {
441
- for (const locator of locators) {
442
- const elements = this.getMatchingElements(locator.css, options);
443
- if (elements.length === 0) {
444
- console.warn(`No elements found for locator: ${locator.css}`);
445
- continue;
446
- } else if (elements.length === 1) {
447
- if (element === elements[0]) {
448
- locator.priority = 1;
449
- unique.push(locator);
450
- } else if (element.contains(elements[0])) {
451
- locator.priority = 1;
452
- const climb = this.dom_Parent.getClimbCountToParent(elements[0], element);
453
- const climbSelector = this.getXPathSelector(climb);
454
- const newSelector = `${locator.css} >> ${climbSelector}`;
455
- locator.css = newSelector;
456
- unique.push(locator);
457
- }
458
- } else {
459
- locator.priority = 2;
460
- locator.elements = elements;
461
- nonUnique.push(locator);
462
- }
463
- }
464
- } catch (error) {
465
- console.error("Error categorizing locators:", error);
466
- }
467
- return { unique, nonUnique };
468
- }
469
-
470
- getUniqueLocators(element, locatorGenerator = this.getNoTextLocators, options = {}) {
471
- return this.getUniqueLocators2(element, locatorGenerator, options);
472
- }
473
- getUniqueLocators1(element, locatorGenerator = this.getNoTextLocators, options = {}) {
474
- try {
475
- const { maxLocators = 5, root = window.document.body, next = "LCA", minLocators = 3 } = options;
476
-
477
- if (!element) {
478
- return [];
479
- }
480
- if (element === root) {
481
- if (element === window.document.documentElement) {
482
- return [
483
- {
484
- css: "html",
485
- score: 1,
486
- priority: 1,
487
- },
488
- {
489
- css: ":root",
490
- score: 1,
491
- priority: 1,
492
- },
493
- ];
494
- } else {
495
- return [
496
- {
497
- css: ":root",
498
- score: 1,
499
- priority: 1,
500
- // }, {
501
- // css: ":root",
502
- // score: 1,
503
- // priority: 1,
504
- },
505
- ];
506
- }
507
- }
508
-
509
- console.log("Generating locators for element:", element);
510
- const locators = locatorGenerator(element, options);
511
- console.log("Generated locators:", locators);
512
- if (!locators || !Array.isArray(locators)) {
513
- // throw new Error("Locator generator did not return an array of locators");
514
- console.error("Locator generator did not return an array of locators");
515
- return [];
516
- }
517
-
518
- console.log("Categorizing locators for element:", element);
519
- const categorizedLocators = this.categorizeLocators(element, locators, options);
520
- console.log("Categorized locators:", categorizedLocators);
521
- // categorizedLocators.unique = limitLocators(categorizedLocators.unique, options);
522
- // categorizedLocators.nonUnique = limitLocators(categorizedLocators.nonUnique, options);
523
-
524
- const { unique, nonUnique } = categorizedLocators;
525
- const result = [];
526
- if (unique.length > 0) {
527
- result.push(...unique);
528
- }
529
- if (result.length >= maxLocators) {
530
- return result.slice(0, maxLocators);
531
- }
532
- let nextElement = null;
533
- for (const locator of nonUnique) {
534
- const selector = locator.css ?? locator.selector;
535
- const elements = locator.elements || this.getMatchingElements(selector, options);
536
-
537
- if (next === "parent") {
538
- nextElement = this.dom_Parent.getActualParent(element);
539
- } else {
540
- // find the branching element the child of common parent that contains the element
541
- const branchingParent = this.dom_Parent.findBranchingParent(elements, element);
542
- nextElement = branchingParent;
543
- }
544
-
545
- if (nextElement && nextElement !== element) {
546
- if (root.contains(nextElement)) {
547
- const _result = this.getUniqueLocators(nextElement, locatorGenerator, {
548
- ...options,
549
- root,
550
- });
551
- for (const _locator of _result) {
552
- if (result.length >= maxLocators) {
553
- return result.slice(0, maxLocators);
554
- }
555
- const _selector = _locator.css ?? _locator.selector;
556
- const fullSelector = `${_selector} >> ${selector}`;
557
- const _elements = this.getMatchingElements(fullSelector, options);
558
- const effectiveScore = (_locator.score + locator.score) / 2 + 100;
559
- if (_elements.length === 1 && _elements[0] === element) {
560
- _locator.css = fullSelector;
561
- _locator.score = effectiveScore;
562
- _locator.priority = 1; // unique locators have higher priority
563
- result.push(_locator);
564
- } else {
565
- const index = _elements.indexOf(element);
566
- if (index !== -1 && index < 5) {
567
- // _locator.selector = fullSelector;
568
- _locator.css = fullSelector;
569
- _locator.index = index;
570
- _locator.priority = 2; // non-unique locators have lower priority
571
- _locator.score = effectiveScore;
572
- result.push(_locator);
573
- }
574
- }
575
- }
576
- } else {
577
- const index = elements.indexOf(element);
578
- if (index !== -1 && index < 5) {
579
- locator.index = index;
580
- locator.priority = 2; // non-unique locators have lower priority
581
- result.push(locator);
582
- }
583
- }
584
- } else {
585
- const index = elements.indexOf(element);
586
- if (index !== -1 && index < 5) {
587
- locator.index = index;
588
- locator.priority = 2; // non-unique locators have lower priority
589
- result.push(locator);
590
- }
591
- }
592
-
593
- delete locator.elements; // remove elements to avoid memory leak
594
- delete locator.strategy;
595
- delete locator.engine;
596
- delete locator.selector;
597
- }
598
-
599
- if (result.length < minLocators && root && root.contains(element)) {
600
- const parent = this.dom_Parent.getActualParent(element);
601
- const locs = this.getUniqueLocators(parent, locatorGenerator, {
602
- ...options,
603
- root,
604
- });
605
- result.push(...locs);
606
- }
607
-
608
- result.sort((a, b) => a.score - b.score);
609
- console.log("Final locators:", result, element);
610
- console.groupEnd();
611
- return result.slice(0, maxLocators);
612
- } catch (error) {
613
- console.error("Error in getUniqueLocators:", error);
614
- return [];
615
- }
616
- }
617
-
618
- getUniqueLocators2(element, locatorGenerator = this.getNoTextLocators, options = {}) {
619
- try {
620
- const { maxLocators = 5, root = window.document.body } = options;
621
-
622
- if (!element) {
623
- return [];
624
- }
625
- if (element === root) {
626
- if (element === window.document.documentElement) {
627
- return [
628
- {
629
- css: "html",
630
- score: 1,
631
- priority: 1,
632
- },
633
- {
634
- css: ":root",
635
- score: 1,
636
- priority: 1,
637
- },
638
- ];
639
- } else {
640
- return [
641
- {
642
- css: ":root",
643
- score: 1,
644
- priority: 1,
645
- },
646
- ];
647
- }
648
- }
649
-
650
- console.log("Generating locators for element:", element);
651
- const locators = locatorGenerator(element, options);
652
- console.log("Generated locators:", locators);
653
- if (!locators || !Array.isArray(locators)) {
654
- console.error("Locator generator did not return an array of locators");
655
- return [];
656
- }
657
-
658
- console.log("Categorizing locators for element:", element);
659
- const categorizedLocators = this.categorizeLocators(element, locators, options);
660
- console.log("Categorized locators:", categorizedLocators);
661
-
662
- const { unique, nonUnique } = categorizedLocators;
663
- const result = [];
664
- if (unique.length > 0) {
665
- result.push(...unique);
666
- }
667
- if (result.length >= maxLocators) {
668
- return result.slice(0, maxLocators);
669
- }
670
-
671
- const elementsCache = new Map();
672
-
673
- const allAncestors = this.dom_Parent.getFullAncestorChainToRoot(element, root);
674
- allAncestors.shift(); // remove the element itself from the ancestors
675
-
676
- const ancestorLocators = [];
677
- for (const ancestor of allAncestors) {
678
- const _locators = locatorGenerator(ancestor, options);
679
- if (!_locators || !Array.isArray(_locators) || _locators.length === 0) {
680
- continue;
681
- }
682
- const _categorized = this.categorizeLocators(ancestor, _locators, options);
683
- ancestorLocators.push({
684
- element: ancestor,
685
- locators: _categorized,
686
- });
687
- elementsCache.set(ancestor, _categorized);
688
- if (_categorized.unique.length > 0) {
689
- break;
690
- }
691
- }
692
-
693
- const uniqueAncestor = ancestorLocators[ancestorLocators.length - 1];
694
-
695
- for (const locator of nonUnique) {
696
- const selector = locator.css ?? locator.selector;
697
- const elements = locator.elements || this.getMatchingElements(selector, options);
698
- if (elements.length === 0) {
699
- console.warn(`No elements found for locator: ${selector}`);
700
- continue;
701
- }
702
-
703
- for (const unique_locator of uniqueAncestor.locators.unique) {
704
- const fullSelector = `${unique_locator.css} >> ${selector}`;
705
- const elements = this.getMatchingElements(fullSelector, options);
706
- if (elements.length === 1 && elements[0] === element) {
707
- const effectiveScore = (unique_locator.score + locator.score) / 2 + 100;
708
- const newLocator = {
709
- ...unique_locator,
710
- css: fullSelector,
711
- score: effectiveScore,
712
- priority: 1, // unique locators have higher priority
713
- };
714
- result.push(newLocator);
715
- } else {
716
- const index = elements.indexOf(element);
717
- if (index !== -1 && index < 5) {
718
- const effectiveScore = (unique_locator.score + locator.score) / 2;
719
- const newLocator = {
720
- ...unique_locator,
721
- css: fullSelector,
722
- index,
723
- score: effectiveScore + 200,
724
- priority: 2, // non-unique locators have lower priority
725
- };
726
- result.push(newLocator);
727
- }
728
- }
729
- }
730
- }
731
- result.sort((a, b) => a.score - b.score);
732
- console.log("Final locators:", result, element);
733
- console.groupEnd();
734
- return result.slice(0, maxLocators);
735
- } catch (error) {
736
- console.error("Error in getUniqueLocators:", error);
737
- return [];
738
- }
739
- }
740
- getElementLocators(element, options = {}) {
741
- try {
742
- const { excludeText = false } = options;
743
-
744
- const allStrategyLocators = {};
745
- if (this.options?.customAttributes) {
746
- console.groupCollapsed("Generating Custom locators for element:", element);
747
- const customLocators = this.getUniqueLocators(element, this.getCustomLocators.bind(this), options);
748
- if (customLocators.length > 0) {
749
- allStrategyLocators[this.locatorStrategies.custom] = customLocators;
750
- }
751
- }
752
- console.groupEnd();
753
- if (!excludeText) {
754
- console.groupCollapsed("Generating Text locators for element:", element);
755
- const basicLocators = this.getUniqueLocators(element, this.getTextLocators.bind(this), options);
756
- console.groupEnd();
757
- if (basicLocators.length > 0) {
758
- allStrategyLocators[this.locatorStrategies.text] = basicLocators;
759
- }
760
-
761
- const textWithIndexLocators = this.getTextwithIndexLocators(basicLocators);
762
- if (textWithIndexLocators.length > 0) {
763
- allStrategyLocators[this.locatorStrategies.text_with_index] = textWithIndexLocators;
764
- }
765
- const digitIgnoreLocators = this.getDigitIgnoreLocators(element, basicLocators);
766
- if (digitIgnoreLocators.length > 0) {
767
- allStrategyLocators[this.locatorStrategies.digitIgnore] = digitIgnoreLocators;
768
- }
769
- const contextLocators = this.getContextLocators(element, basicLocators);
770
- if (contextLocators.length > 0) {
771
- allStrategyLocators[this.locatorStrategies.context] = contextLocators;
772
- }
773
- }
774
- console.groupCollapsed("Generating No Text locators for element:", element);
775
- const noTextLocators = this.getUniqueLocators(element, this.getNoTextLocators.bind(this), options);
776
-
777
- if (noTextLocators.length > 0) {
778
- allStrategyLocators[this.locatorStrategies.no_text] = noTextLocators;
779
- } else {
780
- const _locators = [];
781
- _locators.push({
782
- css: this.generateUniqueCSSSelector(element, options),
783
- score: 500,
784
- priority: 3,
785
- });
786
- if (_locators.length > 0) {
787
- allStrategyLocators[this.locatorStrategies.no_text] = _locators;
788
- }
789
- }
790
- console.groupEnd();
791
-
792
- let bestStrategy = this.getBestStrategy(allStrategyLocators);
793
- if (!bestStrategy) {
794
- throw new Error("No locators found for element: " + element);
795
- }
796
- allStrategyLocators.strategy = bestStrategy;
797
-
798
- const locators = allStrategyLocators[allStrategyLocators.strategy] ?? [];
799
- const result = {
800
- locators,
801
- element_name: "",
802
- };
803
- if (Object.keys(allStrategyLocators).length > 1) {
804
- result.allStrategyLocators = allStrategyLocators;
805
- }
806
- console.log("Generated locators:", result);
807
- return result;
808
- } catch (error) {
809
- console.error("Error in getElementLocators:", error);
810
- return {
811
- locators: [],
812
- element_name: "",
813
- };
814
- }
815
- }
816
- getBestStrategy(allStrategyLocators) {
817
- const orderedPriorities = [
818
- this.locatorStrategies.custom,
819
- this.locatorStrategies.context,
820
- this.locatorStrategies.text,
821
- this.locatorStrategies.text_with_index,
822
- this.locatorStrategies.digitIgnore,
823
- this.locatorStrategies.no_text,
824
- ];
825
- for (const strategy of orderedPriorities) {
826
- if (allStrategyLocators[strategy] && allStrategyLocators[strategy].length > 0) {
827
- return strategy;
828
- }
829
- }
830
- return null;
831
- }
44
+ constructor(injectedScript, options = {}) {
45
+ this.locatorStrategies = {
46
+ text: "basic",
47
+ no_text: "no_text",
48
+ custom: "custom",
49
+ context: "context",
50
+ digitIgnore: "ignore_digit",
51
+ text_with_index: "text_with_index",
52
+ };
53
+ this.options = options;
54
+ this.dom_Parent = new DOM_Parent();
55
+ this.PW = __PW;
56
+ this.injectedScript = injectedScript;
57
+ this.cache = new Map();
58
+ }
59
+
60
+ generateUniqueCSSSelector(element, options) {
61
+ const root = options?.root || window.document;
62
+ const separator = options?.separator || " > ";
63
+ const isUnique = options?.isunique || ((selector) => this.getMatchingElements(selector, options).length === 1);
64
+ const noCSSId = options?.noCSSId || false;
65
+
66
+ if (!(element instanceof Element)) return "";
67
+
68
+ if (!root.contains(element)) return "";
69
+
70
+ let selector = "";
71
+ const id = element.getAttribute("id");
72
+ if (id && !/\d/.test(id) && !noCSSId) {
73
+ selector = makeSelectorForId(id);
74
+ if (isUnique(selector)) return selector;
75
+ }
76
+ if (element.tagName) {
77
+ selector = element.tagName.toLowerCase() + selector;
78
+ if (isUnique(selector)) return selector;
79
+ }
80
+
81
+ const classList = Array.from(element.classList).filter((c) => !/\d/.test(c));
82
+ if (classList.length) {
83
+ selector += classList.map((c) => "." + escapeClassName(c)).join("");
84
+ if (isUnique(selector)) return selector;
85
+ }
86
+
87
+ for (let attr of candidateAttributes) {
88
+ if (element.hasAttribute(attr)) {
89
+ let value = element.getAttribute(attr);
90
+ if (value === "") continue;
91
+ if (/\d/.test(value)) continue;
92
+ // Escape special characters in attribute value
93
+ value = value.replace(/[!"#$%&'()*+,./:;<=>?@[\]^`{|}~]/g, "\\$&");
94
+ selector += `[${attr}=${this.PW.stringUtils.escapeForAttributeSelector(value)}]`;
95
+ }
96
+ }
97
+ if (isUnique(selector)) return selector;
98
+
99
+ if (element === root) return selector;
100
+
101
+ let parentElement = element.parentElement;
102
+ if (!parentElement) {
103
+ // if element is shadowRoot
104
+ if (element.parentNode instanceof ShadowRoot) {
105
+ const parentElement = element.parentNode.host;
106
+ if (parentElement && parentElement !== root) {
107
+ const parentSelector = this.generateUniqueCSSSelector(parentElement, options);
108
+ selector = parentSelector + " >>> " + selector;
109
+ if (isUnique(selector)) return selector;
110
+ }
111
+ }
112
+ } else if (parentElement !== root) {
113
+ // if is a slotted element
114
+ if (element.assignedSlot) {
115
+ parentElement = element.assignedSlot.parentElement ?? element.assignedSlot.parentNode.host;
116
+ }
117
+
118
+ if (parentElement && parentElement !== root) {
119
+ const parentSelector = this.generateUniqueCSSSelector(parentElement, options);
120
+
121
+ selector = parentSelector + separator + selector;
122
+ if (isUnique(selector)) return selector;
123
+ }
124
+ }
125
+
126
+ const siblings = element.parentElement?.children;
127
+ if (siblings) {
128
+ for (let i = 0; i < siblings.length; i++) {
129
+ if (siblings[i] === element) {
130
+ return selector + `:nth-child(${i + 1})`;
131
+ }
132
+ }
133
+ }
134
+ return "";
135
+ }
136
+
137
+ getMatchingElements(selector, options = {}) {
138
+ const { root = window.document, prefix, visible = true } = options;
139
+ if (visible) {
140
+ selector = `${selector} >> visible=true`;
141
+ }
142
+ if (prefix) {
143
+ selector = `${prefix} >> ${selector}`;
144
+ }
145
+ return this.injectedScript.querySelectorAll(this.injectedScript.parseSelector(selector), root);
146
+ }
147
+
148
+ getLocatorStrategies() {
149
+ return this.locatorStrategies;
150
+ }
151
+ getTextLocators(element, options) {
152
+ const injectedScript = this.injectedScript;
153
+ const { textToIgnore = null } = options;
154
+ const selectorPartLists = this.PW.selectorGenerator.buildTextCandidates(injectedScript, element, options);
155
+ const result = [];
156
+ for (const selectorPartList of selectorPartLists) {
157
+ let tScore = 0;
158
+ const tSelectorList = [];
159
+ for (const selectorPart of selectorPartList) {
160
+ const { engine, selector } = selectorPart;
161
+ if (textToIgnore && selector.includes(textToIgnore)) {
162
+ continue;
163
+ }
164
+ if (engine === "css") {
165
+ tSelectorList.push(selector);
166
+ } else {
167
+ tSelectorList.push(`${engine}=${selector}`);
168
+ }
169
+ tScore += selectorPart.score;
170
+ }
171
+ const selector = tSelectorList.join(" >> ");
172
+ const score = tScore / selectorPartList.length;
173
+ result.push({
174
+ css: selector,
175
+ score,
176
+ });
177
+ }
178
+ return result;
179
+ }
180
+ getNoTextLocators(element, options) {
181
+ const injectedScript = this.injectedScript;
182
+ const locators = this.PW.selectorGenerator.buildNoTextCandidates(injectedScript, element, options);
183
+ for (const locator of locators) {
184
+ if (locator.engine === "css") {
185
+ locator.css = locator.selector;
186
+ } else {
187
+ locator.css = `${locator.engine}=${locator.selector}`;
188
+ }
189
+ delete locator.engine; // remove engine to avoid memory leak
190
+ delete locator.selector; // remove selector to avoid memory leak
191
+ }
192
+ return locators;
193
+ }
194
+ getCustomLocators(element, options) {
195
+ const { customAttributes = [] } = this.options;
196
+ if (!customAttributes || !Array.isArray(customAttributes)) {
197
+ console.error("Custom attributes must be an array");
198
+ return [];
199
+ }
200
+
201
+ const result = [];
202
+ let hasAttribures = [];
203
+ for (const customAttribute of customAttributes) {
204
+ if (!customAttribute || typeof customAttribute !== "string") {
205
+ console.error("Custom attribute must be a string");
206
+ continue;
207
+ }
208
+ const val = element.getAttribute(customAttribute);
209
+ if (val !== null) {
210
+ hasAttribures.push(customAttribute);
211
+ result.push({
212
+ css: `[${customAttribute}=${this.PW.stringUtils.escapeForAttributeSelector(val)}]`,
213
+ score: 1,
214
+ priority: 1,
215
+ });
216
+ }
217
+ }
218
+ return result;
219
+ }
220
+ toContextLocators(element, contextElement) {
221
+ const commonParent = this.dom_Parent.findLowestCommonAncestor([contextElement, element]);
222
+ const climb = this.dom_Parent.getClimbCountToParent(contextElement, commonParent);
223
+ const text = contextElement.innerText.trim();
224
+
225
+ const prefix = `internal:text="${text}" >> ${this.getXPathSelector(climb)}`;
226
+ const result = this.getElementLocators(element, {
227
+ root: commonParent,
228
+ strategies: {
229
+ [this.locatorStrategies.custom]: true,
230
+ [this.locatorStrategies.text]: true,
231
+ [this.locatorStrategies.no_text]: true,
232
+ },
233
+ prefix,
234
+ });
235
+
236
+ const attachContextToLocators = (locs) => {
237
+ locs.forEach((loc) => {
238
+ loc.climb = climb;
239
+ loc.text = text;
240
+ });
241
+ };
242
+
243
+ const allStrategyLocators = result.allStrategyLocators;
244
+ const locators = result.locators;
245
+ if (allStrategyLocators) {
246
+ const allLocators = [];
247
+ for (const strategy in allStrategyLocators) {
248
+ if (strategy === "strategy") continue;
249
+ const locators = allStrategyLocators[strategy];
250
+ if (locators.length === 0) continue;
251
+ allLocators.push(...locators);
252
+ allStrategyLocators[strategy] = [];
253
+ }
254
+ attachContextToLocators(allLocators);
255
+ allStrategyLocators[this.locatorStrategies.context] = allLocators;
256
+ allStrategyLocators.strategy = this.locatorStrategies.context;
257
+ result.locators = allLocators;
258
+ return result;
259
+ }
260
+ if (locators) {
261
+ attachContextToLocators(locators);
262
+ return result;
263
+ }
264
+ return result;
265
+ }
266
+ getContextLocators(element, locators) {
267
+ if (!locators || !Array.isArray(locators)) {
268
+ console.error("Locators must be an array");
269
+ return [];
270
+ }
271
+ const result = [];
272
+ try {
273
+ const textSet = new Set();
274
+ for (const locator of locators) {
275
+ const selector = locator.css;
276
+ if (!selector || typeof selector !== "string") {
277
+ console.error("Locator must have a valid css selector");
278
+ continue;
279
+ }
280
+ const parseResult = this.injectedScript.parseSelector(selector);
281
+ const parts = parseResult.parts;
282
+ if (!parts || !Array.isArray(parts) || parts.length === 0) {
283
+ console.error("Locator must have a valid css selector");
284
+ continue;
285
+ }
286
+ // ignore parts.length < 3
287
+ if (parts.length < 3) {
288
+ // console.warn("Locator must have at least 3 parts to be a context locator");
289
+ continue;
290
+ }
291
+ const firstPart = parts[0];
292
+ if (firstPart.name !== "internal:text") {
293
+ // console.warn("Locator must have internal:text as the first part to be a context locator");
294
+ continue;
295
+ }
296
+ const textBody = firstPart.body;
297
+ if (!textBody || typeof textBody !== "string" || textBody.length === 0) {
298
+ console.error("Locator must have a valid text in the first part to be a context locator");
299
+ continue;
300
+ }
301
+ const secondPart = parts[1];
302
+ if (secondPart.name !== "xpath") {
303
+ continue;
304
+ }
305
+ const xpath = secondPart.body;
306
+ if (!xpath || typeof xpath !== "string" || xpath.length === 0) {
307
+ // console.error("Locator must have a valid xpath in the second part to be a context locator");
308
+ continue;
309
+ }
310
+ const climbString = secondPart.body;
311
+ if (!climbString || typeof climbString !== "string" || climbString.length === 0) {
312
+ continue;
313
+ }
314
+ const climbStringRegex = /(\.\.)(\/\.\.)*/;
315
+ try {
316
+ const match = climbStringRegex.test(climbString);
317
+ if (match) {
318
+ const climbCount = climbString.split("..").length - 1;
319
+ const lastIndex = selector.indexOf(climbString);
320
+ const restOfSelector = selector.substring(lastIndex + climbString.length + 3).trim();
321
+ if (restOfSelector.length === 0) {
322
+ // console.warn("Locator must have a valid rest of selector after the xpath part to
323
+ // be a context locator");
324
+ continue;
325
+ }
326
+
327
+ const textLocator = `internal:text=${textBody}`;
328
+ const elements = this.getMatchingElements(textLocator, {});
329
+ if (elements.length !== 1) {
330
+ // throw new Error("Context locator must have exactly one element matching the text part");
331
+ console.error("Context locator must have exactly one element matching the text part");
332
+ continue;
333
+ }
334
+ const textElement = elements[0];
335
+ // const text = this.PW.selectorUtils.elementText(textElement);
336
+ const text = this.injectedScript.utils.elementText(new Map(), textElement).full.trim();
337
+
338
+ const fullSelector = `${textLocator} >> xpath=${xpath} >> ${restOfSelector}`;
339
+ const fullElements = this.getMatchingElements(fullSelector, {});
340
+ if (fullElements.length !== 1) {
341
+ // throw new Error("Context locator must have exactly one element matching the full selector");
342
+ console.error("Context locator must have exactly one element matching the full selector");
343
+ continue;
344
+ }
345
+ const fullElement = fullElements[0];
346
+ if (fullElement !== element) {
347
+ // throw new Error("Context locator must have the text element as the full element");
348
+ console.error("Context locator must have the text element as the full element");
349
+ continue;
350
+ }
351
+ if (!textSet.has(text)) {
352
+ textSet.add(text);
353
+ const loc = {
354
+ css: restOfSelector,
355
+ climb: climbCount,
356
+ text,
357
+ priority: 1,
358
+ };
359
+ if (locator.index !== undefined) {
360
+ loc.index = locator.index;
361
+ }
362
+ result.push(loc);
363
+ }
364
+ }
365
+ } catch (error) {
366
+ console.error("Error parsing climb string:", error);
367
+ continue;
368
+ }
369
+ }
370
+ } catch (error) {
371
+ console.error("Error generating context locators:", error);
372
+ }
373
+ // Sort by text length to prioritize shorter texts
374
+ result.sort((a, b) => a.text.length - b.text.length);
375
+ return result;
376
+ }
377
+ getDigitIgnoreLocators(element, locators) {
378
+ const result = [];
379
+ try {
380
+ if (!locators || !Array.isArray(locators)) {
381
+ console.error("Locators must be an array");
382
+ return [];
383
+ }
384
+
385
+ for (const locator of locators) {
386
+ const selector = locator.css;
387
+ if (!selector || typeof selector !== "string") {
388
+ console.error("Locator must have a valid css selector");
389
+ continue;
390
+ }
391
+ const parseresult = this.injectedScript.parseSelector(selector);
392
+ const parts = parseresult.parts;
393
+ if (!parts || !Array.isArray(parts) || parts.length === 0) {
394
+ console.error("Locator must have a valid css selector");
395
+ continue;
396
+ }
397
+ let finalSelector = "";
398
+ let hasDigitsInText = false;
399
+ for (const part of parts) {
400
+ if (part.name !== "internal:text") {
401
+ finalSelector += `${part.name === "css" ? "" : part.name + "="}${part.source} >> `;
402
+ continue;
403
+ }
404
+ if (typeof part.body !== "string" || part.body.length === 0) {
405
+ // console.error("Locator must have a valid text in the first part to be a digit ignore locator");
406
+ finalSelector += `${part.name === "css" ? "" : part.name + "="}${part.source} >> `;
407
+ continue;
408
+ }
409
+ const text = part.body;
410
+ const digitsRegex = /\d+/g;
411
+ hasDigitsInText = digitsRegex.test(text);
412
+
413
+ let pattern = this.PW.stringUtils.escapeRegExp(text.substring(1, text.length - 2));
414
+ const re = new RegExp(pattern);
415
+
416
+ finalSelector += `internal:text=${escapeRegexForSelector(re).replace(digitsRegex, "\\d+")} >> `;
417
+ }
418
+ if (!hasDigitsInText) {
419
+ continue;
420
+ }
421
+ if (finalSelector.endsWith(` >> `)) {
422
+ finalSelector = finalSelector.slice(0, -4);
423
+ }
424
+ if (finalSelector) {
425
+ const elements = this.getMatchingElements(finalSelector, {});
426
+ if (elements.length !== 1) {
427
+ console.error("Digit ignore locator must have exactly one element matching the final selector");
428
+ continue;
429
+ }
430
+ if (elements[0] !== element) {
431
+ console.error("Digit ignore locator must match the original element");
432
+ continue;
433
+ }
434
+ result.push({
435
+ css: finalSelector,
436
+ priority: locator.priority || 1,
437
+ });
438
+ }
439
+ }
440
+ } catch (error) {
441
+ console.error("Error generating digit ignore locators:", error);
442
+ }
443
+
444
+ return result;
445
+ }
446
+ getTextwithIndexLocators(locators) {
447
+ if (!locators || !Array.isArray(locators)) {
448
+ console.error("Locators must be an array");
449
+ return [];
450
+ }
451
+ const result = [];
452
+ try {
453
+ for (const locator of locators) {
454
+ if (!locator || !locator.css || typeof locator.css !== "string") {
455
+ console.error("Locator must have a valid css selector");
456
+ continue;
457
+ }
458
+ const index = locator.index;
459
+ if (typeof index !== "number" || index < 0) {
460
+ // console.error("Locator must have a valid index");
461
+ continue;
462
+ }
463
+ result.push(locator);
464
+ }
465
+ } catch (error) {
466
+ console.error("Error getting text with index locators:", error);
467
+ }
468
+ return result;
469
+ }
470
+
471
+ getXPathSelector(climb) {
472
+ if (typeof climb !== "number" || climb < 0) {
473
+ // throw new Error("Climb must be a non-negative integer");
474
+ console.error("Climb must be a non-negative integer");
475
+ return "";
476
+ }
477
+ if (climb === 0) return "";
478
+ let selector = "xpath=..";
479
+ if (climb === 1) {
480
+ return selector;
481
+ }
482
+ for (let i = 1; i < climb; i++) {
483
+ selector += "/..";
484
+ }
485
+ return selector;
486
+ }
487
+ categorizeLocators(element, locators, options) {
488
+ const unique = [];
489
+ const nonUnique = [];
490
+ const visible = options?.visible ?? true;
491
+ try {
492
+ for (const locator of locators) {
493
+ if (!locator || !locator.css || typeof locator.css !== "string") {
494
+ console.error("Locator must have a valid css selector found: ", locator);
495
+ continue;
496
+ }
497
+ if (visible === false) {
498
+ locator.visible = false;
499
+ }
500
+ const elements = this.getMatchingElements(locator.css, options);
501
+ if (elements.length === 0) {
502
+ console.warn(`No elements found for locator: ${locator.css}`);
503
+ continue;
504
+ } else if (elements.length === 1) {
505
+ if (element === elements[0]) {
506
+ locator.priority = 1;
507
+ unique.push(locator);
508
+ } else if (element.contains(elements[0])) {
509
+ locator.priority = 1;
510
+ const climb = this.dom_Parent.getClimbCountToParent(elements[0], element);
511
+ const climbSelector = this.getXPathSelector(climb);
512
+ const newSelector = `${locator.css} >> ${climbSelector}`;
513
+ locator.css = newSelector;
514
+ unique.push(locator);
515
+ }
516
+ } else {
517
+ locator.priority = 2;
518
+ locator.elements = elements;
519
+ nonUnique.push(locator);
520
+ }
521
+ }
522
+ } catch (error) {
523
+ console.error("Error categorizing locators:", error);
524
+ }
525
+ return { unique, nonUnique };
526
+ }
527
+
528
+ getUniqueLocators(element, locatorGenerator = this.getNoTextLocators, options = {}) {
529
+ return this.getUniqueLocators2(element, locatorGenerator, options);
530
+ }
531
+ getUniqueLocators1(element, locatorGenerator = this.getNoTextLocators, options = {}) {
532
+ try {
533
+ const { maxLocators = 5, root = window.document.body, next = "LCA", minLocators = 3 } = options;
534
+
535
+ if (!element) {
536
+ return [];
537
+ }
538
+ if (element === root) {
539
+ if (element === window.document.documentElement) {
540
+ return [
541
+ {
542
+ css: "html",
543
+ score: 1,
544
+ priority: 1,
545
+ },
546
+ {
547
+ css: ":root",
548
+ score: 1,
549
+ priority: 1,
550
+ },
551
+ ];
552
+ } else {
553
+ return [
554
+ {
555
+ css: ":root",
556
+ score: 1,
557
+ priority: 1,
558
+ // }, {
559
+ // css: ":root",
560
+ // score: 1,
561
+ // priority: 1,
562
+ },
563
+ ];
564
+ }
565
+ }
566
+
567
+ console.log("Generating locators for element:", element);
568
+ const locators = locatorGenerator(element, options);
569
+ console.log("Generated locators:", locators);
570
+ if (!locators || !Array.isArray(locators)) {
571
+ // throw new Error("Locator generator did not return an array of locators");
572
+ console.error("Locator generator did not return an array of locators");
573
+ return [];
574
+ }
575
+
576
+ console.log("Categorizing locators for element:", element);
577
+ const categorizedLocators = this.categorizeLocators(element, locators, options);
578
+ console.log("Categorized locators:", categorizedLocators);
579
+ // categorizedLocators.unique = limitLocators(categorizedLocators.unique, options);
580
+ // categorizedLocators.nonUnique = limitLocators(categorizedLocators.nonUnique, options);
581
+
582
+ const { unique, nonUnique } = categorizedLocators;
583
+ const result = [];
584
+ if (unique.length > 0) {
585
+ result.push(...unique);
586
+ }
587
+ if (result.length >= maxLocators) {
588
+ return result.slice(0, maxLocators);
589
+ }
590
+ let nextElement = null;
591
+ for (const locator of nonUnique) {
592
+ const selector = locator.css ?? locator.selector;
593
+ const elements = locator.elements || this.getMatchingElements(selector, options);
594
+
595
+ if (next === "parent") {
596
+ nextElement = this.dom_Parent.getActualParent(element);
597
+ } else {
598
+ // find the branching element the child of common parent that contains the element
599
+ const branchingParent = this.dom_Parent.findBranchingParent(elements, element);
600
+ nextElement = branchingParent;
601
+ }
602
+
603
+ if (nextElement && nextElement !== element) {
604
+ if (root.contains(nextElement)) {
605
+ const _result = this.getUniqueLocators(nextElement, locatorGenerator, {
606
+ ...options,
607
+ root,
608
+ });
609
+ for (const _locator of _result) {
610
+ if (result.length >= maxLocators) {
611
+ return result.slice(0, maxLocators);
612
+ }
613
+ const _selector = _locator.css ?? _locator.selector;
614
+ const fullSelector = `${_selector} >> ${selector}`;
615
+ const _elements = this.getMatchingElements(fullSelector, options);
616
+ const effectiveScore = (_locator.score + locator.score) / 2 + 100;
617
+ if (_elements.length === 1 && _elements[0] === element) {
618
+ _locator.css = fullSelector;
619
+ _locator.score = effectiveScore;
620
+ _locator.priority = 1; // unique locators have higher priority
621
+ result.push(_locator);
622
+ } else {
623
+ const index = _elements.indexOf(element);
624
+ if (index !== -1 && index < 5) {
625
+ // _locator.selector = fullSelector;
626
+ _locator.css = fullSelector;
627
+ _locator.index = index;
628
+ _locator.priority = 2; // non-unique locators have lower priority
629
+ _locator.score = effectiveScore;
630
+ result.push(_locator);
631
+ }
632
+ }
633
+ }
634
+ } else {
635
+ const index = elements.indexOf(element);
636
+ if (index !== -1 && index < 5) {
637
+ locator.index = index;
638
+ locator.priority = 2; // non-unique locators have lower priority
639
+ result.push(locator);
640
+ }
641
+ }
642
+ } else {
643
+ const index = elements.indexOf(element);
644
+ if (index !== -1 && index < 5) {
645
+ locator.index = index;
646
+ locator.priority = 2; // non-unique locators have lower priority
647
+ result.push(locator);
648
+ }
649
+ }
650
+
651
+ delete locator.elements; // remove elements to avoid memory leak
652
+ delete locator.strategy;
653
+ delete locator.engine;
654
+ delete locator.selector;
655
+ }
656
+
657
+ if (result.length < minLocators && root && root.contains(element)) {
658
+ const parent = this.dom_Parent.getActualParent(element);
659
+ const locs = this.getUniqueLocators(parent, locatorGenerator, {
660
+ ...options,
661
+ root,
662
+ });
663
+ result.push(...locs);
664
+ }
665
+
666
+ result.sort((a, b) => a.score - b.score);
667
+ console.log("Final locators:", result, element);
668
+ console.groupEnd();
669
+ return result.slice(0, maxLocators);
670
+ } catch (error) {
671
+ console.error("Error in getUniqueLocators:", error);
672
+ return [];
673
+ }
674
+ }
675
+
676
+ getUniqueLocators2(element, locatorGenerator = this.getNoTextLocators, options = {}) {
677
+ try {
678
+ const { maxLocators = 5, root = window.document.body, prefix } = options;
679
+
680
+ if (!element) {
681
+ return [];
682
+ }
683
+ if (element === root) {
684
+ if (element === window.document.documentElement) {
685
+ return [
686
+ {
687
+ css: "html",
688
+ score: 1,
689
+ priority: 1,
690
+ },
691
+ {
692
+ css: ":root",
693
+ score: 1,
694
+ priority: 1,
695
+ },
696
+ ];
697
+ } else {
698
+ return [];
699
+ }
700
+ }
701
+
702
+ console.log("Generating locators for element:", element);
703
+ const locators = locatorGenerator(element, options);
704
+ console.log("Generated locators:", locators);
705
+ if (!locators || !Array.isArray(locators)) {
706
+ console.error("Locator generator did not return an array of locators");
707
+ return [];
708
+ }
709
+
710
+ console.log("Categorizing locators for element:", element);
711
+ const categorizedLocators = this.categorizeLocators(element, locators, options);
712
+ console.log("Categorized locators:", categorizedLocators);
713
+
714
+ const { unique, nonUnique } = categorizedLocators;
715
+ const result = [];
716
+ if (unique.length > 0) {
717
+ result.push(...unique);
718
+ }
719
+ if (result.length >= maxLocators) {
720
+ return result.slice(0, maxLocators);
721
+ }
722
+
723
+ const elementsCache = new Map();
724
+
725
+ const allAncestors = prefix ? [element] : this.dom_Parent.getFullAncestorChainToRoot(element, root);
726
+ allAncestors.shift(); // remove the element itself from the ancestors
727
+
728
+ const cache = new Map();
729
+ const textToIgnore = this.PW.selectorUtils.elementText(cache, element).full.trim();
730
+ const ancestorLocators = [];
731
+ let uniqueAncestor = null;
732
+ for (const ancestor of allAncestors) {
733
+ const _locators = locatorGenerator(ancestor, { ...options, textToIgnore });
734
+ if (!_locators || !Array.isArray(_locators) || _locators.length === 0) {
735
+ continue;
736
+ }
737
+ const _categorized = this.categorizeLocators(ancestor, _locators, options);
738
+ ancestorLocators.push({
739
+ element: ancestor,
740
+ locators: _categorized,
741
+ });
742
+ elementsCache.set(ancestor, _categorized);
743
+ if (_categorized.unique.length > 0) {
744
+ uniqueAncestor = {
745
+ element: ancestor,
746
+ locators: _categorized,
747
+ };
748
+ break;
749
+ }
750
+ }
751
+
752
+ // const uniqueAncestor = ancestorLocators[ancestorLocators.length - 1];
753
+
754
+ for (const locator of nonUnique) {
755
+ const selector = locator.css ?? locator.selector;
756
+ const elements = locator.elements || this.getMatchingElements(selector, options);
757
+ if (elements.length === 0) {
758
+ console.warn(`No elements found for locator: ${selector}`);
759
+ continue;
760
+ }
761
+ if (!uniqueAncestor) {
762
+ const elements = this.getMatchingElements(selector, options);
763
+ if (elements.length === 1 && elements[0] === element) {
764
+ result.push({
765
+ css: selector,
766
+ score: locator.score,
767
+ priority: 1,
768
+ });
769
+ } else {
770
+ const index = elements.indexOf(element);
771
+ if (index !== -1) {
772
+ result.push({
773
+ css: selector,
774
+ index,
775
+ score: locator.score + 200,
776
+ priority: 2,
777
+ });
778
+ }
779
+ }
780
+ continue;
781
+ }
782
+
783
+ for (const unique_locator of uniqueAncestor.locators.unique) {
784
+ const fullSelector = `${unique_locator.css} >> ${selector}`;
785
+ const elements = this.getMatchingElements(fullSelector, options);
786
+ if (elements.length === 1 && elements[0] === element) {
787
+ const effectiveScore = (unique_locator.score + locator.score) / 2 + 100;
788
+ const newLocator = {
789
+ ...unique_locator,
790
+ css: fullSelector,
791
+ score: effectiveScore,
792
+ priority: 1, // unique locators have higher priority
793
+ };
794
+ result.push(newLocator);
795
+ } else {
796
+ const index = elements.indexOf(element);
797
+ if (index !== -1 && index < 5) {
798
+ const effectiveScore = (unique_locator.score + locator.score) / 2;
799
+ const newLocator = {
800
+ ...unique_locator,
801
+ css: fullSelector,
802
+ index,
803
+ score: effectiveScore + 200,
804
+ priority: 2, // non-unique locators have lower priority
805
+ };
806
+ result.push(newLocator);
807
+ }
808
+ }
809
+ }
810
+ }
811
+ result.sort((a, b) => a.score - b.score);
812
+ console.log("Final locators:", result, element);
813
+ console.groupEnd();
814
+ return result.slice(0, maxLocators);
815
+ } catch (error) {
816
+ console.error("Error in getUniqueLocators:", error);
817
+ return [];
818
+ }
819
+ }
820
+ isElementVisible(element) {
821
+ if (!(element instanceof Element)) return false;
822
+ const style = window.getComputedStyle(element);
823
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
824
+ const rect = element.getBoundingClientRect();
825
+ if (rect.width === 0 || rect.height === 0) return false;
826
+ return true;
827
+ }
828
+ getElementLocators(element, options = {}) {
829
+ const isVisible = this.isElementVisible(element);
830
+ if (isVisible === false) {
831
+ console.warn("Element is not visible: ", element);
832
+ options.visible = isVisible;
833
+ }
834
+ try {
835
+ const {
836
+ excludeText = false,
837
+ strategies = {
838
+ [this.locatorStrategies.custom]: true,
839
+ [this.locatorStrategies.context]: true,
840
+ [this.locatorStrategies.text]: true,
841
+ [this.locatorStrategies.text_with_index]: true,
842
+ [this.locatorStrategies.digitIgnore]: true,
843
+ [this.locatorStrategies.no_text]: true,
844
+ },
845
+ } = options;
846
+
847
+ const allStrategyLocators = {
848
+ [this.locatorStrategies.custom]: [],
849
+ [this.locatorStrategies.context]: [],
850
+ [this.locatorStrategies.text]: [],
851
+ [this.locatorStrategies.text_with_index]: [],
852
+ [this.locatorStrategies.digitIgnore]: [],
853
+ [this.locatorStrategies.no_text]: [],
854
+ };
855
+ if (
856
+ strategies[this.locatorStrategies.custom] &&
857
+ this.options?.customAttributes &&
858
+ Array.isArray(this.options.customAttributes) &&
859
+ this.options.customAttributes.length > 0
860
+ ) {
861
+ console.groupCollapsed("Generating Custom locators for element:", element);
862
+ const customLocators = this.getUniqueLocators(element, this.getCustomLocators.bind(this), options);
863
+ if (customLocators.length > 0) {
864
+ allStrategyLocators[this.locatorStrategies.custom] = customLocators;
865
+ }
866
+ }
867
+ console.groupEnd();
868
+ if (!excludeText) {
869
+ if (strategies[this.locatorStrategies.text]) {
870
+ console.groupCollapsed("Generating Text locators for element:", element);
871
+
872
+ const basicLocators = this.getUniqueLocators(element, this.getTextLocators.bind(this), options);
873
+ console.groupEnd();
874
+ if (basicLocators.length > 0) {
875
+ allStrategyLocators[this.locatorStrategies.text] = basicLocators;
876
+ }
877
+ if (strategies[this.locatorStrategies.text_with_index]) {
878
+ const textWithIndexLocators = this.getTextwithIndexLocators(basicLocators);
879
+ if (textWithIndexLocators.length > 0) {
880
+ allStrategyLocators[this.locatorStrategies.text_with_index] = textWithIndexLocators;
881
+ }
882
+ }
883
+ if (strategies[this.locatorStrategies.digitIgnore]) {
884
+ const digitIgnoreLocators = this.getDigitIgnoreLocators(element, basicLocators);
885
+ if (digitIgnoreLocators.length > 0) {
886
+ allStrategyLocators[this.locatorStrategies.digitIgnore] = digitIgnoreLocators;
887
+ }
888
+ }
889
+ if (strategies[this.locatorStrategies.context]) {
890
+ const contextLocators = this.getContextLocators(element, basicLocators);
891
+ if (contextLocators.length > 0) {
892
+ allStrategyLocators[this.locatorStrategies.context] = contextLocators;
893
+ }
894
+ }
895
+ }
896
+ }
897
+ if (strategies[this.locatorStrategies.no_text]) {
898
+ console.groupCollapsed("Generating No Text locators for element:", element);
899
+ const noTextLocators = this.getUniqueLocators(element, this.getNoTextLocators.bind(this), options);
900
+
901
+ if (noTextLocators.length > 0) {
902
+ allStrategyLocators[this.locatorStrategies.no_text] = noTextLocators;
903
+ } else {
904
+ const _locators = [];
905
+ _locators.push({
906
+ css: this.generateUniqueCSSSelector(element, options),
907
+ score: 500,
908
+ priority: 3,
909
+ });
910
+ if (_locators.length > 0) {
911
+ allStrategyLocators[this.locatorStrategies.no_text] = _locators;
912
+ }
913
+ }
914
+ console.groupEnd();
915
+ }
916
+
917
+ let bestStrategy = this.getBestStrategy(allStrategyLocators);
918
+ if (!bestStrategy) {
919
+ throw new Error("No locators found for element: " + element);
920
+ }
921
+ allStrategyLocators.strategy = bestStrategy;
922
+
923
+ const locators = allStrategyLocators[allStrategyLocators.strategy] ?? [];
924
+ const result = {
925
+ locators,
926
+ element_name: "",
927
+ };
928
+ if (Object.keys(allStrategyLocators).length > 1) {
929
+ result.allStrategyLocators = allStrategyLocators;
930
+ }
931
+ console.log("Generated locators:", result);
932
+ return result;
933
+ } catch (error) {
934
+ console.error("Error in getElementLocators:", error);
935
+ return {
936
+ locators: [],
937
+ element_name: "",
938
+ };
939
+ }
940
+ }
941
+ getBestStrategy(allStrategyLocators) {
942
+ const orderedPriorities = [
943
+ this.locatorStrategies.custom,
944
+ this.locatorStrategies.context,
945
+ this.locatorStrategies.text,
946
+ this.locatorStrategies.text_with_index,
947
+ this.locatorStrategies.digitIgnore,
948
+ this.locatorStrategies.no_text,
949
+ ];
950
+ for (const strategy of orderedPriorities) {
951
+ if (allStrategyLocators[strategy] && allStrategyLocators[strategy].length > 0) {
952
+ return strategy;
953
+ }
954
+ }
955
+ return null;
956
+ }
832
957
  }
833
958
 
834
959
  export default LocatorGenerator;