ff-dom 1.0.16 → 1.0.17

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,5 +1,4 @@
1
- //@ts-nocheck
2
- import { JSDOM, VirtualConsole } from "jsdom";
1
+ import { JSDOM, VirtualConsole } from 'jsdom';
3
2
 
4
3
  type ElementDetails = {
5
4
  id?: string | null;
@@ -25,7 +24,7 @@ const getElementFromShadowRoot = (
25
24
  selector: string
26
25
  ): Element | null => {
27
26
  const shadowRoot = (element as HTMLElement).shadowRoot;
28
- if (shadowRoot && !selector.includes("dynamic")) {
27
+ if (shadowRoot && !selector.includes('dynamic')) {
29
28
  return shadowRoot.querySelector(selector);
30
29
  }
31
30
  return null;
@@ -33,8 +32,8 @@ const getElementFromShadowRoot = (
33
32
 
34
33
  const isSVGElement = (element: Element): boolean => {
35
34
  return (
36
- typeof window !== "undefined" &&
37
- typeof SVGElement !== "undefined" &&
35
+ typeof window !== 'undefined' &&
36
+ typeof SVGElement !== 'undefined' &&
38
37
  element instanceof SVGElement
39
38
  );
40
39
  };
@@ -44,13 +43,7 @@ const getXPath = (element: Element, xpath: string): string | null => {
44
43
  if (!window) return null;
45
44
 
46
45
  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
- );
46
+ const xpathResult = xpathEvaluator.evaluate(xpath, element.ownerDocument, null, window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
54
47
 
55
48
  return isUnique(xpathResult) ? xpath : null;
56
49
  };
@@ -99,24 +92,22 @@ const getXpathByName = (element: Element): string | null => {
99
92
  const selector = element.nodeName.toLowerCase();
100
93
  const elementEl = element as HTMLElement;
101
94
 
102
- if (elementEl.hasAttribute("name")) {
103
- const attrValue = elementEl.getAttribute("name");
95
+ if (elementEl.hasAttribute('name')) {
96
+ const attrValue = elementEl.getAttribute('name');
104
97
  const xpath = `//${selector}[@name='${attrValue}']`;
105
98
  return getXPath(element, xpath) || null;
106
99
  }
107
-
108
100
  return null;
109
101
  };
110
102
 
111
103
  const getName = (element: Element): string | null => {
112
104
  const elementEl = element as HTMLElement;
113
105
 
114
- if (elementEl.hasAttribute("name")) {
115
- const attrValue = elementEl.getAttribute("name");
106
+ if (elementEl.hasAttribute('name')) {
107
+ const attrValue = elementEl.getAttribute('name');
116
108
  const name = `${attrValue}`;
117
109
  return name || null;
118
110
  }
119
-
120
111
  return null;
121
112
  };
122
113
 
@@ -124,12 +115,11 @@ const getXpathByPlaceholder = (element: Element): string | null => {
124
115
  const selector = element.nodeName.toLowerCase();
125
116
  const elementEl = element as HTMLElement;
126
117
 
127
- if (elementEl.hasAttribute("placeholder")) {
128
- const attrValue = elementEl.getAttribute("placeholder");
118
+ if (elementEl.hasAttribute('placeholder')) {
119
+ const attrValue = elementEl.getAttribute('placeholder');
129
120
  const xpath = `//${selector}[@placeholder='${attrValue}']`;
130
121
  return getXPath(element, xpath) || null;
131
122
  }
132
-
133
123
  return null;
134
124
  };
135
125
 
@@ -137,12 +127,11 @@ const getXpathByType = (element: Element): string | null => {
137
127
  const selector = element.nodeName.toLowerCase();
138
128
  const elementEl = element as HTMLElement;
139
129
 
140
- if (elementEl.hasAttribute("type")) {
141
- const attrValue = elementEl.getAttribute("type");
130
+ if (elementEl.hasAttribute('type')) {
131
+ const attrValue = elementEl.getAttribute('type');
142
132
  const xpath = `//${selector}[@type='${attrValue}']`;
143
133
  return getXPath(element, xpath) || null;
144
134
  }
145
-
146
135
  return null;
147
136
  };
148
137
 
@@ -166,33 +155,33 @@ const getXPathAbsolute = (element: Element): string => {
166
155
  path.unshift(selector);
167
156
  element = element.parentNode as Element;
168
157
  }
169
- return "//" + path.join("/");
158
+ return '//' + path.join('/');
170
159
  };
171
160
 
172
161
  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",
162
+ '/preceding-sibling',
163
+ '/following-sibling',
164
+ '/parent',
165
+ '/descendant',
166
+ '/ancestor',
167
+ '/self',
168
+ '/ancestor-or-self',
169
+ '/child',
170
+ '/preceding',
171
+ '/following',
183
172
  ];
184
173
 
185
174
  const normalizeXPath = (xpath: string): string => {
175
+ // Replace text() comparisons like text() = 'something' with normalize-space(.) = 'something'
176
+ // to avoid issues with whitespace differences
186
177
  xpath = xpath.replace(/text\(\)\s*=\s*['"](.+?)['"]/g, "normalize-space(.)='$1'");
187
- xpath = xpath.replace(/\.\s*=\s*['"](.+?)['"]/g, "normalize-space(.)='$1'");
188
178
 
179
+ // Replace direct . = 'something' comparisons with normalize-space(.) = 'something' for consistent text matching
180
+ xpath = xpath.replace(/\.\s*=\s*['"](.+?)['"]/g, "normalize-space(.)='$1'");
189
181
  return xpath;
190
182
  };
191
183
 
192
- function getElementFromXPath(
193
- tempDiv: HTMLElement,
194
- xpath: string
195
- ): Element | null {
184
+ function getElementFromXPath(tempDiv: HTMLElement, xpath: string): Element | null {
196
185
  let currentElement: Element | null = tempDiv;
197
186
  const window = currentElement.ownerDocument.defaultView;
198
187
  if (!window) return null;
@@ -208,18 +197,14 @@ function getElementFromXPath(
208
197
  return xpathResult.singleNodeValue as Element | null;
209
198
  }
210
199
 
211
- function checkReferenceElementIsValid(
212
- locator: string,
213
- relation: string,
214
- tempDiv: HTMLElement
215
- ): string | null {
200
+ function checkReferenceElementIsValid(locator: string, relation: string, tempDiv: HTMLElement): string | null {
216
201
  if (locator.includes(relation)) {
217
202
  const locatotSplitArray: string[] = locator.split(relation);
218
203
  const sourceLoc = locatotSplitArray[0].trim();
219
204
  let currentElement: Element | null = tempDiv;
220
205
  const window = currentElement.ownerDocument.defaultView;
221
206
  if (!window) return null;
222
- if (!locator.includes("dynamic")) {
207
+ if (!locator.includes('dynamic')) {
223
208
  const xpathEvaluator = new window.XPathEvaluator();
224
209
  const xpathResult = xpathEvaluator.evaluate(
225
210
  sourceLoc,
@@ -244,12 +229,12 @@ function checkReferenceElementIsValid(
244
229
  relativeXpath = locator;
245
230
  return relativeXpath;
246
231
  } else {
247
- console.error("Complete Locator is Invalid:", locator);
232
+ console.error('Complete Locator is Invalid:', locator);
248
233
  relativeXpath = locator;
249
234
  return relativeXpath;
250
235
  }
251
236
  } else {
252
- console.error("Source Locator Not Found:", sourceLoc);
237
+ console.error('Source Locator Not Found:', sourceLoc);
253
238
  }
254
239
  }
255
240
  }
@@ -262,6 +247,7 @@ interface Locator {
262
247
  reference: string;
263
248
  status: string;
264
249
  isRecorded: string;
250
+ isSelfHealed?: string;
265
251
  }
266
252
 
267
253
  const getElementsFromHTML = (
@@ -284,8 +270,8 @@ const getElementsFromHTML = (
284
270
  ): ElementDetails | null => {
285
271
  const virtualConsole = new VirtualConsole();
286
272
  const dom = new JSDOM(htmlString, {
287
- resources: "usable",
288
- runScripts: "outside-only", // Prevents inline script execution in JSDOM
273
+ resources: 'usable',
274
+ runScripts: 'outside-only', // Prevents inline script execution in JSDOM
289
275
  pretendToBeVisual: true,
290
276
  virtualConsole,
291
277
  includeNodeLocations: true,
@@ -293,10 +279,8 @@ const getElementsFromHTML = (
293
279
 
294
280
  const document = dom.window.document;
295
281
  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
- );
282
+ const tempDiv = document.createElement('div');
283
+ const elementsToRemove = document.querySelectorAll("script, style, link[rel='stylesheet'], meta, noscript, embed, object, param, source, svg");
300
284
 
301
285
  if (elementsToRemove) {
302
286
  elementsToRemove.forEach((tag) => {
@@ -305,52 +289,101 @@ const getElementsFromHTML = (
305
289
  }
306
290
 
307
291
  tempDiv.innerHTML = document.body.innerHTML;
308
- let finalLocators = [];
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
+ };
309
358
 
310
359
  locators: for (const locator of locators) {
311
360
  try {
312
- const isRecorded = String(locator.isRecorded || "");
313
- const recordedNLocators = locators.filter((l) => l.isRecorded === "N");
361
+ const isRecorded = String(locator.isRecorded || '');
362
+ const recordedNLocators = locators.filter((l) => l.isRecorded === 'N');
314
363
 
315
364
  if (recordedNLocators.length > 0) {
316
365
  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
- });
366
+ createLocator(locator);
325
367
  }
326
368
  }
327
369
 
328
- let relativeXpath: string | null = null;
329
- const isDynamic = String(locator.value || locator.type || "");
370
+ const isDynamic = String(locator.value || locator.type || '');
330
371
  if (
331
- isDynamic.includes("dynamic") ||
332
- isDynamic.match("dynamic") ||
333
- isDynamic.includes("{") ||
334
- isDynamic.includes("}")
372
+ isDynamic.includes('dynamic') || isDynamic.match('dynamic') ||
373
+ isDynamic.includes('{') || isDynamic.includes('}')
335
374
  ) {
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
- });
375
+ createLocator(locator);
344
376
  continue;
345
377
  }
346
378
 
347
- if (isShared.includes("Y")) {
379
+ if (isShared.includes('Y')) {
348
380
  break locators;
349
381
  }
382
+
350
383
  for (const relation of relations) {
351
384
  try {
352
385
  let targetElement: Element | null = null;
353
- if (locator.value.startsWith("iframe")) {
386
+ if (locator.value.startsWith('iframe')) {
354
387
  const iframe = tempDiv.querySelector(
355
388
  locator.value
356
389
  ) as HTMLIFrameElement;
@@ -364,254 +397,139 @@ const getElementsFromHTML = (
364
397
  }
365
398
  }
366
399
  } else {
367
- const selectors = locator.value.split(">>>"); // Custom delimiter for shadow DOM
400
+ const selectors = locator.value.split('>>>'); // Custom delimiter for shadow DOM
368
401
  let currentElement: Element | null = tempDiv;
369
402
 
370
403
  for (const selector of selectors) {
371
404
  if (currentElement) {
372
405
  const trimmedSelector = selector.trim();
373
- if (
374
- locator.name.includes("id") ||
375
- trimmedSelector.startsWith("#")
376
- ) {
377
- targetElement = currentElement.querySelector(
378
- "#" + trimmedSelector
379
- );
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);
380
410
  } 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")
411
+ (locator.name.includes('xpath') || trimmedSelector.startsWith('//')) &&
412
+ !locator.type.match('dynamic')
391
413
  ) {
392
414
  if (tempDiv.innerHTML) {
393
415
  const normalizedXPath = normalizeXPath(trimmedSelector);
394
- targetElement = getElementFromXPath(
395
- tempDiv,
396
- normalizedXPath
397
- );
416
+ targetElement = getElementFromXPath(tempDiv,normalizedXPath);
398
417
  if (targetElement) {
399
- finalLocators.push({
400
- name: locator.name,
401
- type: locator.type,
418
+ createLocator(locator, {
402
419
  value: trimmedSelector,
403
- reference: locator.reference,
404
- status: locator.status,
405
- isRecorded: String(locator.isRecorded).includes("N")
406
- ? "N"
407
- : "Y",
420
+ isRecorded: String(locator.isRecorded).includes('N') ? 'N' : 'Y',
408
421
  });
409
422
  }
410
423
  }
411
424
  } else {
412
425
  targetElement = currentElement.querySelector(trimmedSelector);
413
426
  if (!targetElement) {
414
- targetElement = getElementFromShadowRoot(
415
- currentElement,
416
- trimmedSelector
417
- );
427
+ targetElement = getElementFromShadowRoot(currentElement, trimmedSelector);
418
428
  }
419
429
  }
420
430
 
421
- if (
422
- !targetElement &&
423
- isSVGElement(currentElement) &&
424
- !locator.type.match("dynamic")
425
- ) {
431
+ if (!targetElement && isSVGElement(currentElement) && !locator.type.match('dynamic')) {
426
432
  targetElement = currentElement.querySelector(trimmedSelector);
427
433
  }
428
-
429
434
  currentElement = targetElement;
430
435
  } else {
431
- console.error("Element not found at:", selector);
436
+ console.error('Element not found at:', selector);
432
437
  break;
433
438
  }
434
439
  }
435
440
  }
436
441
 
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
- );
442
+ const locatorExists = (name: string, value: string): boolean => {
443
+ const key = `${name}:${value}`;
444
+ return finalLocatorsSet.has(key);
445
445
  };
446
446
 
447
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 },
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
455
  ];
456
456
 
457
457
  if (targetElement) {
458
458
  const idValue = getId(targetElement);
459
- if (idValue && !locatorExists("id", idValue)) {
459
+ if (idValue && !locatorExists('id', idValue)) {
460
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
- }
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
+ });
481
467
  });
482
468
  }
483
- if (getVisibleText(targetElement)) {
484
- const textValue = getVisibleText(targetElement);
469
+
470
+ const textValue = getVisibleText(targetElement);
471
+ if (textValue) {
485
472
  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
- }
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
+ });
506
480
  });
507
481
  }
508
482
 
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
- }
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',
533
492
  });
534
- }
493
+ });
535
494
  }
536
495
 
537
496
  const classValue = getClassName(targetElement);
538
- if (
539
- classValue &&
540
- classValue.trim() !== "" &&
541
- !locatorExists("className", classValue)
542
- ) {
497
+ if (classValue && classValue.trim() !== '' && !locatorExists('className', classValue)) {
543
498
  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
- }
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
+ });
564
505
  });
565
506
  }
507
+
566
508
  xpathFunctions.forEach((fn) => {
509
+ const fnValue = fn.value(targetElement);
567
510
  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
- }
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
+ });
588
517
  });
