ff-automationv2 1.0.0 → 2.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/bitbucket-pipelines.yml +20 -0
  2. package/eslint.config.ts +29 -29
  3. package/package.json +51 -51
  4. package/src/ai/llmcalls/decodeApiKey.ts +14 -14
  5. package/src/ai/llmcalls/llmAction.ts +89 -89
  6. package/src/ai/llmcalls/parseLlmOputput.ts +69 -69
  7. package/src/ai/llmprompts/systemPrompts/actionExtractorPrompt.ts +70 -70
  8. package/src/ai/llmprompts/systemPrompts/errorDescriptionPrompt.ts +23 -23
  9. package/src/ai/llmprompts/systemPrompts/fireflinkElementIndexExtactors.ts +198 -198
  10. package/src/ai/llmprompts/systemPrompts/userStoryToListPrompt.ts +24 -24
  11. package/src/ai/llmprompts/systemPrompts/visionPrompt.ts +28 -28
  12. package/src/automation/actions/executor.ts +74 -74
  13. package/src/automation/actions/interaction/enterInput.ts +27 -27
  14. package/src/automation/actions/interface/interactionActionInterface.ts +26 -26
  15. package/src/automation/actions/interface/navigationActionInterface.ts +21 -21
  16. package/src/automation/actions/interface/waitActionInterface.ts +5 -5
  17. package/src/automation/actions/navigation/getTitle.ts +8 -8
  18. package/src/automation/actions/navigation/goBack.ts +8 -8
  19. package/src/automation/actions/navigation/navigate.ts +9 -9
  20. package/src/automation/actions/navigation/refresh.ts +8 -8
  21. package/src/automation/actions/wait/wait.ts +9 -9
  22. package/src/automation/browserSession/initiateBrowserSession.ts +81 -81
  23. package/src/core/constants/supportedActions.ts +7 -7
  24. package/src/core/interfaces/StableDomInterface.ts +5 -5
  25. package/src/core/interfaces/actionInterface.ts +13 -13
  26. package/src/core/interfaces/automationRunnerInterface.ts +2 -2
  27. package/src/core/interfaces/browserCapabilitiesInterface.ts +4 -4
  28. package/src/core/interfaces/browserConfigurationInterface.ts +2 -2
  29. package/src/core/interfaces/domAnalysisInterface.ts +34 -34
  30. package/src/core/interfaces/executionDetails.ts +29 -29
  31. package/src/core/interfaces/fireflinkScriptPayloadInterface.ts +39 -39
  32. package/src/core/interfaces/llmConfigurationInterface.ts +2 -2
  33. package/src/core/interfaces/llmResponseInterface.ts +38 -38
  34. package/src/core/interfaces/promptInterface.ts +21 -21
  35. package/src/core/interfaces/scriptGenrationDataInterface.ts +16 -16
  36. package/src/core/interfaces/toolsInterface.ts +5 -5
  37. package/src/core/main/actionHandlerFactory.ts +86 -86
  38. package/src/core/main/executionContext.ts +18 -18
  39. package/src/core/main/runAutomationScript.ts +177 -177
  40. package/src/core/main/stepProcessor.ts +28 -28
  41. package/src/core/types/llmResponseType.ts +10 -10
  42. package/src/core/types/visionllmInputType.ts +4 -4
  43. package/src/domAnalysis/getRelaventElements.ts +24 -24
  44. package/src/domAnalysis/relativeElementsFromDom.ts +94 -94
  45. package/src/domAnalysis/searchBest.ts +159 -159
  46. package/src/domAnalysis/simplifyAndFlatten.ts +118 -118
  47. package/src/fireflinkData/fireflinkLocators/elementsFromHTML.ts +656 -656
  48. package/src/fireflinkData/fireflinkLocators/getListOfLocators.ts +31 -31
  49. package/src/fireflinkData/fireflinkLocators/typeList.ts +36 -36
  50. package/src/fireflinkData/fireflinkScript/scriptGenrationData.ts +30 -30
  51. package/src/index.ts +5 -5
  52. package/src/llmConfig/llmConfiguration.ts +26 -26
  53. package/src/service/fireflinkApi.service.ts +46 -46
  54. package/src/service/scriptRunner.service.ts +83 -83
  55. package/src/utils/DomExtraction/jsForAttributeInjection.ts +254 -254
  56. package/src/utils/javascript/jsFindElement.ts +161 -161
  57. package/src/utils/javascript/jsForShadowRoot.ts +216 -216
  58. package/src/utils/javascript/jsForToaster.ts +60 -60
  59. package/src/utils/logger/logData.ts +36 -36
  60. package/tsconfig.json +26 -26
