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