589
518
  });
590
519
 
591
520
  for (const locator of locators) {
592
521
  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
- );
522
+ for (const loc of locators) {
523
+ if (!loc.value) continue;
602
524
 
525
+ for (const relation of relations) {
526
+ if (loc.value.includes(relation)) {
527
+ const relativeXpath = checkReferenceElementIsValid(loc.value, relation, tempDiv);
603
528
  if (relativeXpath) {
604
- finalLocators.push({
605
- name: "xpath",
606
- type: locator.type,
529
+ createLocator(loc, {
530
+ name: 'xpath',
607
531
  value: relativeXpath,
608
- reference: locator.reference,
609
- status: locator.status,
610
- isRecorded:
611
- locator.isRecorded !== "" ||
612
- locator.isRecorded !== null
613
- ? locator.isRecorded
614
- : "Y",
532
+ isRecorded: locator.isRecorded !== '' && locator.isRecorded !== null ? locator.isRecorded : 'Y',
615
533
  });
616
534
  break;
617
535
  }
@@ -619,40 +537,30 @@ const getElementsFromHTML = (
619
537
  }
620
538
  }
621
539
  } catch (error) {
622
- console.error("Error processing locator:", locator, error);
540
+ console.error('Error processing locator:', locator, error);
623
541
  }
624
542
  }
625
543
 
626
- let uniqueLocators = new Map();
544
+ const finalAutoHealedLocators = finalLocators.map((obj) => ({
545
+ ...obj,
546
+ value: cleanLocatorValue(obj.value, obj.name, obj.isRecorded),
547
+ }));
627
548
 
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());
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
+ );
644
557
 
645
558
  const jsonResult = [
646
559
  {
647
560
  name: `${name}`,
648
561
  desc: `${desc}`,
649
562
  type: `${type}`,
650
- locators: ffLoc.filter(
651
- (locator) =>
652
- locator.hasOwnProperty("value") &&
653
- locator?.value !== null &&
654
- locator.value !== ""
655
- ),
563
+ locators: finalUniqueAutoHealedLocators.filter((locator) => locator?.value != null && locator.value !== ''),
656
564
  isShared: `${isShared}`,
657
565
  projectId: `${projectId}`,
658
566
  projectType: `${projectType}`,
@@ -670,16 +578,16 @@ const getElementsFromHTML = (
670
578
  return jsonResult;
671
579
  }
672
580
  } catch (error) {
673
- console.error("Error processing locator:", locator, error);
581
+ console.error('Error processing locator:', locator, error);
674
582
  continue;
675
583
  }
676
584
  }
677
585
  } catch (error) {
678
- console.error("Error processing locator:", locator, error);
586
+ console.error('Error processing locator:', locator, error);
679
587
  continue;
680
588
  }
681
589
  }
682
590
  return null;
683
591
  };
684
592
 
685
- export { getElementsFromHTML };
593
+ export { getElementsFromHTML };