ff-dom 1.0.16 → 1.0.18-beta.1

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.
@@ -1,685 +1,513 @@
1
- //@ts-nocheck
2
- import { JSDOM, VirtualConsole } from "jsdom";
3
-
4
- type ElementDetails = {
5
- id?: string | null;
6
- className?: string | null;
7
- xpathByText?: string | null;
8
- xpathById?: string | null;
9
- xpathByClass?: string | null;
10
- xpathAbsolute?: string | null;
11
- xpathByName?: string | null;
12
- xpathByPlaceholder?: string | null;
13
- xpathByType?: string | null;
14
- visibleText?: string | null;
15
- relativeXpath?: string | null;
16
- [key: string]: any;
17
- };
18
-
19
- const isUnique = (xpathResult: XPathResult): boolean => {
20
- return xpathResult && xpathResult.snapshotLength === 1;
21
- };
22
-
23
- const getElementFromShadowRoot = (
24
- element: Element,
25
- selector: string
26
- ): Element | null => {
27
- const shadowRoot = (element as HTMLElement).shadowRoot;
28
- if (shadowRoot && !selector.includes("dynamic")) {
29
- return shadowRoot.querySelector(selector);
30
- }
31
- return null;
32
- };
33
-
34
- const isSVGElement = (element: Element): boolean => {
35
- return (
36
- typeof window !== "undefined" &&
37
- typeof SVGElement !== "undefined" &&
38
- element instanceof SVGElement
39
- );
40
- };
41
-
42
- const getXPath = (element: Element, xpath: string): string | null => {
43
- const window = element.ownerDocument.defaultView;
44
- if (!window) return null;
45
-
46
- const xpathEvaluator = new window.XPathEvaluator();
47
- const xpathResult = xpathEvaluator.evaluate(
48
- xpath,
49
- element.ownerDocument,
50
- null,
51
- window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
52
- null
53
- );
54
-
55
- return isUnique(xpathResult) ? xpath : null;
56
- };
57
-
58
- const getId = (element: Element | null): string | null => {
59
- return element?.id || null;
60
- };
61
-
62
- const getClassName = (element: Element): string | null => {
63
- return (element as HTMLElement).className || null;
64
- };
65
-
66
- const getVisibleText = (element: Element): string | null => {
67
- return element.textContent?.trim() || null;
68
- };
69
-
70
- const getXPathByText = (element: Element): string | null => {
71
- const text = getVisibleText(element);
72
- if (text) {
73
- const xpath = `//${element.nodeName.toLowerCase()}[text()='${text}']`;
74
- return getXPath(element, xpath);
75
- }
76
- return null;
77
- };
78
-
79
- const getXPathById = (element: Element): string | null => {
80
- if (element.id) {
81
- const xpath = `//${element.nodeName.toLowerCase()}[@id='${element.id}']`;
82
- return getXPath(element, xpath);
83
- }
84
- return null;
85
- };
86
-
87
- const getXPathByClass = (element: Element): string | null => {
88
- const classNames = (element as HTMLElement).classList;
89
- if (classNames.length > 0) {
90
- const xpath = `//${element.nodeName.toLowerCase()}[@class='${
91
- classNames[0]
92
- }']`;
93
- return getXPath(element, xpath);
94
- }
95
- return null;
96
- };
97
-
98
- const getXpathByName = (element: Element): string | null => {
99
- const selector = element.nodeName.toLowerCase();
100
- const elementEl = element as HTMLElement;
101
-
102
- if (elementEl.hasAttribute("name")) {
103
- const attrValue = elementEl.getAttribute("name");
104
- const xpath = `//${selector}[@name='${attrValue}']`;
105
- return getXPath(element, xpath) || null;
106
- }
107
-
108
- return null;
109
- };
110
-
111
- const getName = (element: Element): string | null => {
112
- const elementEl = element as HTMLElement;
113
-
114
- if (elementEl.hasAttribute("name")) {
115
- const attrValue = elementEl.getAttribute("name");
116
- const name = `${attrValue}`;
117
- return name || null;
118
- }
119
-
120
- return null;
121
- };
122
-
123
- const getXpathByPlaceholder = (element: Element): string | null => {
124
- const selector = element.nodeName.toLowerCase();
125
- const elementEl = element as HTMLElement;
126
-
127
- if (elementEl.hasAttribute("placeholder")) {
128
- const attrValue = elementEl.getAttribute("placeholder");
129
- const xpath = `//${selector}[@placeholder='${attrValue}']`;
130
- return getXPath(element, xpath) || null;
131
- }
132
-
133
- return null;
134
- };
135
-
136
- const getXpathByType = (element: Element): string | null => {
137
- const selector = element.nodeName.toLowerCase();
138
- const elementEl = element as HTMLElement;
139
-
140
- if (elementEl.hasAttribute("type")) {
141
- const attrValue = elementEl.getAttribute("type");
142
- const xpath = `//${selector}[@type='${attrValue}']`;
143
- return getXPath(element, xpath) || null;
144
- }
145
-
146
- return null;
147
- };
148
-
149
- const getXPathAbsolute = (element: Element): string => {
150
- const path: string[] = [];
151
- while (element && element.nodeType === 1) {
152
- // ELEMENT_NODE
153
- let selector = element.nodeName.toLowerCase();
154
- let sibling = element;
155
- let siblingCount = 1;
156
-
157
- while ((sibling = sibling.previousElementSibling as Element)) {
158
- if (sibling.nodeName.toLowerCase() === element.nodeName.toLowerCase()) {
159
- siblingCount++;
160
- }
161
- }
162
-
163
- if (siblingCount > 1) {
164
- selector += `[${siblingCount}]`;
165
- }
166
- path.unshift(selector);
167
- element = element.parentNode as Element;
168
- }
169
- return "//" + path.join("/");
170
- };
171
-
172
- const relations: string[] = [
173
- "/preceding-sibling",
174
- "/following-sibling",
175
- "/parent",
176
- "/descendant",
177
- "/ancestor",
178
- "/self",
179
- "/ancestor-or-self",
180
- "/child",
181
- "/preceding",
182
- "/following",
183
- ];
184
-
185
- const normalizeXPath = (xpath: string): string => {
186
- xpath = xpath.replace(/text\(\)\s*=\s*['"](.+?)['"]/g, "normalize-space(.)='$1'");
187
- xpath = xpath.replace(/\.\s*=\s*['"](.+?)['"]/g, "normalize-space(.)='$1'");
188
-
189
- return xpath;
190
- };
191
-
192
- function getElementFromXPath(
193
- tempDiv: HTMLElement,
194
- xpath: string
195
- ): Element | null {
196
- let currentElement: Element | null = tempDiv;
197
- const window = currentElement.ownerDocument.defaultView;
198
- if (!window) return null;
199
-
200
- const xpathEvaluator = new window.XPathEvaluator();
201
- const xpathResult = xpathEvaluator.evaluate(
202
- xpath,
203
- currentElement.ownerDocument, //here even tempDiv can be passed
204
- null,
205
- window.XPathResult.FIRST_ORDERED_NODE_TYPE,
206
- null
207
- );
208
- return xpathResult.singleNodeValue as Element | null;
209
- }
210
-
211
- function checkReferenceElementIsValid(
212
- locator: string,
213
- relation: string,
214
- tempDiv: HTMLElement
215
- ): string | null {
216
- if (locator.includes(relation)) {
217
- const locatotSplitArray: string[] = locator.split(relation);
218
- const sourceLoc = locatotSplitArray[0].trim();
219
- let currentElement: Element | null = tempDiv;
220
- const window = currentElement.ownerDocument.defaultView;
221
- if (!window) return null;
222
- if (!locator.includes("dynamic")) {
223
- const xpathEvaluator = new window.XPathEvaluator();
224
- const xpathResult = xpathEvaluator.evaluate(
225
- sourceLoc,
226
- currentElement.ownerDocument,
227
- null,
228
- window.XPathResult.FIRST_ORDERED_NODE_TYPE,
229
- null
230
- );
231
-
232
- const sourceElement = xpathResult.singleNodeValue;
233
- if (sourceElement) {
234
- const xpathResultComplete = xpathEvaluator.evaluate(
235
- locator,
236
- currentElement.ownerDocument,
237
- null,
238
- window.XPathResult.FIRST_ORDERED_NODE_TYPE,
239
- null
240
- );
241
- const completeElement = xpathResultComplete.singleNodeValue;
242
- let relativeXpath: string;
243
- if (completeElement) {
244
- relativeXpath = locator;
245
- return relativeXpath;
246
- } else {
247
- console.error("Complete Locator is Invalid:", locator);
248
- relativeXpath = locator;
249
- return relativeXpath;
250
- }
251
- } else {
252
- console.error("Source Locator Not Found:", sourceLoc);
253
- }
254
- }
255
- }
256
- return null;
257
- }
258
- interface Locator {
259
- name: string;
260
- type: string;
261
- value: string;
262
- reference: string;
263
- status: string;
264
- isRecorded: string;
265
- }
266
-
267
- const getElementsFromHTML = (
268
- name: string,
269
- desc: string,
270
- type: string,
271
- locators: Locator[],
272
- isShared: string,
273
- projectId: string,
274
- projectType: string,
275
- isRecorded: string,
276
- folder: string,
277
- parentId: string,
278
- parentName: string,
279
- platform: string,
280
- licenseId: string,
281
- licenseType: string,
282
- userId: string,
283
- htmlString: string
284
- ): ElementDetails | null => {
285
- const virtualConsole = new VirtualConsole();
286
- const dom = new JSDOM(htmlString, {
287
- resources: "usable",
288
- runScripts: "outside-only", // Prevents inline script execution in JSDOM
289
- pretendToBeVisual: true,
290
- virtualConsole,
291
- includeNodeLocations: true,
292
- });
293
-
294
- const document = dom.window.document;
295
- global.SVGElement = dom.window.SVGElement;
296
- const tempDiv = document.createElement("div");
297
- const elementsToRemove = document.querySelectorAll(
298
- "script, style, link[rel='stylesheet'], meta, noscript, embed, object, param, source, svg"
299
- );
300
-
301
- if (elementsToRemove) {
302
- elementsToRemove.forEach((tag) => {
303
- (tag as Element).remove();
304
- });
305
- }
306
-
307
- tempDiv.innerHTML = document.body.innerHTML;
308
- let finalLocators = [];
309
-
310
- locators: for (const locator of locators) {
311
- try {
312
- const isRecorded = String(locator.isRecorded || "");
313
- const recordedNLocators = locators.filter((l) => l.isRecorded === "N");
314
-
315
- if (recordedNLocators.length > 0) {
316
- for (const locator of recordedNLocators) {
317
- finalLocators.push({
318
- name: locator.name,
319
- type: locator.type,
320
- value: locator.value,
321
- reference: locator.reference,
322
- status: locator.status,
323
- isRecorded: locator.isRecorded,
324
- });
325
- }
326
- }
327
-
328
- let relativeXpath: string | null = null;
329
- const isDynamic = String(locator.value || locator.type || "");
330
- if (
331
- isDynamic.includes("dynamic") ||
332
- isDynamic.match("dynamic") ||
333
- isDynamic.includes("{") ||
334
- isDynamic.includes("}")
335
- ) {
336
- finalLocators.push({
337
- name: locator.name,
338
- type: locator.type,
339
- value: locator.value,
340
- reference: locator.reference,
341
- status: locator.status,
342
- isRecorded: locator.isRecorded,
343
- });
344
- continue;
345
- }
346
-
347
- if (isShared.includes("Y")) {
348
- break locators;
349
- }
350
- for (const relation of relations) {
351
- try {
352
- let targetElement: Element | null = null;
353
- if (locator.value.startsWith("iframe")) {
354
- const iframe = tempDiv.querySelector(
355
- locator.value
356
- ) as HTMLIFrameElement;
357
- if (iframe) {
358
- const iframeDocument =
359
- iframe.contentDocument || iframe.contentWindow?.document;
360
- if (iframeDocument) {
361
- targetElement = iframeDocument.querySelector(
362
- locator.value.slice(6)
363
- );
364
- }
365
- }
366
- } else {
367
- const selectors = locator.value.split(">>>"); // Custom delimiter for shadow DOM
368
- let currentElement: Element | null = tempDiv;
369
-
370
- for (const selector of selectors) {
371
- if (currentElement) {
372
- const trimmedSelector = selector.trim();
373
- if (
374
- locator.name.includes("id") ||
375
- trimmedSelector.startsWith("#")
376
- ) {
377
- targetElement = currentElement.querySelector(
378
- "#" + trimmedSelector
379
- );
380
- } else if (
381
- locator.name.includes("className") ||
382
- trimmedSelector.startsWith(".")
383
- ) {
384
- targetElement = currentElement.querySelector(
385
- "." + trimmedSelector
386
- );
387
- } else if (
388
- (locator.name.includes("xpath") ||
389
- trimmedSelector.startsWith("//")) &&
390
- !locator.type.match("dynamic")
391
- ) {
392
- if (tempDiv.innerHTML) {
393
- const normalizedXPath = normalizeXPath(trimmedSelector);
394
- targetElement = getElementFromXPath(
395
- tempDiv,
396
- normalizedXPath
397
- );
398
- if (targetElement) {
399
- finalLocators.push({
400
- name: locator.name,
401
- type: locator.type,
402
- value: trimmedSelector,
403
- reference: locator.reference,
404
- status: locator.status,
405
- isRecorded: String(locator.isRecorded).includes("N")
406
- ? "N"
407
- : "Y",
408
- });
409
- }
410
- }
411
- } else {
412
- targetElement = currentElement.querySelector(trimmedSelector);
413
- if (!targetElement) {
414
- targetElement = getElementFromShadowRoot(
415
- currentElement,
416
- trimmedSelector
417
- );
418
- }
419
- }
420
-
421
- if (
422
- !targetElement &&
423
- isSVGElement(currentElement) &&
424
- !locator.type.match("dynamic")
425
- ) {
426
- targetElement = currentElement.querySelector(trimmedSelector);
427
- }
428
-
429
- currentElement = targetElement;
430
- } else {
431
- console.error("Element not found at:", selector);
432
- break;
433
- }
434
- }
435
- }
436
-
437
- const locatorExists = (name: string, value: string) => {
438
- return finalLocators.some(
439
- (loc) =>
440
- loc.name === name &&
441
- loc.value === value &&
442
- (!loc.value.includes("dynamic") ||
443
- !locator.type.match("dynamic"))
444
- );
445
- };
446
-
447
- const xpathFunctions = [
448
- { name: "xpath", value: getXPathByText },
449
- { name: "xpath", value: getXPathById },
450
- { name: "xpath", value: getXPathByClass },
451
- { name: "xpath", value: getXPathAbsolute },
452
- { name: "xpath", value: getXpathByName },
453
- { name: "xpath", value: getXpathByPlaceholder },
454
- { name: "xpath", value: getXpathByType },
455
- ];
456
-
457
- if (targetElement) {
458
- const idValue = getId(targetElement);
459
- if (idValue && !locatorExists("id", idValue)) {
460
- locators.forEach((loc) => {
461
- if (locator.value === idValue && locator.name === "id") {
462
- finalLocators.push({
463
- name: "id",
464
- type: locator.type,
465
- value: idValue,
466
- reference: locator.reference,
467
- status: locator.status,
468
- isRecorded: locator.isRecorded,
469
- });
470
- } else {
471
- finalLocators.push({
472
- name: "id",
473
- type: locator.type,
474
- value: idValue,
475
- reference: locator.reference,
476
- status: locator.status,
477
- isRecorded: "Y",
478
- isSelfHealed: "Y",
479
- });
480
- }
481
- });
482
- }
483
- if (getVisibleText(targetElement)) {
484
- const textValue = getVisibleText(targetElement);
485
- locators.forEach((loc) => {
486
- if (loc.value === textValue) {
487
- finalLocators.push({
488
- name: "linkText",
489
- type: "static",
490
- value: textValue,
491
- reference: locator.reference,
492
- status: locator.status,
493
- isRecorded: locator.isRecorded,
494
- });
495
- } else {
496
- finalLocators.push({
497
- name: "linkText",
498
- type: "static",
499
- value: textValue,
500
- reference: locator.reference,
501
- status: locator.status,
502
- isRecorded: "Y",
503
- isSelfHealed: "Y",
504
- });
505
- }
506
- });
507
- }
508
-
509
- if (getName(targetElement)) {
510
- const nameLocator = getName(targetElement);
511
- if (nameLocator && !locatorExists("name", nameLocator)) {
512
- locators.forEach((loc) => {
513
- if (loc.value === nameLocator && locator.name === "name") {
514
- finalLocators.push({
515
- name: "name",
516
- type: "static",
517
- value: nameLocator,
518
- reference: locator.reference,
519
- status: locator.status,
520
- isRecorded: locator.isRecorded,
521
- });
522
- } else {
523
- finalLocators.push({
524
- name: "name",
525
- type: "static",
526
- value: nameLocator,
527
- reference: locator.reference,
528
- status: locator.status,
529
- isRecorded: "Y",
530
- isSelfHealed: "Y",
531
- });
532
- }
533
- });
534
- }
535
- }
536
-
537
- const classValue = getClassName(targetElement);
538
- if (
539
- classValue &&
540
- classValue.trim() !== "" &&
541
- !locatorExists("className", classValue)
542
- ) {
543
- locators.forEach((loc) => {
544
- if (loc.value === classValue && locator.name === "className") {
545
- finalLocators.push({
546
- name: "className",
547
- type: locator.type,
548
- value: classValue,
549
- reference: locator.reference,
550
- status: locator.status,
551
- isRecorded: locator.isRecorded,
552
- });
553
- } else {
554
- finalLocators.push({
555
- name: "className",
556
- type: locator.type,
557
- value: classValue,
558
- reference: locator.reference,
559
- status: locator.status,
560
- isRecorded: "Y",
561
- isSelfHealed: "Y",
562
- });
563
- }
564
- });
565
- }
566
- xpathFunctions.forEach((fn) => {
567
- locators.forEach((loc) => {
568
- if (loc.value === fn.value(targetElement)) {
569
- finalLocators.push({
570
- name: fn.name,
571
- type: locator.type,
572
- value: fn.value(targetElement),
573
- reference: locator.reference,
574
- status: locator.status,
575
- isRecorded: locator.isRecorded,
576
- });
577
- } else {
578
- finalLocators.push({
579
- name: fn.name,
580
- type: locator.type,
581
- value: fn.value(targetElement),
582
- reference: locator.reference,
583
- status: locator.status,
584
- isRecorded: "Y",
585
- isSelfHealed: "Y",
586
- });
587
- }
588
- });
589
- });
590
-
591
- for (const locator of locators) {
592
- try {
593
- let relativeXpath = null;
594
- if (locator.value) {
595
- for (const relation of relations) {
596
- if (locator.value.includes(relation)) {
597
- relativeXpath = checkReferenceElementIsValid(
598
- locator.value,
599
- relation,
600
- tempDiv
601
- );
602
-
603
- if (relativeXpath) {
604
- finalLocators.push({
605
- name: "xpath",
606
- type: locator.type,
607
- value: relativeXpath,
608
- reference: locator.reference,
609
- status: locator.status,
610
- isRecorded:
611
- locator.isRecorded !== "" ||
612
- locator.isRecorded !== null
613
- ? locator.isRecorded
614
- : "Y",
615
- });
616
- break;
617
- }
618
- }
619
- }
620
- }
621
- } catch (error) {
622
- console.error("Error processing locator:", locator, error);
623
- }
624
- }
625
-
626
- let uniqueLocators = new Map();
627
-
628
- finalLocators.forEach((obj) => {
629
- let val = obj.value;
630
- if (obj.isRecorded === 'Y' && val) {
631
- val = String(val).replace(/"/g, "'").replace(/\\'/g, "'");
632
- }
633
-
634
- // filter out redundant/invalid values
635
- if (!val || val === 'null' || val === "''") return;
636
-
637
- // ensure unique entries
638
- if (!uniqueLocators.has(val)) {
639
- uniqueLocators.set(val, { ...obj, value: val });
640
- }
641
- });
642
-
643
- let ffLoc = Array.from(uniqueLocators.values());
644
-
645
- const jsonResult = [
646
- {
647
- name: `${name}`,
648
- desc: `${desc}`,
649
- type: `${type}`,
650
- locators: ffLoc.filter(
651
- (locator) =>
652
- locator.hasOwnProperty("value") &&
653
- locator?.value !== null &&
654
- locator.value !== ""
655
- ),
656
- isShared: `${isShared}`,
657
- projectId: `${projectId}`,
658
- projectType: `${projectType}`,
659
- isRecorded: `${isRecorded}`,
660
- folder: `${folder}`,
661
- parentId: `${parentId}`,
662
- parentName: `${parentName}`,
663
- platform: `${platform}`,
664
- licenseId: `${licenseId}`,
665
- licenseType: `${licenseType}`,
666
- userId: `${userId}`,
667
- },
668
- ];
669
-
670
- return jsonResult;
671
- }
672
- } catch (error) {
673
- console.error("Error processing locator:", locator, error);
674
- continue;
675
- }
676
- }
677
- } catch (error) {
678
- console.error("Error processing locator:", locator, error);
679
- continue;
680
- }
681
- }
682
- return null;
683
- };
684
-
685
- export { getElementsFromHTML };
1
+ import { ElementRecord, Locator } from "../types/locator.ts";
2
+ import { parseDOM } from "./xpath.ts";
3
+ import { isSvg, normalizeXPath } from "./xpathHelpers.ts";
4
+
5
+ type ElementDetails = {
6
+ id?: string | null;
7
+ className?: string | null;
8
+ xpathByText?: string | null;
9
+ xpathById?: string | null;
10
+ xpathByClass?: string | null;
11
+ xpathAbsolute?: string | null;
12
+ xpathByName?: string | null;
13
+ xpathByPlaceholder?: string | null;
14
+ xpathByType?: string | null;
15
+ visibleText?: string | null;
16
+ relativeXpath?: string | null;
17
+ [key: string]: any;
18
+ };
19
+
20
+ const getElementFromShadowRoot = (
21
+ element: Element,
22
+ selector: string
23
+ ): Element | null => {
24
+ const shadowRoot = (element as HTMLElement).shadowRoot;
25
+ if (shadowRoot && !selector.includes("dynamic")) {
26
+ return shadowRoot.querySelector(selector);
27
+ }
28
+ return null;
29
+ };
30
+
31
+ const getId = (element: Element | null): string | null => {
32
+ return element?.id || null;
33
+ };
34
+
35
+ const getClassName = (element: Element): string | null => {
36
+ return (element as HTMLElement).className || null;
37
+ };
38
+
39
+ const getVisibleText = (element: Element): string | null => {
40
+ return element.textContent?.trim() || null;
41
+ };
42
+
43
+ const getName = (element: Element): string | null => {
44
+ const elementEl = element as HTMLElement;
45
+
46
+ if (elementEl.hasAttribute("name")) {
47
+ const attrValue = elementEl.getAttribute("name");
48
+ const name = `${attrValue}`;
49
+ return name || null;
50
+ }
51
+ return null;
52
+ };
53
+
54
+ const relations: string[] = [
55
+ "/preceding-sibling",
56
+ "/following-sibling",
57
+ "/parent",
58
+ "/descendant",
59
+ "/ancestor",
60
+ "/self",
61
+ "/ancestor-or-self",
62
+ "/child",
63
+ "/preceding",
64
+ "/following",
65
+ ];
66
+
67
+ function getElementFromXPath(
68
+ tempDiv: HTMLElement,
69
+ xpath: string
70
+ ): Element | null {
71
+ let currentElement: Element | null = tempDiv;
72
+ const window = currentElement.ownerDocument.defaultView;
73
+ if (!window) return null;
74
+
75
+ const xpathEvaluator = new window.XPathEvaluator();
76
+ const xpathResult = xpathEvaluator.evaluate(
77
+ xpath,
78
+ currentElement.ownerDocument, //here even tempDiv can be passed
79
+ null,
80
+ window.XPathResult.FIRST_ORDERED_NODE_TYPE,
81
+ null
82
+ );
83
+ return xpathResult.singleNodeValue as Element | null;
84
+ }
85
+
86
+ function checkReferenceElementIsValid(
87
+ locator: string,
88
+ relation: string,
89
+ tempDiv: HTMLElement
90
+ ): string | null {
91
+ if (locator.includes(relation)) {
92
+ const locatotSplitArray: string[] = locator.split(relation);
93
+ const sourceLoc = locatotSplitArray[0].trim();
94
+ let currentElement: Element | null = tempDiv;
95
+ const window = currentElement.ownerDocument.defaultView;
96
+ if (!window) return null;
97
+ if (!locator.includes("dynamic")) {
98
+ const xpathEvaluator = new window.XPathEvaluator();
99
+ const xpathResult = xpathEvaluator.evaluate(
100
+ sourceLoc,
101
+ currentElement.ownerDocument,
102
+ null,
103
+ window.XPathResult.FIRST_ORDERED_NODE_TYPE,
104
+ null
105
+ );
106
+
107
+ const sourceElement = xpathResult.singleNodeValue;
108
+ if (sourceElement) {
109
+ const xpathResultComplete = xpathEvaluator.evaluate(
110
+ locator,
111
+ currentElement.ownerDocument,
112
+ null,
113
+ window.XPathResult.FIRST_ORDERED_NODE_TYPE,
114
+ null
115
+ );
116
+ const completeElement = xpathResultComplete.singleNodeValue;
117
+ let relativeXpath: string;
118
+ if (completeElement) {
119
+ relativeXpath = locator;
120
+ return relativeXpath;
121
+ } else {
122
+ console.error("Complete Locator is Invalid:", locator);
123
+ relativeXpath = locator;
124
+ return relativeXpath;
125
+ }
126
+ } else {
127
+ console.error("Source Locator Not Found:", sourceLoc);
128
+ }
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+
134
+ const getElementsFromHTML = (
135
+ record: ElementRecord,
136
+ docmt: Document
137
+ ): ElementDetails | null => {
138
+ const document = docmt;
139
+ // global.SVGElement = document.defaultView?.SVGElement!;
140
+ const tempDiv = document.createElement("div");
141
+ const elementsToRemove = document.querySelectorAll(
142
+ "script, style, link[rel='stylesheet'], meta, noscript, embed, object, param, source, svg"
143
+ );
144
+
145
+ if (elementsToRemove) {
146
+ elementsToRemove.forEach((tag) => {
147
+ (tag as Element).remove();
148
+ });
149
+ }
150
+
151
+ tempDiv.innerHTML = document.body.innerHTML;
152
+ const finalLocatorsSet: Set<string> = new Set();
153
+ let finalLocators: any[] = [];
154
+
155
+ function createLocator(base: any, overrides: Partial<any> = {}) {
156
+ const newLocator: any = {
157
+ name: overrides.name ?? base?.name,
158
+ type: overrides.type ?? base?.type,
159
+ value: overrides.value ?? base?.value,
160
+ reference: overrides.reference ?? base?.reference,
161
+ status: overrides.status ?? base?.status,
162
+ isRecorded: overrides.isRecorded ?? base?.isRecorded,
163
+ };
164
+
165
+ if (overrides.hasOwnProperty("isSelfHealed")) {
166
+ newLocator.isSelfHealed = overrides.isSelfHealed;
167
+ } else if (base?.hasOwnProperty("isSelfHealed")) {
168
+ newLocator.isSelfHealed = base.isSelfHealed;
169
+ }
170
+
171
+ pushUniqueLocator(newLocator);
172
+ }
173
+
174
+ function pushUniqueLocator(obj: any) {
175
+ const key = `${obj.name}:${obj.value}`;
176
+ if (!finalLocatorsSet.has(key)) {
177
+ finalLocatorsSet.add(key);
178
+ finalLocators.push(obj);
179
+ }
180
+ }
181
+
182
+ /** Locator Value Cleaner (Handles Special Scenarios) **/
183
+ const cleanLocatorValue = (
184
+ val: string | null | undefined,
185
+ type?: string,
186
+ isRecorded?: string
187
+ ): string | null => {
188
+ if (!val) return null;
189
+
190
+ let cleaned = val.trim();
191
+
192
+ // Return null for empty or literal "null"
193
+ if (!cleaned || cleaned.toLowerCase() === "null") return null;
194
+
195
+ // Unescape any escaped quotes
196
+ cleaned = cleaned.replace(/\\"/g, '"').replace(/\\'/g, "'");
197
+
198
+ // Remove surrounding single or double quotes
199
+ cleaned = cleaned.replace(/^['"](.+?)['"]$/, "$1");
200
+
201
+ // Replace double single quotes with a single quote inside XPath
202
+ cleaned = cleaned.replace(/''/g, "'");
203
+
204
+ // Normalize double quotes in XPath attribute selectors [@id="" -> [@id='']
205
+ cleaned = cleaned.replace(
206
+ /\[@(id|name)=['"]{2}(.+?)['"]{2}\]/g,
207
+ "[@$1='$2']"
208
+ );
209
+
210
+ // For DOM selectors (id or name), remove ALL quotes
211
+ if (type === "id" || type === "name") {
212
+ cleaned = cleaned.replace(/['"]/g, "").trim();
213
+ }
214
+
215
+ if (type === "xpath" && isRecorded === "Y" && !val.startsWith("//"))
216
+ return null;
217
+
218
+ // Final check for empty strings
219
+ if (!cleaned || /^['"]{2}$/.test(cleaned)) return null;
220
+
221
+ return cleaned;
222
+ };
223
+
224
+ locators: for (const locator of record.locators) {
225
+ try {
226
+ const isRecorded = String(locator.isRecorded || "");
227
+ const recordedNLocators = record.locators.filter((l) => l.isRecorded === "N");
228
+
229
+ if (recordedNLocators.length > 0) {
230
+ for (const locator of recordedNLocators) {
231
+ createLocator(locator);
232
+ }
233
+ }
234
+
235
+ const isDynamic = String(locator.value || locator.type || "");
236
+ if (
237
+ isDynamic.includes("dynamic") ||
238
+ isDynamic.match("dynamic") ||
239
+ isDynamic.includes("{") ||
240
+ isDynamic.includes("}")
241
+ ) {
242
+ createLocator(locator);
243
+ continue;
244
+ }
245
+
246
+ if (record.isShared.includes("Y")) {
247
+ break locators;
248
+ }
249
+
250
+ for (const relation of relations) {
251
+ try {
252
+ let targetElement: Element | null = null;
253
+ if (locator.value.startsWith("iframe")) {
254
+ const iframe = tempDiv.querySelector(
255
+ locator.value
256
+ ) as HTMLIFrameElement;
257
+ if (iframe) {
258
+ const iframeDocument =
259
+ iframe.contentDocument || iframe.contentWindow?.document;
260
+ if (iframeDocument) {
261
+ targetElement = iframeDocument.querySelector(
262
+ locator.value.slice(6)
263
+ );
264
+ }
265
+ }
266
+ } else {
267
+ const selectors = locator.value.split(">>>"); // Custom delimiter for shadow DOM
268
+ let currentElement: Element | null = tempDiv;
269
+
270
+ for (const selector of selectors) {
271
+ if (currentElement) {
272
+ const trimmedSelector = selector.trim();
273
+ if (
274
+ locator.name.includes("id") ||
275
+ trimmedSelector.startsWith("#")
276
+ ) {
277
+ targetElement = currentElement.querySelector(
278
+ "#" + trimmedSelector
279
+ );
280
+ } else if (
281
+ locator.name.includes("className") ||
282
+ trimmedSelector.startsWith(".")
283
+ ) {
284
+ targetElement = currentElement.querySelector(
285
+ "." + trimmedSelector
286
+ );
287
+ } else if (
288
+ (locator.name.includes("xpath") ||
289
+ trimmedSelector.startsWith("//")) &&
290
+ !locator.type.match("dynamic")
291
+ ) {
292
+ if (tempDiv.innerHTML) {
293
+ const normalizedXPath = normalizeXPath(trimmedSelector);
294
+ targetElement = getElementFromXPath(
295
+ tempDiv,
296
+ normalizedXPath
297
+ );
298
+ if (targetElement) {
299
+ createLocator(locator, {
300
+ value: trimmedSelector,
301
+ isRecorded: String(locator.isRecorded).includes("N")
302
+ ? "N"
303
+ : "Y",
304
+ });
305
+ }
306
+ }
307
+ } else {
308
+ targetElement = currentElement.querySelector(trimmedSelector);
309
+ if (!targetElement) {
310
+ targetElement = getElementFromShadowRoot(
311
+ currentElement,
312
+ trimmedSelector
313
+ );
314
+ }
315
+ }
316
+
317
+ if (
318
+ !targetElement &&
319
+ isSvg(currentElement) &&
320
+ !locator.type.match("dynamic")
321
+ ) {
322
+ targetElement = currentElement.querySelector(trimmedSelector);
323
+ }
324
+ currentElement = targetElement;
325
+ } else {
326
+ console.error("Element not found at:", selector);
327
+ break;
328
+ }
329
+ }
330
+ }
331
+
332
+ const locatorExists = (name: string, value: string): boolean => {
333
+ const key = `${name}:${value}`;
334
+ return finalLocatorsSet.has(key);
335
+ };
336
+
337
+ if (targetElement) {
338
+ const idValue = getId(targetElement);
339
+ if (idValue && !locatorExists("id", idValue)) {
340
+ record.locators.forEach((loc) => {
341
+ createLocator(loc, {
342
+ name: "id",
343
+ value: idValue,
344
+ isRecorded:
345
+ loc.value === idValue && loc.name === "id"
346
+ ? loc.isRecorded
347
+ : "Y",
348
+ isSelfHealed:
349
+ loc.value === idValue && loc.name === "id"
350
+ ? loc.isSelfHealed
351
+ : "Y",
352
+ });
353
+ });
354
+ }
355
+
356
+ const textValue = getVisibleText(targetElement);
357
+ if (textValue) {
358
+ record.locators.forEach((loc) => {
359
+ createLocator(loc, {
360
+ name: "linkText",
361
+ type: "static",
362
+ value: textValue,
363
+ isRecorded: loc.value === textValue ? loc.isRecorded : "Y",
364
+ isSelfHealed:
365
+ loc.value === textValue ? loc.isSelfHealed : "Y",
366
+ });
367
+ });
368
+ }
369
+
370
+ const nameLocator = getName(targetElement);
371
+ if (nameLocator && !locatorExists("name", nameLocator)) {
372
+ record.locators.forEach((loc) => {
373
+ createLocator(loc, {
374
+ name: "name",
375
+ type: "static",
376
+ value: nameLocator,
377
+ isRecorded:
378
+ loc.value === nameLocator && loc.name === "name"
379
+ ? loc.isRecorded
380
+ : "Y",
381
+ isSelfHealed:
382
+ loc.value === nameLocator && loc.name === "name"
383
+ ? loc.isSelfHealed
384
+ : "Y",
385
+ });
386
+ });
387
+ }
388
+
389
+ const classValue = getClassName(targetElement);
390
+ if (
391
+ classValue &&
392
+ classValue.trim() !== "" &&
393
+ !locatorExists("className", classValue)
394
+ ) {
395
+ record.locators.forEach((loc) => {
396
+ createLocator(loc, {
397
+ name: "className",
398
+ value: classValue,
399
+ isRecorded:
400
+ loc.value === classValue && loc.name === "className"
401
+ ? loc.isRecorded
402
+ : "Y",
403
+ isSelfHealed:
404
+ loc.value === classValue && loc.name === "className"
405
+ ? loc.isSelfHealed
406
+ : "Y",
407
+ });
408
+ });
409
+ }
410
+
411
+ const fnValue = parseDOM(targetElement, document, false, true);
412
+ record.locators.forEach((loc) => {
413
+ createLocator(loc, {
414
+ name: 'xpath',
415
+ value: fnValue,
416
+ isRecorded: fnValue?.find((x) => x.value === loc.value)
417
+ ? loc.isRecorded
418
+ : "Y",
419
+ isSelfHealed: fnValue?.find((x) => x.value === loc.value)
420
+ ? loc.isSelfHealed
421
+ : "Y",
422
+ });
423
+ });
424
+
425
+ for (const locator of record.locators) {
426
+ try {
427
+ for (const loc of record.locators) {
428
+ if (!loc.value) continue;
429
+
430
+ for (const relation of relations) {
431
+ if (loc.value.includes(relation)) {
432
+ const relativeXpath = checkReferenceElementIsValid(
433
+ loc.value,
434
+ relation,
435
+ tempDiv
436
+ );
437
+ if (relativeXpath) {
438
+ createLocator(loc, {
439
+ name: "xpath",
440
+ value: relativeXpath,
441
+ isRecorded:
442
+ locator.isRecorded !== "" &&
443
+ locator.isRecorded !== null
444
+ ? locator.isRecorded
445
+ : "Y",
446
+ });
447
+ break;
448
+ }
449
+ }
450
+ }
451
+ }
452
+ } catch (error) {
453
+ console.error("Error processing locator:", locator, error);
454
+ }
455
+ }
456
+
457
+ const finalAutoHealedLocators = finalLocators.map((obj) => ({
458
+ ...obj,
459
+ value: cleanLocatorValue(obj.value, obj.name, obj.isRecorded),
460
+ }));
461
+
462
+ const finalUniqueAutoHealedLocators: Locator[] =
463
+ finalAutoHealedLocators.reduce(
464
+ (unique: Locator[], locator: Locator) => {
465
+ if (
466
+ locator.value &&
467
+ !unique.some((l: Locator) => l.value === locator.value)
468
+ ) {
469
+ unique.push(locator);
470
+ }
471
+ return unique;
472
+ },
473
+ [] as Locator[]
474
+ );
475
+
476
+ const jsonResult = [
477
+ {
478
+ name: `${record.name}`,
479
+ desc: `${record.desc}`,
480
+ type: `${record.type}`,
481
+ locators: finalUniqueAutoHealedLocators.filter(
482
+ (locator) => locator?.value != null && locator.value !== ""
483
+ ),
484
+ isShared: `${record.isShared}`,
485
+ projectId: `${record.projectId}`,
486
+ projectType: `${record.projectType}`,
487
+ isRecorded: `${record.isRecorded}`,
488
+ folder: `${record.folder}`,
489
+ parentId: `${record.parentId}`,
490
+ parentName: `${record.parentName}`,
491
+ platform: `${record.platform}`,
492
+ licenseId: `${record.licenseId}`,
493
+ licenseType: `${record.licenseType}`,
494
+ userId: `${record.userId}`,
495
+ },
496
+ ];
497
+
498
+ return jsonResult;
499
+ }
500
+ } catch (error) {
501
+ console.error("Error processing locator:", locator, error);
502
+ continue;
503
+ }
504
+ }
505
+ } catch (error) {
506
+ console.error("Error processing locator:", locator, error);
507
+ continue;
508
+ }
509
+ }
510
+ return null;
511
+ };
512
+
513
+ export { getElementsFromHTML };