@@ -1,656 +1,656 @@
1
- import { JSDOM, VirtualConsole } from "jsdom";
2
- import { logger } from "../../utils/logger/logData.js"
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(
47
- xpath,
48
- element.ownerDocument,
49
- null,
50
- window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
51
- null
52
- );
53
-
54
- return isUnique(xpathResult) ? xpath : null;
55
- };
56
-
57
- const getId = (element: Element | null): string | null => {
58
- return element?.id || null;
59
- };
60
-
61
- const getClassName = (element: Element): string | null => {
62
- return (element as HTMLElement).className || null;
63
- };
64
-
65
- const getVisibleText = (element: Element): string | null => {
66
- return element.textContent?.trim() || null;
67
- };
68
-
69
- const getXPathByText = (element: Element): string | null => {
70
- const text = getVisibleText(element);
71
- if (text) {
72
- const xpath = `//${element.nodeName.toLowerCase()}[text()='${text}']`;
73
- return getXPath(element, xpath);
74
- }
75
- return null;
76
- };
77
-
78
-
79
-
80
-
81
-
82
- const getXPathById = (element: Element): string | null => {
83
- if (element.id) {
84
- const xpath = `//${element.nodeName.toLowerCase()}[@id='${element.id}']`;
85
- return getXPath(element, xpath);
86
- }
87
- return null;
88
- };
89
-
90
- const getXPathByClass = (element: Element): string | null => {
91
- const classNames = (element as HTMLElement).classList;
92
- if (classNames.length > 0) {
93
- const xpath = `//${element.nodeName.toLowerCase()}[@class='${classNames[0]
94
- }']`;
95
- return getXPath(element, xpath);
96
- }
97
- return null;
98
- };
99
-
100
- const getXpathByName = (element: Element): string | null => {
101
- const selector = element.nodeName.toLowerCase();
102
- const elementEl = element as HTMLElement;
103
-
104
- if (elementEl.hasAttribute("name")) {
105
- const attrValue = elementEl.getAttribute("name");
106
- const xpath = `//${selector}[@name='${attrValue}']`;
107
- return getXPath(element, xpath) || null;
108
- }
109
-
110
- return null;
111
- };
112
-
113
- const getName = (element: Element): string | null => {
114
- const elementEl = element as HTMLElement;
115
-
116
- if (elementEl.hasAttribute("name")) {
117
- const attrValue = elementEl.getAttribute("name");
118
- const name = `${attrValue}`;
119
- return name || null;
120
- }
121
-
122
- return null;
123
- };
124
-
125
- const getFFxpath = (element: Element): string | null => {
126
- const elementEl = element as HTMLElement;
127
-
128
- if (elementEl.hasAttribute("ff-xpath") && !elementEl.getAttribute("ff-xpath")?.includes("/iframe")) {
129
- const attrValue = elementEl.getAttribute("ff-xpath");
130
- const name = `${attrValue}`;
131
- return name || null;
132
- }
133
-
134
- return null;
135
- };
136
-
137
- const getXpathByPlaceholder = (element: Element): string | null => {
138
- const selector = element.nodeName.toLowerCase();
139
- const elementEl = element as HTMLElement;
140
-
141
- if (elementEl.hasAttribute("placeholder")) {
142
- const attrValue = elementEl.getAttribute("placeholder");
143
- const xpath = `//${selector}[@placeholder='${attrValue}']`;
144
- return getXPath(element, xpath) || null;
145
- }
146
-
147
- return null;
148
- };
149
-
150
-
151
-
152
- const getXpathByType = (element: Element): string | null => {
153
- const selector = element.nodeName.toLowerCase();
154
- const elementEl = element as HTMLElement;
155
-
156
- if (elementEl.hasAttribute("type")) {
157
- const attrValue = elementEl.getAttribute("type");
158
- const xpath = `//${selector}[@type='${attrValue}']`;
159
- return getXPath(element, xpath) || null;
160
- }
161
-
162
- return null;
163
- };
164
-
165
- const getXPathAbsolute = (element: Element): string => {
166
- const path: string[] = [];
167
- while (element && element.nodeType === 1) {
168
- // ELEMENT_NODE
169
- let selector = element.nodeName.toLowerCase();
170
- let sibling = element;
171
- let siblingCount = 1;
172
-
173
- while ((sibling = sibling.previousElementSibling as Element)) {
174
- if (sibling.nodeName.toLowerCase() === element.nodeName.toLowerCase()) {
175
- siblingCount++;
176
- }
177
- }
178
-
179
- if (siblingCount > 1) {
180
- selector += `[${siblingCount}]`;
181
- }
182
- path.unshift(selector);
183
- element = element.parentNode as Element;
184
- }
185
- return "//" + path.join("/");
186
- };
187
-
188
- const relations: string[] = [
189
- "/preceding-sibling",
190
- "/following-sibling",
191
- "/parent",
192
- "/descendant",
193
- "/ancestor",
194
- "/self",
195
- "/ancestor-or-self",
196
- "/child",
197
- "/preceding",
198
- "/following",
199
- ];
200
-
201
- function getElementFromXPath(
202
- tempDiv: HTMLElement,
203
- xpath: string
204
- ): Element | null {
205
- const currentElement: Element | null = tempDiv;
206
- const window = currentElement.ownerDocument.defaultView;
207
- if (!window) return null;
208
-
209
- const xpathEvaluator = new window.XPathEvaluator();
210
- const xpathResult = xpathEvaluator.evaluate(
211
- xpath,
212
- currentElement.ownerDocument,
213
- null,
214
- window.XPathResult.FIRST_ORDERED_NODE_TYPE,
215
- null
216
- );
217
- return xpathResult.singleNodeValue as Element | null;
218
- }
219
-
220
- function checkReferenceElementIsValid(
221
- locator: string,
222
- relation: string,
223
- tempDiv: HTMLElement
224
- ): string | null {
225
- if (locator.includes(relation)) {
226
- const locatotSplitArray: string[] = locator.split(relation);
227
- const sourceLoc = locatotSplitArray[0].trim();
228
- const currentElement: Element | null = tempDiv;
229
- const window = currentElement.ownerDocument.defaultView;
230
- if (!window) return null;
231
- if (!locator.includes("dynamic")) {
232
- const xpathEvaluator = new window.XPathEvaluator();
233
- const xpathResult = xpathEvaluator.evaluate(
234
- sourceLoc,
235
- currentElement.ownerDocument,
236
- null,
237
- window.XPathResult.FIRST_ORDERED_NODE_TYPE,
238
- null
239
- );
240
-
241
- const sourceElement = xpathResult.singleNodeValue;
242
- if (sourceElement) {
243
- const xpathResultComplete = xpathEvaluator.evaluate(
244
- locator,
245
- currentElement.ownerDocument,
246
- null,
247
- window.XPathResult.FIRST_ORDERED_NODE_TYPE,
248
- null
249
- );
250
- const completeElement = xpathResultComplete.singleNodeValue;
251
- let relativeXpath: string;
252
- if (completeElement) {
253
- relativeXpath = locator;
254
- return relativeXpath;
255
- } else {
256
- logger.error("Complete Locator is Invalid:", locator);
257
- relativeXpath = locator;
258
- return relativeXpath;
259
- }
260
- } else {
261
- logger.error("Source Locator Not Found:", sourceLoc);
262
- }
263
- }
264
- }
265
- return null;
266
- }
267
- interface Locator {
268
- name: string;
269
- type: string;
270
- value: string;
271
- reference: string;
272
- status: string;
273
- isRecorded: string;
274
- }
275
-
276
- const getElementsFromHTML = (
277
- locators: Locator[],
278
- htmlString: string
279
- ): ElementDetails | null => {
280
- const virtualConsole = new VirtualConsole();
281
- const dom = new JSDOM(htmlString, {
282
- resources: "usable",
283
- runScripts: "outside-only", // Prevents inline script execution in JSDOM
284
- pretendToBeVisual: true,
285
- virtualConsole,
286
- includeNodeLocations: true,
287
- });
288
-
289
- const document = dom.window.document;
290
- global.SVGElement = dom.window.SVGElement;
291
- const tempDiv = document.createElement("div");
292
- const elementsToRemove = document.querySelectorAll(
293
- "script, style, link[rel='stylesheet'], meta, noscript, embed, object, param, source, svg"
294
- );
295
-
296
- if (elementsToRemove) {
297
- elementsToRemove.forEach((tag) => {
298
- (tag as Element).remove();
299
- });
300
- }
301
-
302
- tempDiv.innerHTML = document.body.innerHTML;
303
- // tempDiv.innerHTML = htmlString;
304
- const finalLocators: any = [];
305
-
306
- for (const locator of locators) {
307
- try {
308
- const isDynamic = String(locator.value || locator.type || "");
309
- if (
310
- isDynamic.includes("dynamic") ||
311
- isDynamic.match("dynamic") ||
312
- isDynamic.includes("{") ||
313
- isDynamic.includes("}")
314
- ) {
315
- finalLocators.push({
316
- name: locator.name,
317
- type: locator.type,
318
- value: locator.value,
319
- reference: locator.reference,
320
- status: locator.status,
321
- isRecorded: locator.isRecorded,
322
- });
323
- continue;
324
- }
325
- const isRecorded = String(locator.isRecorded || "");
326
-
327
- if (isRecorded.includes("N") || isRecorded.match("N")) {
328
- finalLocators.push({
329
- name: locator.name,
330
- type: locator.type,
331
- value: locator.value,
332
- reference: locator.reference,
333
- status: locator.status,
334
- isRecorded: locator.isRecorded,
335
- });
336
- }
337
- try {
338
- let targetElement: Element | null = null;
339
- if (locator.value.startsWith("iframe")) {
340
- const iframe = tempDiv.querySelector(
341
- locator.value
342
- ) as HTMLIFrameElement;
343
- if (iframe) {
344
- const iframeDocument =
345
- iframe.contentDocument || iframe.contentWindow?.document;
346
- if (iframeDocument) {
347
- targetElement = iframeDocument.querySelector(
348
- locator.value.slice(6)
349
- );
350
- }
351
- }
352
- } else {
353
- const selectors = locator.value.split(">>>"); // Custom delimiter for shadow DOM
354
- let currentElement: Element | null = tempDiv;
355
-
356
- for (const selector of selectors) {
357
- if (currentElement) {
358
- const trimmedSelector = selector.trim();
359
- if (
360
- locator.name.includes("id") ||
361
- trimmedSelector.startsWith("#")
362
- ) {
363
- targetElement = currentElement.querySelector(
364
- "#" + trimmedSelector
365
- );
366
- } else if (
367
- locator.name.includes("className") ||
368
- trimmedSelector.startsWith(".")
369
- ) {
370
- targetElement = currentElement.querySelector(
371
- "." + trimmedSelector
372
- );
373
- } else if (
374
- (locator.name.includes("xpath") ||
375
- trimmedSelector.startsWith("//")) &&
376
- !locator.type.match("dynamic")
377
- ) {
378
- targetElement = getElementFromXPath(tempDiv, trimmedSelector);
379
- } else {
380
- targetElement = currentElement.querySelector(trimmedSelector);
381
- if (!targetElement) {
382
- targetElement = getElementFromShadowRoot(
383
- currentElement,
384
- trimmedSelector
385
- );
386
- }
387
- }
388
-
389
- if (
390
- !targetElement &&
391
- isSVGElement(currentElement) &&
392
- !locator.type.match("dynamic")
393
- ) {
394
- targetElement = currentElement.querySelector(trimmedSelector);
395
- }
396
-
397
- currentElement = targetElement;
398
- }
399
- }
400
- }
401
-
402
- const locatorExists = (name: string, value: string) => {
403
- return finalLocators.some(
404
- (_loc: any) =>
405
- _loc.name === name &&
406
- _loc.value === value &&
407
- (!_loc.value.includes("dynamic") ||
408
- !locator.type.match("dynamic"))
409
- );
410
- };
411
-
412
- const xpathFunctions = [
413
- { name: "xpath", value: getXPathByText },
414
- { name: "xpath", value: getXPathById },
415
- { name: "xpath", value: getXPathByClass },
416
- { name: "xpath", value: getXPathAbsolute },
417
- { name: "xpath", value: getXpathByName },
418
- { name: "xpath", value: getXpathByPlaceholder },
419
- { name: "xpath", value: getXpathByType },
420
- ];
421
-
422
- if (targetElement) {
423
- const idValue = getId(targetElement);
424
- if (idValue && !locatorExists("id", idValue)) {
425
- locators.forEach(() => {
426
- if (locator.value === idValue) {
427
- finalLocators.push({
428
- name: "id",
429
- type: locator.type,
430
- value: idValue,
431
- reference: locator.reference,
432
- status: locator.status,
433
- isRecorded: locator.isRecorded,
434
- isAiRecorded: "Y",
435
- });
436
- } else {
437
- finalLocators.push({
438
- name: "id",
439
- type: locator.type,
440
- value: idValue,
441
- reference: locator.reference,
442
- status: locator.status,
443
- isRecorded: "Y",
444
- isAiRecorded: "Y",
445
- });
446
- }
447
- });
448
- }
449
-
450
-
451
- if (getVisibleText(targetElement)) {
452
- const textValue = getVisibleText(targetElement);
453
- locators.forEach((_loc) => {
454
- if (_loc.value === textValue) {
455
- finalLocators.push({
456
- name: "linkText",
457
- type: "static",
458
- value: textValue,
459
- reference: locator.reference,
460
- status: locator.status,
461
- isRecorded: locator.isRecorded,
462
- isAiRecorded: "Y",
463
- });
464
- } else {
465
- finalLocators.push({
466
- name: "linkText",
467
- type: "static",
468
- value: textValue,
469
- reference: locator.reference,
470
- status: locator.status,
471
- isRecorded: "Y",
472
- isAiRecorded: "Y",
473
- });
474
- }
475
- });
476
- }
477
-
478
- if (getName(targetElement)) {
479
- const nameLocator = getName(targetElement);
480
- locators.forEach((_loc) => {
481
- if (_loc.value === nameLocator) {
482
- finalLocators.push({
483
- name: "name",
484
- type: "static",
485
- value: nameLocator,
486
- reference: locator.reference,
487
- status: locator.status,
488
- isRecorded: locator.isRecorded,
489
- isAiRecorded: "Y",
490
- });
491
- } else {
492
- finalLocators.push({
493
- name: "name",
494
- type: "static",
495
- value: nameLocator,
496
- reference: locator.reference,
497
- status: locator.status,
498
- isRecorded: "Y",
499
- isAiRecorded: "Y",
500
- });
501
- }
502
- });
503
- }
504
- if (getFFxpath(targetElement)) {
505
- const nameLocator = getFFxpath(targetElement);
506
- locators.forEach((_loc) => {
507
- if (_loc.value === nameLocator) {
508
- finalLocators.push({
509
- name: "xpath",
510
- type: "static",
511
- value: nameLocator,
512
- reference: locator.reference,
513
- status: locator.status,
514
- isRecorded: locator.isRecorded,
515
- isAiRecorded: "Y",
516
- });
517
- } else {
518
- finalLocators.push({
519
- name: "xpath",
520
- type: "static",
521
- value: nameLocator,
522
- reference: locator.reference,
523
- status: locator.status,
524
- isRecorded: "Y",
525
- isAiRecorded: "Y",
526
- });
527
- }
528
- });
529
- }
530
-
531
- const classValue = getClassName(targetElement);
532
- if (
533
- classValue &&
534
- classValue.trim() !== "" &&
535
- !locatorExists("className", classValue)
536
- ) {
537
- locators.forEach((_loc) => {
538
- if (_loc.value === classValue) {
539
- finalLocators.push({
540
- name: "className",
541
- type: locator.type,
542
- value: classValue,
543
- reference: locator.reference,
544
- status: locator.status,
545
- isRecorded: locator.isRecorded,
546
- isAiRecorded: "Y",
547
- });
548
- } else {
549
- finalLocators.push({
550
- name: "className",
551
- type: locator.type,
552
- value: classValue,
553
- reference: locator.reference,
554
- status: locator.status,
555
- isRecorded: "Y",
556
- isAiRecorded: "Y",
557
- });
558
- }
559
- });
560
- }
561
- xpathFunctions.forEach((fn) => {
562
- locators.forEach((_loc) => {
563
- if (_loc.value === fn.value(targetElement)) {
564
- finalLocators.push({
565
- name: fn.name,
566
- type: locator.type,
567
- value: fn.value(targetElement),
568
- reference: locator.reference,
569
- status: locator.status,
570
- isRecorded: locator.isRecorded,
571
- isAiRecorded: "Y",
572
- });
573
- } else {
574
- finalLocators.push({
575
- name: fn.name,
576
- type: locator.type,
577
- value: fn.value(targetElement),
578
- reference: locator.reference,
579
- status: locator.status,
580
- isRecorded: "Y",
581
- isAiRecorded: "Y",
582
- });
583
- }
584
- });
585
- });
586
-
587
- for (const locator of locators) {
588
- try {
589
- let relativeXpath = null;
590
- if (locator.value) {
591
- for (const relation of relations) {
592
- if (locator.value.includes(relation)) {
593
- relativeXpath = checkReferenceElementIsValid(
594
- locator.value,
595
- relation,
596
- tempDiv
597
- );
598
-
599
- if (relativeXpath) {
600
- finalLocators.push({
601
- name: "xpath",
602
- type: locator.type,
603
- value: relativeXpath,
604
- reference: locator.reference,
605
- status: locator.status,
606
- isRecorded:
607
- locator.isRecorded !== "" &&
608
- locator.isRecorded !== null
609
- ? locator.isRecorded
610
- : "Y",
611
- });
612
- break;
613
- }
614
- }
615
- }
616
- }
617
- } catch (error: any) {
618
- logger.error(`[${Object.getPrototypeOf(this).constructor.name}] : Error processing locator:`, locator, error.message);
619
- }
620
- }
621
-
622
- const uniqueLocators = new Map();
623
- finalLocators.forEach((_loc: any) => {
624
- if (!uniqueLocators.has(_loc.value)) {
625
- uniqueLocators.set(_loc.value, _loc);
626
- }
627
- });
628
-
629
- const ffLoc = Array.from(uniqueLocators.values());
630
-
631
- const jsonResult = [
632
- {
633
- locators: ffLoc.filter(
634
- (locator) =>
635
- Object.prototype.hasOwnProperty.call(locator, "value") &&
636
- locator?.value !== null &&
637
- locator.value !== ""
638
- ),
639
- },
640
- ];
641
-
642
- return jsonResult;
643
- }
644
- } catch (error: any) {
645
- logger.error(`[${Object.getPrototypeOf(this).constructor.name}] : Error processing locator:`, locator, error.message);
646
- continue;
647
- }
648
- } catch (error: any) {
649
- logger.error(`[${Object.getPrototypeOf(this).constructor.name}] : Error processing locator:`, locator, error.message);
650
- continue;
651
- }
652
- }
653
- return null;
654
- };
655
-
656
- export { getElementsFromHTML };
1
+ import { JSDOM, VirtualConsole } from "jsdom";
2
+ import { logger } from "../../utils/logger/logData.js"
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(
47
+ xpath,
48
+ element.ownerDocument,
49
+ null,
50
+ window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
51
+ null
52
+ );
53
+
54
+ return isUnique(xpathResult) ? xpath : null;
55
+ };
56
+
57
+ const getId = (element: Element | null): string | null => {
58
+ return element?.id || null;
59
+ };
60
+
61
+ const getClassName = (element: Element): string | null => {
62
+ return (element as HTMLElement).className || null;
63
+ };
64
+
65
+ const getVisibleText = (element: Element): string | null => {
66
+ return element.textContent?.trim() || null;
67
+ };
68
+
69
+ const getXPathByText = (element: Element): string | null => {
70
+ const text = getVisibleText(element);
71
+ if (text) {
72
+ const xpath = `//${element.nodeName.toLowerCase()}[text()='${text}']`;
73
+ return getXPath(element, xpath);
74
+ }
75
+ return null;
76
+ };
77
+
78
+
79
+
80
+
81
+
82
+ const getXPathById = (element: Element): string | null => {
83
+ if (element.id) {
84
+ const xpath = `//${element.nodeName.toLowerCase()}[@id='${element.id}']`;
85
+ return getXPath(element, xpath);
86
+ }
87
+ return null;
88
+ };
89
+
90
+ const getXPathByClass = (element: Element): string | null => {
91
+ const classNames = (element as HTMLElement).classList;
92
+ if (classNames.length > 0) {
93
+ const xpath = `//${element.nodeName.toLowerCase()}[@class='${classNames[0]
94
+ }']`;
95
+ return getXPath(element, xpath);
96
+ }
97
+ return null;
98
+ };
99
+
100
+ const getXpathByName = (element: Element): string | null => {
101
+ const selector = element.nodeName.toLowerCase();
102
+ const elementEl = element as HTMLElement;
103
+
104
+ if (elementEl.hasAttribute("name")) {
105
+ const attrValue = elementEl.getAttribute("name");
106
+ const xpath = `//${selector}[@name='${attrValue}']`;
107
+ return getXPath(element, xpath) || null;
108
+ }
109
+
110
+ return null;
111
+ };
112
+
113
+ const getName = (element: Element): string | null => {
114
+ const elementEl = element as HTMLElement;
115
+
116
+ if (elementEl.hasAttribute("name")) {
117
+ const attrValue = elementEl.getAttribute("name");
118
+ const name = `${attrValue}`;
119
+ return name || null;
120
+ }
121
+
122
+ return null;
123
+ };
124
+
125
+ const getFFxpath = (element: Element): string | null => {
126
+ const elementEl = element as HTMLElement;
127
+
128
+ if (elementEl.hasAttribute("ff-xpath") && !elementEl.getAttribute("ff-xpath")?.includes("/iframe")) {
129
+ const attrValue = elementEl.getAttribute("ff-xpath");
130
+ const name = `${attrValue}`;
131
+ return name || null;
132
+ }
133
+
134
+ return null;
135
+ };
136
+
137
+ const getXpathByPlaceholder = (element: Element): string | null => {
138
+ const selector = element.nodeName.toLowerCase();
139
+ const elementEl = element as HTMLElement;
140
+
141
+ if (elementEl.hasAttribute("placeholder")) {
142
+ const attrValue = elementEl.getAttribute("placeholder");
143
+ const xpath = `//${selector}[@placeholder='${attrValue}']`;
144
+ return getXPath(element, xpath) || null;
145
+ }
146
+
147
+ return null;
148
+ };
149
+
150
+
151
+
152
+ const getXpathByType = (element: Element): string | null => {
153
+ const selector = element.nodeName.toLowerCase();
154
+ const elementEl = element as HTMLElement;
155
+
156
+ if (elementEl.hasAttribute("type")) {
157
+ const attrValue = elementEl.getAttribute("type");
158
+ const xpath = `//${selector}[@type='${attrValue}']`;
159
+ return getXPath(element, xpath) || null;
160
+ }
161
+
162
+ return null;
163
+ };
164
+
165
+ const getXPathAbsolute = (element: Element): string => {
166
+ const path: string[] = [];
167
+ while (element && element.nodeType === 1) {
168
+ // ELEMENT_NODE
169
+ let selector = element.nodeName.toLowerCase();
170
+ let sibling = element;
171
+ let siblingCount = 1;
172
+
173
+ while ((sibling = sibling.previousElementSibling as Element)) {
174
+ if (sibling.nodeName.toLowerCase() === element.nodeName.toLowerCase()) {
175
+ siblingCount++;
176
+ }
177
+ }
178
+
179
+ if (siblingCount > 1) {
180
+ selector += `[${siblingCount}]`;
181
+ }
182
+ path.unshift(selector);
183
+ element = element.parentNode as Element;
184
+ }
185
+ return "//" + path.join("/");
186
+ };
187
+
188
+ const relations: string[] = [
189
+ "/preceding-sibling",
190
+ "/following-sibling",
191
+ "/parent",
192
+ "/descendant",
193
+ "/ancestor",
194
+ "/self",
195
+ "/ancestor-or-self",
196
+ "/child",
197
+ "/preceding",
198
+ "/following",
199
+ ];
200
+
201
+ function getElementFromXPath(
202
+ tempDiv: HTMLElement,
203
+ xpath: string
204
+ ): Element | null {
205
+ const currentElement: Element | null = tempDiv;
206
+ const window = currentElement.ownerDocument.defaultView;
207
+ if (!window) return null;
208
+
209
+ const xpathEvaluator = new window.XPathEvaluator();
210
+ const xpathResult = xpathEvaluator.evaluate(
211
+ xpath,
212
+ currentElement.ownerDocument,
213
+ null,
214
+ window.XPathResult.FIRST_ORDERED_NODE_TYPE,
215
+ null
216
+ );
217
+ return xpathResult.singleNodeValue as Element | null;
218
+ }
219
+
220
+ function checkReferenceElementIsValid(
221
+ locator: string,
222
+ relation: string,
223
+ tempDiv: HTMLElement
224
+ ): string | null {
225
+ if (locator.includes(relation)) {
226
+ const locatotSplitArray: string[] = locator.split(relation);
227
+ const sourceLoc = locatotSplitArray[0].trim();
228
+ const currentElement: Element | null = tempDiv;
229
+ const window = currentElement.ownerDocument.defaultView;
230
+ if (!window) return null;
231
+ if (!locator.includes("dynamic")) {
232
+ const xpathEvaluator = new window.XPathEvaluator();
233
+ const xpathResult = xpathEvaluator.evaluate(
234
+ sourceLoc,
235
+ currentElement.ownerDocument,
236
+ null,
237
+ window.XPathResult.FIRST_ORDERED_NODE_TYPE,
238
+ null
239
+ );
240
+
241
+ const sourceElement = xpathResult.singleNodeValue;
242
+ if (sourceElement) {
243
+ const xpathResultComplete = xpathEvaluator.evaluate(
244
+ locator,
245
+ currentElement.ownerDocument,
246
+ null,
247
+ window.XPathResult.FIRST_ORDERED_NODE_TYPE,
248
+ null
249
+ );
250
+ const completeElement = xpathResultComplete.singleNodeValue;
251
+ let relativeXpath: string;
252
+ if (completeElement) {
253
+ relativeXpath = locator;
254
+ return relativeXpath;
255
+ } else {
256
+ logger.error("Complete Locator is Invalid:", locator);
257
+ relativeXpath = locator;
258
+ return relativeXpath;
259
+ }
260
+ } else {
261
+ logger.error("Source Locator Not Found:", sourceLoc);
262
+ }
263
+ }
264
+ }
265
+ return null;
266
+ }
267
+ interface Locator {
268
+ name: string;
269
+ type: string;
270
+ value: string;
271
+ reference: string;
272
+ status: string;
273
+ isRecorded: string;
274
+ }
275
+
276
+ const getElementsFromHTML = (
277
+ locators: Locator[],
278
+ htmlString: string
279
+ ): ElementDetails | null => {
280
+ const virtualConsole = new VirtualConsole();
281
+ const dom = new JSDOM(htmlString, {
282
+ resources: "usable",
283
+ runScripts: "outside-only", // Prevents inline script execution in JSDOM
284
+ pretendToBeVisual: true,
285
+ virtualConsole,
286
+ includeNodeLocations: true,
287
+ });
288
+
289
+ const document = dom.window.document;
290
+ global.SVGElement = dom.window.SVGElement;
291
+ const tempDiv = document.createElement("div");
292
+ const elementsToRemove = document.querySelectorAll(
293
+ "script, style, link[rel='stylesheet'], meta, noscript, embed, object, param, source, svg"
294
+ );
295
+
296
+ if (elementsToRemove) {
297
+ elementsToRemove.forEach((tag) => {
298
+ (tag as Element).remove();
299
+ });
300
+ }
301
+
302
+ tempDiv.innerHTML = document.body.innerHTML;
303
+ // tempDiv.innerHTML = htmlString;
304
+ const finalLocators: any = [];
305
+
306
+ for (const locator of locators) {
307
+ try {
308
+ const isDynamic = String(locator.value || locator.type || "");
309
+ if (
310
+ isDynamic.includes("dynamic") ||
311
+ isDynamic.match("dynamic") ||
312
+ isDynamic.includes("{") ||
313
+ isDynamic.includes("}")
314
+ ) {
315
+ finalLocators.push({
316
+ name: locator.name,
317
+ type: locator.type,
318
+ value: locator.value,
319
+ reference: locator.reference,
320
+ status: locator.status,
321
+ isRecorded: locator.isRecorded,
322
+ });
323
+ continue;
324
+ }
325
+ const isRecorded = String(locator.isRecorded || "");
326
+
327
+ if (isRecorded.includes("N") || isRecorded.match("N")) {
328
+ finalLocators.push({
329
+ name: locator.name,
330
+ type: locator.type,
331
+ value: locator.value,
332
+ reference: locator.reference,
333
+ status: locator.status,
334
+ isRecorded: locator.isRecorded,
335
+ });
336
+ }
337
+ try {
338
+ let targetElement: Element | null = null;
339
+ if (locator.value.startsWith("iframe")) {
340
+ const iframe = tempDiv.querySelector(
341
+ locator.value
342
+ ) as HTMLIFrameElement;
343
+ if (iframe) {
344
+ const iframeDocument =
345
+ iframe.contentDocument || iframe.contentWindow?.document;
346
+ if (iframeDocument) {
347
+ targetElement = iframeDocument.querySelector(
348
+ locator.value.slice(6)
349
+ );
350
+ }
351
+ }
352
+ } else {
353
+ const selectors = locator.value.split(">>>"); // Custom delimiter for shadow DOM
354
+ let currentElement: Element | null = tempDiv;
355
+
356
+ for (const selector of selectors) {
357
+ if (currentElement) {
358
+ const trimmedSelector = selector.trim();
359
+ if (
360
+ locator.name.includes("id") ||
361
+ trimmedSelector.startsWith("#")
362
+ ) {
363
+ targetElement = currentElement.querySelector(
364
+ "#" + trimmedSelector
365
+ );
366
+ } else if (
367
+ locator.name.includes("className") ||
368
+ trimmedSelector.startsWith(".")
369
+ ) {
370
+ targetElement = currentElement.querySelector(
371
+ "." + trimmedSelector
372
+ );
373
+ } else if (
374
+ (locator.name.includes("xpath") ||
375
+ trimmedSelector.startsWith("//")) &&
376
+ !locator.type.match("dynamic")
377
+ ) {
378
+ targetElement = getElementFromXPath(tempDiv, trimmedSelector);
379
+ } else {
380
+ targetElement = currentElement.querySelector(trimmedSelector);
381
+ if (!targetElement) {
382
+ targetElement = getElementFromShadowRoot(
383
+ currentElement,
384
+ trimmedSelector
385
+ );
386
+ }
387
+ }
388
+
389
+ if (
390
+ !targetElement &&
391
+ isSVGElement(currentElement) &&
392
+ !locator.type.match("dynamic")
393
+ ) {
394
+ targetElement = currentElement.querySelector(trimmedSelector);
395
+ }
396
+
397
+ currentElement = targetElement;
398
+ }
399
+ }
400
+ }
401
+
402
+ const locatorExists = (name: string, value: string) => {
403
+ return finalLocators.some(
404
+ (_loc: any) =>
405
+ _loc.name === name &&
406
+ _loc.value === value &&
407
+ (!_loc.value.includes("dynamic") ||
408
+ !locator.type.match("dynamic"))
409
+ );
410
+ };
411
+
412
+ const xpathFunctions = [
413
+ { name: "xpath", value: getXPathByText },
414
+ { name: "xpath", value: getXPathById },
415
+ { name: "xpath", value: getXPathByClass },
416
+ { name: "xpath", value: getXPathAbsolute },
417
+ { name: "xpath", value: getXpathByName },
418
+ { name: "xpath", value: getXpathByPlaceholder },
419
+ { name: "xpath", value: getXpathByType },
420
+ ];
421
+
422
+ if (targetElement) {
423
+ const idValue = getId(targetElement);
424
+ if (idValue && !locatorExists("id", idValue)) {
425
+ locators.forEach(() => {
426
+ if (locator.value === idValue) {
427
+ finalLocators.push({
428
+ name: "id",
429
+ type: locator.type,
430
+ value: idValue,
431
+ reference: locator.reference,
432
+ status: locator.status,
433
+ isRecorded: locator.isRecorded,
434
+ isAiRecorded: "Y",
435
+ });
436
+ } else {
437
+ finalLocators.push({
438
+ name: "id",
439
+ type: locator.type,
440
+ value: idValue,
441
+ reference: locator.reference,
442
+ status: locator.status,
443
+ isRecorded: "Y",
444
+ isAiRecorded: "Y",
445
+ });
446
+ }
447
+ });
448
+ }
449
+
450
+
451
+ if (getVisibleText(targetElement)) {
452
+ const textValue = getVisibleText(targetElement);
453
+ locators.forEach((_loc) => {
454
+ if (_loc.value === textValue) {
455
+ finalLocators.push({
456
+ name: "linkText",
457
+ type: "static",
458
+ value: textValue,
459
+ reference: locator.reference,
460
+ status: locator.status,
461
+ isRecorded: locator.isRecorded,
462
+ isAiRecorded: "Y",
463
+ });
464
+ } else {
465
+ finalLocators.push({
466
+ name: "linkText",
467
+ type: "static",
468
+ value: textValue,
469
+ reference: locator.reference,
470
+ status: locator.status,
471
+ isRecorded: "Y",
472
+ isAiRecorded: "Y",
473
+ });
474
+ }
475
+ });
476
+ }
477
+
478
+ if (getName(targetElement)) {
479
+ const nameLocator = getName(targetElement);
480
+ locators.forEach((_loc) => {
481
+ if (_loc.value === nameLocator) {
482
+ finalLocators.push({
483
+ name: "name",
484
+ type: "static",
485
+ value: nameLocator,
486
+ reference: locator.reference,
487
+ status: locator.status,
488
+ isRecorded: locator.isRecorded,
489
+ isAiRecorded: "Y",
490
+ });
491
+ } else {
492
+ finalLocators.push({
493
+ name: "name",
494
+ type: "static",
495
+ value: nameLocator,
496
+ reference: locator.reference,
497
+ status: locator.status,
498
+ isRecorded: "Y",
499
+ isAiRecorded: "Y",
500
+ });
501
+ }
502
+ });
503
+ }
504
+ if (getFFxpath(targetElement)) {
505
+ const nameLocator = getFFxpath(targetElement);
506
+ locators.forEach((_loc) => {
507
+ if (_loc.value === nameLocator) {
508
+ finalLocators.push({
509
+ name: "xpath",
510
+ type: "static",
511
+ value: nameLocator,
512
+ reference: locator.reference,
513
+ status: locator.status,
514
+ isRecorded: locator.isRecorded,
515
+ isAiRecorded: "Y",
516
+ });
517
+ } else {
518
+ finalLocators.push({
519
+ name: "xpath",
520
+ type: "static",
521
+ value: nameLocator,
522
+ reference: locator.reference,
523
+ status: locator.status,
524
+ isRecorded: "Y",
525
+ isAiRecorded: "Y",
526
+ });
527
+ }
528
+ });
529
+ }
530
+
531
+ const classValue = getClassName(targetElement);
532
+ if (
533
+ classValue &&
534
+ classValue.trim() !== "" &&
535
+ !locatorExists("className", classValue)
536
+ ) {
537
+ locators.forEach((_loc) => {
538
+ if (_loc.value === classValue) {
539
+ finalLocators.push({
540
+ name: "className",
541
+ type: locator.type,
542
+ value: classValue,
543
+ reference: locator.reference,
544
+ status: locator.status,
545
+ isRecorded: locator.isRecorded,
546
+ isAiRecorded: "Y",
547
+ });
548
+ } else {
549
+ finalLocators.push({
550
+ name: "className",
551
+ type: locator.type,
552
+ value: classValue,
553
+ reference: locator.reference,
554
+ status: locator.status,
555
+ isRecorded: "Y",
556
+ isAiRecorded: "Y",
557
+ });
558
+ }
559
+ });
560
+ }
561
+ xpathFunctions.forEach((fn) => {
562
+ locators.forEach((_loc) => {
563
+ if (_loc.value === fn.value(targetElement)) {
564
+ finalLocators.push({
565
+ name: fn.name,
566
+ type: locator.type,
567
+ value: fn.value(targetElement),
568
+ reference: locator.reference,
569
+ status: locator.status,
570
+ isRecorded: locator.isRecorded,
571
+ isAiRecorded: "Y",
572
+ });
573
+ } else {
574
+ finalLocators.push({
575
+ name: fn.name,
576
+ type: locator.type,
577
+ value: fn.value(targetElement),
578
+ reference: locator.reference,
579
+ status: locator.status,
580
+ isRecorded: "Y",
581
+ isAiRecorded: "Y",
582
+ });
583
+ }
584
+ });
585
+ });
586
+
587
+ for (const locator of locators) {
588
+ try {
589
+ let relativeXpath = null;
590
+ if (locator.value) {
591
+ for (const relation of relations) {
592
+ if (locator.value.includes(relation)) {
593
+ relativeXpath = checkReferenceElementIsValid(
594
+ locator.value,
595
+ relation,
596
+ tempDiv
597
+ );
598
+
599
+ if (relativeXpath) {
600
+ finalLocators.push({
601
+ name: "xpath",
602
+ type: locator.type,
603
+ value: relativeXpath,
604
+ reference: locator.reference,
605
+ status: locator.status,
606
+ isRecorded:
607
+ locator.isRecorded !== "" &&
608
+ locator.isRecorded !== null
609
+ ? locator.isRecorded
610
+ : "Y",
611
+ });
612
+ break;
613
+ }
614
+ }
615
+ }
616
+ }
617
+ } catch (error: any) {
618
+ logger.error(`[${Object.getPrototypeOf(this).constructor.name}] : Error processing locator:`, locator, error.message);
619
+ }
620
+ }
621
+
622
+ const uniqueLocators = new Map();
623
+ finalLocators.forEach((_loc: any) => {
624
+ if (!uniqueLocators.has(_loc.value)) {
625
+ uniqueLocators.set(_loc.value, _loc);
626
+ }
627
+ });
628
+
629
+ const ffLoc = Array.from(uniqueLocators.values());
630
+
631
+ const jsonResult = [
632
+ {
633
+ locators: ffLoc.filter(
634
+ (locator) =>
635
+ Object.prototype.hasOwnProperty.call(locator, "value") &&
636
+ locator?.value !== null &&
637
+ locator.value !== ""
638
+ ),
639
+ },
640
+ ];
641
+
642
+ return jsonResult;
643
+ }
644
+ } catch (error: any) {
645
+ logger.error(`[${Object.getPrototypeOf(this).constructor.name}] : Error processing locator:`, locator, error.message);
646
+ continue;
647
+ }
648
+ } catch (error: any) {
649
+ logger.error(`[${Object.getPrototypeOf(this).constructor.name}] : Error processing locator:`, locator, error.message);
650
+ continue;
651
+ }
652
+ }
653
+ return null;
654
+ };
655
+
656
+ export { getElementsFromHTML };