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.
@@ -0,0 +1,1299 @@
1
+ export const reWhiteSpace = /^[\S]+( [\S]+)*$/gi;
2
+ let cspEnabled: boolean = false;
3
+ const xpathCache: { [x: string]: number } = {};
4
+ let multiElementReferenceMode: boolean = false;
5
+ let relativeXPathCache = new Map();
6
+ export let modifiedElementAttributes: {
7
+ url: string | null;
8
+ attributeName: string | null;
9
+ element: HTMLElement | Element;
10
+ doc: Document;
11
+ }[] = [];
12
+ let mutationObserver: MutationObserver;
13
+
14
+ export const createObserver = (addedNodeCallBack: Function) => {
15
+ mutationObserver = new MutationObserver((mutations) => {
16
+ mutations.forEach((mutation) => {
17
+ if (mutation?.addedNodes?.length) {
18
+ addedNodeCallBack(mutation?.addedNodes);
19
+ }
20
+
21
+ if (mutation.target instanceof HTMLElement) {
22
+ if (
23
+ mutation?.type === "attributes" &&
24
+ mutation.attributeName === "class" &&
25
+ ((isSvg(mutation.target) &&
26
+ mutation.oldValue?.trim() ===
27
+ mutation.target.classList.value?.trim()) ||
28
+ mutation.target.classList?.value
29
+ ?.replace(mutation.oldValue!, "")
30
+ .trim() === "marked-element-temp" ||
31
+ mutation?.oldValue
32
+ ?.replace(mutation.target.classList?.value, "")
33
+ .trim() === "marked-element-temp")
34
+ ) {
35
+ } else if (
36
+ mutation?.type === "attributes" &&
37
+ !["flndisabled"].includes(mutation.attributeName!)
38
+ ) {
39
+ modifiedElementAttributes.push({
40
+ url: mutation.target.baseURI,
41
+ attributeName: mutation.attributeName,
42
+ element: mutation.target,
43
+ doc: mutation.target.ownerDocument,
44
+ });
45
+ }
46
+ }
47
+ });
48
+ });
49
+ };
50
+ export const startObserver = (
51
+ target: Node,
52
+ options: MutationObserverInit | undefined
53
+ ) => {
54
+ mutationObserver?.observe(target, options);
55
+ };
56
+
57
+ export const stopObserver = () => {
58
+ mutationObserver?.disconnect();
59
+ };
60
+
61
+ export const isNumberExist = (str: string): boolean => {
62
+ const hasNumber = /\d/;
63
+ return hasNumber.test(str);
64
+ };
65
+
66
+ export const buildPattern = (
67
+ pattern: string,
68
+ isSvg: boolean,
69
+ tagName: string
70
+ ) => {
71
+ return isSvg
72
+ ? `//*[local-name()='${tagName}' and ${pattern}]`
73
+ : `//${tagName}[${pattern}]`;
74
+ };
75
+
76
+ export const getTextContent = (
77
+ targetElement: HTMLElement | Element
78
+ ): string => {
79
+ const textContent = targetElement?.textContent;
80
+ if (cspEnabled) {
81
+ if (textContent) {
82
+ const tooltip = document.querySelector(".flntooltip") as HTMLElement;
83
+ if (tooltip) {
84
+ const lastIndex = textContent.lastIndexOf(tooltip.innerText);
85
+ if (
86
+ lastIndex &&
87
+ textContent.length === lastIndex + tooltip.innerText.length
88
+ ) {
89
+ return textContent.substring(0, lastIndex);
90
+ }
91
+ } else {
92
+ return textContent;
93
+ }
94
+ }
95
+ } else {
96
+ return textContent;
97
+ }
98
+
99
+ return "";
100
+ };
101
+
102
+ export const getFilteredText = (element: Node) => {
103
+ return element?.childNodes[0]?.nodeValue || "";
104
+ };
105
+
106
+ export const getCountOfXPath = (
107
+ xpath: string,
108
+ element: HTMLElement | Element,
109
+ docmt: Node
110
+ ) => {
111
+ try {
112
+ let count;
113
+ // Check if result is cached
114
+ if (xpathCache[xpath] !== undefined) {
115
+ count = xpathCache[xpath];
116
+ } else {
117
+ const owner =
118
+ docmt.nodeType === 9 // DOCUMENT_NODE
119
+ ? (docmt as Document)
120
+ : docmt.ownerDocument!;
121
+
122
+ count = owner.evaluate(
123
+ `count(${xpath})`,
124
+ docmt,
125
+ null,
126
+ XPathResult.NUMBER_TYPE,
127
+ null
128
+ ).numberValue;
129
+ xpathCache[xpath] = count;
130
+ }
131
+
132
+ if (multiElementReferenceMode && Array.isArray(element)) {
133
+ if (count > 1) {
134
+ const elementsFromXpath = getElementFromXpath(xpath, docmt, true);
135
+ let nodex,
136
+ matchedCount = 0;
137
+ if (elementsFromXpath instanceof XPathResult) {
138
+ while ((nodex = elementsFromXpath?.iterateNext())) {
139
+ if (element.includes(nodex)) {
140
+ matchedCount += 1;
141
+
142
+ if (matchedCount === 2) break;
143
+ }
144
+ }
145
+ }
146
+
147
+ if (matchedCount !== 2) return 0;
148
+ }
149
+ } else {
150
+ // Short-circuit if we only need to match one element
151
+ if (count === 1) {
152
+ const elementFromXpath = getElementFromXpath(xpath, docmt);
153
+ return elementFromXpath === element ? count : 0; // Return 0 if no match
154
+ }
155
+ }
156
+ return count; // Return the count
157
+ } catch (error) {
158
+ console.error(`Error evaluating XPath: ${xpath}`, error);
159
+ return 0; // Return 0 on error to avoid re-processing
160
+ }
161
+ };
162
+
163
+ export const escapeCharacters = (text: string) => {
164
+ if (text) {
165
+ if (!(text.indexOf('"') === -1)) {
166
+ return `'${text}'`;
167
+ }
168
+ if (!(text.indexOf("'") === -1)) {
169
+ return `"${text}"`;
170
+ }
171
+ }
172
+ return `'${text}'`;
173
+ };
174
+
175
+ export const removeParenthesis = (xpath: string) => {
176
+ const charArr = xpath.split("");
177
+
178
+ let count = charArr.length;
179
+ const indexArray = [];
180
+
181
+ while (charArr[count - 2] !== "[") {
182
+ indexArray.push(charArr[count - 2]);
183
+ count--;
184
+ }
185
+
186
+ indexArray.reverse();
187
+ let finalStr = "";
188
+ for (let i = 0; i < indexArray.length; i++) {
189
+ finalStr += indexArray[i];
190
+ }
191
+
192
+ const endBracketLength = finalStr.length + 2;
193
+ let firstpart = xpath.slice(0, xpath.length - endBracketLength);
194
+
195
+ if (firstpart.startsWith("(") && firstpart.endsWith(")")) {
196
+ firstpart = firstpart.slice(1, -1);
197
+ } else {
198
+ firstpart = xpath;
199
+ }
200
+
201
+ return firstpart;
202
+ };
203
+
204
+ export const findXpathWithIndex = (
205
+ val: string,
206
+ node: HTMLElement | Element,
207
+ docmt: Node,
208
+ count: number
209
+ ) => {
210
+ try {
211
+ const owner =
212
+ docmt.nodeType === 9 // DOCUMENT_NODE
213
+ ? (docmt as Document)
214
+ : docmt.ownerDocument!;
215
+
216
+ let index = 0;
217
+ if (count) {
218
+ if (getCountOfXPath(`${val}[${count}]`, node, docmt) === 1) {
219
+ return `${val}[${count}]`;
220
+ }
221
+
222
+ if (getCountOfXPath(`(${val})[${count}]`, node, docmt) === 1) {
223
+ return `(${val})[${count}]`;
224
+ }
225
+ }
226
+
227
+ const nodes = owner.evaluate(val, docmt, null, XPathResult.ANY_TYPE, null);
228
+ let nodex;
229
+ while ((nodex = nodes.iterateNext())) {
230
+ index++;
231
+ if (nodex.isSameNode(node)) {
232
+ if (getCountOfXPath(`${val}[${index}]`, node, docmt) === 1) {
233
+ return `${val}[${index}]`;
234
+ }
235
+ if (getCountOfXPath(`(${val})[${index}]`, node, docmt) === 1) {
236
+ return `(${val})[${index}]`;
237
+ }
238
+ return;
239
+ }
240
+ }
241
+ } catch (error) {
242
+ console.log(error);
243
+ }
244
+ };
245
+
246
+ const deleteLineGap = (a: string) => {
247
+ a &&= a.split("\n")[0].length > 0 ? a.split("\n")[0] : a.split("\n")[1];
248
+ return a;
249
+ };
250
+
251
+ const deleteGarbageFromInnerText = (a: string) => {
252
+ a = deleteLineGap(a);
253
+ a = a
254
+ .split(/[^\u0000-\u00ff]/)
255
+ .reduce((b, c) => {
256
+ return b.length > c.length ? b : c;
257
+ }, "")
258
+ .trim();
259
+ return (a = a.split("/")[0].trim());
260
+ };
261
+
262
+ export const replaceWhiteSpaces = (str: string) => {
263
+ if (str) {
264
+ return str.replace(/\s\s+/g, " ").trim();
265
+ }
266
+
267
+ return str;
268
+ };
269
+
270
+ export const getShadowRoot = (a: HTMLElement | Element) => {
271
+ for (a = a && a.parentElement!; a; ) {
272
+ if (a.toString() === "[object ShadowRoot]") return a;
273
+ a = a.parentElement!;
274
+ }
275
+ return null;
276
+ };
277
+
278
+ export const checkBlockedAttributes = (
279
+ attribute: {
280
+ name: string;
281
+ value: string;
282
+ },
283
+ targetElement: HTMLElement | Element,
284
+ isTarget: boolean
285
+ ) => {
286
+ if (
287
+ !attribute?.value ||
288
+ typeof attribute?.value === "boolean" ||
289
+ ["true", "false", "on", "off", "flntooltip"].some(
290
+ (x) => x === attribute.value
291
+ ) ||
292
+ ["type", "style", "locator-data-tooltip", "value"].some(
293
+ (x) => x === attribute.name
294
+ ) ||
295
+ modifiedElementAttributes?.find(
296
+ (x) =>
297
+ x.doc === targetElement.ownerDocument &&
298
+ x.element === targetElement &&
299
+ x.attributeName === attribute.name
300
+ ) ||
301
+ (attribute?.name?.indexOf("on") === 0 && attribute?.name?.length > 3) ||
302
+ typeof attribute.value === "function" ||
303
+ (isTarget && isNumberExist(attribute.value))
304
+ ) {
305
+ return false;
306
+ }
307
+
308
+ return true;
309
+ };
310
+
311
+ export const getRelationship = (a: HTMLElement, b: HTMLElement) => {
312
+ let pos = a.compareDocumentPosition(b);
313
+ return pos === 2
314
+ ? "preceding"
315
+ : pos === 4
316
+ ? "following"
317
+ : pos === 8
318
+ ? "ancestor"
319
+ : pos === 16
320
+ ? "descendant"
321
+ : pos === 32
322
+ ? "self"
323
+ : "";
324
+ };
325
+
326
+ export const findRoot = (node: Node) => {
327
+ while (node && node.parentNode) {
328
+ node = node.parentNode;
329
+ }
330
+
331
+ return node;
332
+ };
333
+
334
+ export const isSvg = (element: HTMLElement | Element) => {
335
+ return element instanceof SVGElement;
336
+ };
337
+
338
+ export const replaceTempAttributes = (str: string): string => {
339
+ if (!str) return str;
340
+
341
+ return str.replace(/\b[a-zA-Z_]*disabled\b/gi, "disabled");
342
+ };
343
+
344
+ const getElementFromXpath = (xpath: string, docmt: Node, multi = false) => {
345
+ const owner =
346
+ docmt.nodeType === 9 // DOCUMENT_NODE
347
+ ? (docmt as Document)
348
+ : docmt.ownerDocument!;
349
+
350
+ if (multi) {
351
+ return owner.evaluate(xpath, docmt, null, XPathResult.ANY_TYPE, null);
352
+ } else {
353
+ return owner.evaluate(
354
+ xpath,
355
+ docmt,
356
+ null,
357
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
358
+ null
359
+ ).singleNodeValue;
360
+ }
361
+ };
362
+
363
+ export const getPropertyXPath = (
364
+ element: HTMLElement | Element,
365
+ docmt: Document,
366
+ prop: string,
367
+ value: string,
368
+ isIndex: boolean,
369
+ isTarget: boolean
370
+ ) => {
371
+ if (value) {
372
+ const { tagName } = element;
373
+ let count;
374
+ let combinePattern = "";
375
+ const mergePattern = [];
376
+ let pattern;
377
+
378
+ if (value && (!isTarget || !isNumberExist(value))) {
379
+ if (!reWhiteSpace.test(value)) {
380
+ pattern = buildPattern(
381
+ `normalize-space(${prop})=${escapeCharacters(
382
+ replaceWhiteSpaces(value)
383
+ ).trim()}`,
384
+ isSvg(element),
385
+ element.tagName.toLowerCase()
386
+ );
387
+ } else {
388
+ pattern = `//${tagName}[${prop}=${escapeCharacters(value)}]`;
389
+ }
390
+
391
+ count = getCountOfXPath(pattern, element, docmt);
392
+
393
+ if (count === 1 && !isIndex) {
394
+ return pattern;
395
+ }
396
+ }
397
+
398
+ if (value && isTarget) {
399
+ const splitText = value.split(" ");
400
+ if (splitText?.length) {
401
+ if (splitText.length === 1) {
402
+ const contentRes = [...new Set(splitText[0].match(/([^0-9]+)/g))];
403
+ if (contentRes?.length >= 1) {
404
+ if (
405
+ contentRes[0] &&
406
+ replaceWhiteSpaces(contentRes[0].trim())?.length > 1
407
+ ) {
408
+ if (value.startsWith(contentRes[0])) {
409
+ if (!reWhiteSpace.test(contentRes[0])) {
410
+ combinePattern = `starts-with(${prop},${escapeCharacters(
411
+ replaceWhiteSpaces(contentRes[0])
412
+ ).trim()})`;
413
+ } else {
414
+ combinePattern = `starts-with(${prop},${escapeCharacters(
415
+ contentRes[0]
416
+ ).trim()})`;
417
+ }
418
+ }
419
+ }
420
+ }
421
+
422
+ if (contentRes?.length > 1) {
423
+ if (
424
+ contentRes[contentRes.length - 1] &&
425
+ replaceWhiteSpaces(contentRes[contentRes.length - 1].trim())
426
+ ?.length > 1
427
+ ) {
428
+ if (value.endsWith(contentRes[contentRes.length - 1])) {
429
+ if (!reWhiteSpace.test(contentRes[contentRes.length - 1])) {
430
+ combinePattern = `ends-with(${prop},${escapeCharacters(
431
+ replaceWhiteSpaces(contentRes[contentRes.length - 1])
432
+ ).trim()})`;
433
+ } else {
434
+ combinePattern = `ends-with(${prop},${escapeCharacters(
435
+ contentRes[contentRes.length - 1]
436
+ ).trim()})`;
437
+ }
438
+ }
439
+ }
440
+ }
441
+
442
+ if (combinePattern?.length) {
443
+ if (isSvg(element)) {
444
+ pattern = `//*[local-name()='${tagName}' and ${combinePattern}]`;
445
+ } else {
446
+ pattern = `//${tagName}[${combinePattern}]`;
447
+ }
448
+ count = getCountOfXPath(pattern, element, docmt);
449
+ if (count === 1 && !isIndex) {
450
+ return pattern;
451
+ }
452
+ }
453
+ } else {
454
+ const endIndex =
455
+ splitText.length % 2 === 0
456
+ ? splitText.length / 2
457
+ : splitText.length % 2;
458
+ const startIndexString = splitText.slice(0, endIndex).join(" ");
459
+ let contentRes = [...new Set(startIndexString.match(/([^0-9]+)/g))];
460
+ if (contentRes?.length) {
461
+ if (
462
+ contentRes[0] &&
463
+ replaceWhiteSpaces(contentRes[0].trim())?.length
464
+ ) {
465
+ if (value.startsWith(contentRes[0])) {
466
+ if (!reWhiteSpace.test(contentRes[0])) {
467
+ combinePattern = `starts-with(${prop},${escapeCharacters(
468
+ replaceWhiteSpaces(contentRes[0])
469
+ ).trim()})`;
470
+ } else {
471
+ combinePattern = `starts-with(${prop},${escapeCharacters(
472
+ contentRes[0]
473
+ ).trim()})`;
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ if (combinePattern?.length) {
480
+ if (isSvg(element)) {
481
+ pattern = `//*[local-name()='${tagName}' and ${combinePattern}]`;
482
+ } else {
483
+ pattern = `//${tagName}[${combinePattern}]`;
484
+ }
485
+ count = getCountOfXPath(pattern, element, docmt);
486
+ if (count === 1 && !isIndex) {
487
+ return pattern;
488
+ }
489
+ }
490
+
491
+ const endIndexString = splitText
492
+ .slice(endIndex, splitText.length - 1)
493
+ .join(" ");
494
+ contentRes = [...new Set(endIndexString.match(/([^0-9]+)/g))];
495
+ if (contentRes?.length) {
496
+ if (
497
+ contentRes[0] &&
498
+ replaceWhiteSpaces(contentRes[0].trim())?.length > 3
499
+ ) {
500
+ if (value.endsWith(contentRes[0])) {
501
+ if (!reWhiteSpace.test(contentRes[0])) {
502
+ combinePattern = `ends-with(${prop},${escapeCharacters(
503
+ replaceWhiteSpaces(contentRes[0])
504
+ ).trim()})`;
505
+ } else {
506
+ combinePattern = `ends-with(${prop},${escapeCharacters(
507
+ contentRes[0]
508
+ ).trim()})`;
509
+ }
510
+ }
511
+ }
512
+ }
513
+
514
+ if (combinePattern?.length) {
515
+ if (isSvg(element)) {
516
+ pattern = `//*[local-name()='${tagName}' and ${combinePattern}]`;
517
+ } else {
518
+ pattern = `//${tagName}[${combinePattern}]`;
519
+ }
520
+ count = getCountOfXPath(pattern, element, docmt);
521
+ if (count === 1 && !isIndex) {
522
+ return pattern;
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+
529
+ if (value && isTarget && isNumberExist(value)) {
530
+ const contentRes = [...new Set(value.match(/([^0-9]+)/g))];
531
+ if (contentRes?.length) {
532
+ for (let i = 0; i < contentRes?.length; i++) {
533
+ if (
534
+ contentRes[i] &&
535
+ replaceWhiteSpaces(contentRes[i].trim())?.length > 1
536
+ ) {
537
+ if (!reWhiteSpace.test(contentRes[i])) {
538
+ mergePattern.push(
539
+ `contains(${prop},${escapeCharacters(
540
+ replaceWhiteSpaces(contentRes[i])
541
+ ).trim()})`
542
+ );
543
+ } else {
544
+ mergePattern.push(
545
+ `contains(${prop},${escapeCharacters(
546
+ contentRes[i].trim()
547
+ ).trim()})`
548
+ );
549
+ }
550
+ }
551
+ }
552
+ }
553
+
554
+ if (mergePattern?.length) {
555
+ if (isSvg(element)) {
556
+ pattern = `//*[local-name()='${tagName}' and ${mergePattern.join(
557
+ " and "
558
+ )}]`;
559
+ } else {
560
+ pattern = `//${tagName}[${mergePattern.join(" and ")}]`;
561
+ }
562
+ count = getCountOfXPath(pattern, element, docmt);
563
+ if (count === 1 && !isIndex) {
564
+ return pattern;
565
+ }
566
+ }
567
+ }
568
+
569
+ if (isSvg(element)) {
570
+ pattern = `//*[local-name()='${tagName}' and text()]`;
571
+ } else {
572
+ pattern = `//${tagName}[text()]`;
573
+ }
574
+
575
+ count = getCountOfXPath(pattern, element, docmt);
576
+ if (count === 1 && !isIndex) {
577
+ return pattern;
578
+ }
579
+ }
580
+ };
581
+
582
+ export const getAbsoluteXPath = (
583
+ domNode: HTMLElement | Element | null,
584
+ docmt: Document
585
+ ): string => {
586
+ try {
587
+ if (!domNode) {
588
+ return "";
589
+ }
590
+
591
+ let xpathe = isSvg(domNode)
592
+ ? `/*[local-name()='${domNode.tagName}']`
593
+ : `/${domNode.tagName}`;
594
+
595
+ // // If this node has siblings of the same tagName, get the index of this node
596
+ if (domNode.parentElement) {
597
+ // Get the siblings
598
+ const childNodes = Array.prototype.slice
599
+ .call(domNode.parentElement.children, 0)
600
+ .filter(
601
+ (childNode: HTMLElement) => childNode.tagName === domNode.tagName
602
+ );
603
+
604
+ // // If there's more than one sibling, append the index
605
+ if (childNodes.length > 1) {
606
+ const index = childNodes.indexOf(domNode);
607
+ xpathe += `[${index + 1}]`;
608
+ }
609
+ } else if (domNode instanceof HTMLElement) {
610
+ if (domNode.offsetParent) {
611
+ const childNodes = Array.prototype.slice
612
+ .call(domNode.offsetParent.children, 0)
613
+ .filter(
614
+ (childNode: HTMLElement) => childNode.tagName === domNode.tagName
615
+ );
616
+
617
+ // // If there's more than one sibling, append the index
618
+ if (childNodes.length > 1) {
619
+ const index = childNodes.indexOf(domNode);
620
+ xpathe += `[${index + 1}]`;
621
+ }
622
+ }
623
+ }
624
+
625
+ // // Make a recursive call to this nodes parents and prepend it to this xpath
626
+ return getAbsoluteXPath(domNode?.parentElement, docmt) + xpathe;
627
+ } catch (error) {
628
+ // If there's an unexpected exception, abort and don't get an XPath
629
+ console.log("xpath", error);
630
+
631
+ return "";
632
+ }
633
+ };
634
+
635
+ export const getRelativeXPath = (
636
+ domNode: HTMLElement | Element,
637
+ docmt: Document,
638
+ isIndex: boolean,
639
+ isTarget: boolean = false,
640
+ attributesArray: Attr[]
641
+ ) => {
642
+ try {
643
+ // Generate a cache key based on the node's identifier, index, and target flag
644
+ // Check if the result for this node is already cached
645
+ if (relativeXPathCache.has(domNode)) {
646
+ return relativeXPathCache.get(domNode);
647
+ }
648
+
649
+ // Initialize an array to hold parts of the XPath
650
+ const xpathParts = [];
651
+ let currentNode = domNode;
652
+
653
+ // Traverse up the DOM tree iteratively instead of using recursion
654
+ while (currentNode) {
655
+ let xpathe: string | undefined = "";
656
+ let hasUniqueAttr = false;
657
+ let attributes = domNode === currentNode ? attributesArray : currentNode.attributes
658
+
659
+ // Loop through attributes to check for unique identifiers
660
+ for (const attrName of attributes) {
661
+ if (checkBlockedAttributes(attrName, currentNode, isTarget)) {
662
+ let attrValue = attrName.nodeValue;
663
+
664
+ // Clean up attribute value
665
+ attrValue = attrValue?.replace("removePointers", "") ?? "";
666
+
667
+ const elementName = attrName.name;
668
+
669
+ // Construct an XPath based on attribute
670
+ xpathe = getXpathString(currentNode, elementName, attrValue);
671
+
672
+ let othersWithAttr: number = 0;
673
+ if (xpathe) {
674
+ othersWithAttr = getCountOfXPath(xpathe, currentNode, docmt);
675
+ if (othersWithAttr === 1) {
676
+ xpathParts.unshift(replaceTempAttributes(xpathe));
677
+ hasUniqueAttr = true;
678
+ break;
679
+ }
680
+ }
681
+
682
+ if (othersWithAttr > 1 && isIndex) {
683
+ xpathe = findXpathWithIndex(
684
+ xpathe,
685
+ currentNode,
686
+ docmt,
687
+ othersWithAttr
688
+ );
689
+ if (xpathe) {
690
+ xpathParts.unshift(replaceTempAttributes(xpathe));
691
+ hasUniqueAttr = true;
692
+ break;
693
+ }
694
+ // return replaceTempAttributes(xpathe);
695
+ }
696
+ }
697
+ }
698
+
699
+ if (currentNode.textContent) {
700
+ if (!isTarget || (isTarget && !isNumberExist(currentNode.textContent))) {
701
+ let reWhiteSpace = new RegExp(/^[\S]+( [\S]+)*$/gi);
702
+
703
+ if (!reWhiteSpace.test(currentNode.textContent)) {
704
+ xpathe =
705
+ isSvg(currentNode)
706
+ ? `//*[local-name()='${
707
+ currentNode.tagName
708
+ }' and normalize-space(.)=${escapeCharacters(
709
+ getFilteredText(currentNode)
710
+ )}]`
711
+ : `//${
712
+ currentNode.tagName || "*"
713
+ }[normalize-space(.)=${escapeCharacters(
714
+ getFilteredText(currentNode)
715
+ )}]`;
716
+ } else {
717
+ xpathe =
718
+ isSvg(currentNode)
719
+ ? `//*[local-name()='${
720
+ currentNode.tagName
721
+ }' and .=${escapeCharacters(getFilteredText(currentNode))}]`
722
+ : `//${currentNode.tagName || "*"}[.=${escapeCharacters(
723
+ getFilteredText(currentNode)
724
+ )}]`;
725
+ }
726
+
727
+ let othersWithAttr = getCountOfXPath(xpathe, currentNode, docmt);
728
+ if (othersWithAttr === 1) {
729
+ return xpathe;
730
+ }
731
+
732
+ if (othersWithAttr > 1 && isIndex) {
733
+ xpathe = findXpathWithIndex(xpathe, currentNode, docmt, othersWithAttr);
734
+ return xpathe;
735
+ }
736
+ } else {
737
+ let combinePattern: string[] = [];
738
+ const contentRes = [
739
+ ...new Set(getFilteredText(currentNode).match(/([^0-9]+)/g)),
740
+ ];
741
+ let reWhiteSpace = new RegExp(/^[\S]+( [\S]+)*$/gi);
742
+ if (contentRes?.length) {
743
+ for (let i = 0; i < contentRes?.length; i++) {
744
+ if (
745
+ contentRes[i] &&
746
+ replaceWhiteSpaces((contentRes[i] as string).trim())
747
+ ) {
748
+ if (!reWhiteSpace.test(contentRes[i] as string)) {
749
+ combinePattern.push(
750
+ `contains(.,${escapeCharacters(
751
+ replaceWhiteSpaces(contentRes[i])
752
+ ).trim()})`
753
+ );
754
+ } else {
755
+ combinePattern.push(
756
+ `contains(.,${escapeCharacters(
757
+ (contentRes[i] as string).trim()
758
+ ).trim()})`
759
+ );
760
+ }
761
+ }
762
+ }
763
+ }
764
+
765
+ if (combinePattern?.length) {
766
+ xpathe =
767
+ isSvg(currentNode)
768
+ ? `//*[local-name()='${
769
+ currentNode.tagName
770
+ }' and ${combinePattern.join(" and ")}]`
771
+ : `//${currentNode.tagName || "*"}[${combinePattern.join(
772
+ " and "
773
+ )}]`;
774
+ let othersWithAttr = getCountOfXPath(xpathe, currentNode, docmt);
775
+ if (othersWithAttr === 1) {
776
+ return xpathe;
777
+ }
778
+
779
+ if (othersWithAttr > 1 && isIndex) {
780
+ xpathe = findXpathWithIndex(xpathe, currentNode, docmt, othersWithAttr);
781
+ return xpathe;
782
+ }
783
+ }
784
+ }
785
+ }
786
+
787
+ // If no unique attribute was found, construct XPath by tag name
788
+ if (!hasUniqueAttr) {
789
+ let tagBasedXPath = isSvg(currentNode)
790
+ ? `/*[local-name()='${currentNode.tagName}']`
791
+ : `/${currentNode.tagName}`;
792
+
793
+ // Handle sibling nodes
794
+ if (currentNode.parentElement) {
795
+ const siblings = Array.from(
796
+ currentNode.parentElement.children
797
+ ).filter((childNode) => childNode.tagName === currentNode.tagName);
798
+
799
+ // Append index to distinguish between siblings
800
+ if (siblings.length > 1) {
801
+ const index = siblings.indexOf(currentNode);
802
+ tagBasedXPath += `[${index + 1}]`;
803
+ }
804
+ }
805
+
806
+ // Add the constructed tag-based XPath to the parts array
807
+ xpathParts.unshift(tagBasedXPath);
808
+ } else {
809
+ break;
810
+ }
811
+
812
+ // Move up to the parent node for the next iteration
813
+ currentNode = currentNode.parentElement!;
814
+ }
815
+
816
+ // Combine all parts into the final XPath
817
+ const finalXPath = `${xpathParts.join("")}`;
818
+
819
+ // Cache the final XPath for this node
820
+ relativeXPathCache.set(domNode, finalXPath);
821
+ return finalXPath;
822
+ } catch (error) {
823
+ console.log(error);
824
+ return null;
825
+ }
826
+ };
827
+
828
+ export const getCombinationXpath = (
829
+ attribute: Attr,
830
+ domNode: HTMLElement | Element
831
+ ) => {
832
+ const combinePattern = [];
833
+ let pattern: string = "";
834
+
835
+ if (
836
+ attribute &&
837
+ !isNumberExist(attribute.value) &&
838
+ typeof attribute.nodeValue !== "function" // &&
839
+ // !modifiedElementAttributes?.find(
840
+ // (x) => x.element === domNode && x.attributeName === attribute.name
841
+ // )
842
+ ) {
843
+ const contentRes = [...new Set(attribute.value.match(/([^0-9]+)/g))];
844
+ if (contentRes?.length) {
845
+ for (let i = 0; i < contentRes?.length; i++) {
846
+ if (
847
+ contentRes[i] &&
848
+ replaceWhiteSpaces(contentRes[i].trim())?.length > 2
849
+ ) {
850
+ if (!reWhiteSpace.test(contentRes[i])) {
851
+ combinePattern.push(
852
+ `contains(@${attribute.name},${escapeCharacters(
853
+ replaceWhiteSpaces(contentRes[i])
854
+ ).trim()})`
855
+ );
856
+ } else {
857
+ combinePattern.push(
858
+ `contains(@${attribute.name},${escapeCharacters(
859
+ contentRes[i].trim()
860
+ )})`
861
+ );
862
+ }
863
+ }
864
+ }
865
+ }
866
+
867
+ if (combinePattern?.length) {
868
+ pattern = isSvg(domNode)
869
+ ? `//*[local-name()='${domNode.tagName}' and ${combinePattern.join(
870
+ " and "
871
+ )}]`
872
+ : `//${domNode.tagName}[${combinePattern.join(" and ")}]`;
873
+ return pattern;
874
+ }
875
+ }
876
+ };
877
+
878
+ export const getAttributeCombinationXpath = (
879
+ domNode: HTMLElement | Element,
880
+ docmt: Document,
881
+ uniqueAttributes: Attr[],
882
+ isTarget: boolean
883
+ ) => {
884
+ try {
885
+ const xpathAttributes = [];
886
+
887
+ if (uniqueAttributes.length > 1) {
888
+ for (const attrName of uniqueAttributes) {
889
+ if (checkBlockedAttributes(attrName, domNode, isTarget)) {
890
+ const attrValue = attrName.nodeValue;
891
+
892
+ if (!reWhiteSpace.test(attrValue!)) {
893
+ xpathAttributes.push(
894
+ `normalize-space(@${attrName.nodeName})="${attrValue}"`
895
+ );
896
+ } else if (attrName.nodeName === "class") {
897
+ xpathAttributes.push(
898
+ `contains(@${attrName.nodeName},"${attrValue}")`
899
+ );
900
+ } else {
901
+ xpathAttributes.push(`@${attrName.nodeName}="${attrValue}"`);
902
+ }
903
+
904
+ const xpathe = isSvg(domNode)
905
+ ? `//*[local-name()='${domNode.tagName}' and ${xpathAttributes.join(
906
+ " and "
907
+ )}]`
908
+ : `//${domNode.tagName}[${xpathAttributes.join(" and ")}]`;
909
+ let othersWithAttr;
910
+
911
+ // If the XPath does not parse, move to the next unique attribute
912
+ try {
913
+ othersWithAttr = getCountOfXPath(xpathe, domNode, docmt);
914
+ } catch (ign) {
915
+ continue;
916
+ }
917
+
918
+ if (othersWithAttr === 1 && !xpathe.includes("and")) {
919
+ return "";
920
+ }
921
+ // If the attribute isn't actually unique, get it's index too
922
+ if (othersWithAttr === 1 && xpathe.includes("and")) {
923
+ return xpathe;
924
+ }
925
+ }
926
+ }
927
+ }
928
+ } catch (error) {
929
+ // If there's an unexpected exception, abort and don't get an XPath
930
+ console.log(`'${JSON.stringify(error, null, 2)}'`);
931
+ }
932
+ };
933
+
934
+ export const intermediateXpathStep = (
935
+ targetElemt: HTMLElement | Element,
936
+ attr: { name: string; value: string },
937
+ isTarget: boolean
938
+ ): string => {
939
+ let isSvgElement = isSvg(targetElemt);
940
+ let expression: string = "";
941
+
942
+ if (checkBlockedAttributes(attr, targetElemt, isTarget)) {
943
+ let attrValue = attr.value;
944
+ attrValue = attrValue.replace("removePointers", "");
945
+ const elementName = attr.name;
946
+
947
+ if (!reWhiteSpace.test(attrValue)) {
948
+ expression = isSvgElement
949
+ ? `*[local-name()='${
950
+ targetElemt.tagName
951
+ }' and normalize-space(@${elementName})=${escapeCharacters(
952
+ attrValue
953
+ )}]`
954
+ : `${
955
+ targetElemt.tagName || "*"
956
+ }[normalize-space(@${elementName})=${escapeCharacters(attrValue)}]`;
957
+ } else {
958
+ expression = isSvgElement
959
+ ? `*[local-name()='${
960
+ targetElemt.tagName
961
+ }' and @${elementName}=${escapeCharacters(attrValue)}]`
962
+ : `${targetElemt.tagName || "*"}[@${elementName}=${escapeCharacters(
963
+ attrValue
964
+ )}]`;
965
+ }
966
+ }
967
+
968
+ return expression;
969
+ };
970
+
971
+ export const getFilteredTextXPath = (
972
+ node: HTMLElement | Element,
973
+ docmt: Document
974
+ ) => {
975
+ if (!node.textContent) return "";
976
+
977
+ const filteredText = getFilteredText(node);
978
+
979
+ let xpathe;
980
+
981
+ if (!reWhiteSpace.test(filteredText)) {
982
+ xpathe = isSvg(node)
983
+ ? `//*[local-name()='${
984
+ node.tagName
985
+ }' and normalize-space(.)=${escapeCharacters(filteredText)}]`
986
+ : `//${node.tagName || "*"}[normalize-space(.)=${escapeCharacters(
987
+ filteredText
988
+ )}]`;
989
+ } else {
990
+ xpathe = isSvg(node)
991
+ ? `//*[local-name()='${node.tagName}' and .=${escapeCharacters(
992
+ filteredText
993
+ )}]`
994
+ : `//${node.tagName || "*"}[.=${escapeCharacters(filteredText)}]`;
995
+ }
996
+
997
+ return xpathe;
998
+ };
999
+
1000
+ export const getTextXpathFunction = (domNode: HTMLElement | Element) => {
1001
+ const trimmedText = getTextContent(domNode)?.trim();
1002
+ const filteredText = trimmedText
1003
+ ? escapeCharacters(deleteGarbageFromInnerText(trimmedText))
1004
+ : trimmedText;
1005
+ if (filteredText) {
1006
+ if (filteredText !== `'${trimmedText}'`) {
1007
+ return `contains(.,${filteredText})`;
1008
+ }
1009
+ return `normalize-space(.)='${trimmedText}'`;
1010
+ }
1011
+ };
1012
+
1013
+ export const getXpathString = (
1014
+ node: HTMLElement | Element,
1015
+ attrName: string,
1016
+ attrValue: string
1017
+ ) => {
1018
+ const reWhiteSpace = new RegExp(/^[\S]+( [\S]+)*$/gi);
1019
+ let xpathe: string = "";
1020
+
1021
+ if (attrValue) {
1022
+ if (!reWhiteSpace.test(attrValue)) {
1023
+ xpathe = isSvg(node)
1024
+ ? `//*[local-name()='${
1025
+ node.tagName
1026
+ }' and contains(@${attrName},${escapeCharacters(attrValue)})]`
1027
+ : `//${node.tagName || "*"}[contains(@${attrName},${escapeCharacters(
1028
+ attrValue
1029
+ )})]`;
1030
+ } else if (attrName === "class") {
1031
+ xpathe = isSvg(node)
1032
+ ? `//*[local-name()='${
1033
+ node.tagName
1034
+ }' and contains(@${attrName},${escapeCharacters(attrValue)})]`
1035
+ : `//${node.tagName || "*"}[contains(@${attrName},${escapeCharacters(
1036
+ attrValue
1037
+ )})]`;
1038
+ } else {
1039
+ xpathe = isSvg(node)
1040
+ ? `//*[local-name()='${
1041
+ node.tagName
1042
+ }' and @${attrName}=${escapeCharacters(attrValue)}]`
1043
+ : `//${node.tagName || "*"}[@${attrName}=${escapeCharacters(
1044
+ attrValue
1045
+ )}]`;
1046
+ }
1047
+ }
1048
+
1049
+ return xpathe;
1050
+ };
1051
+
1052
+ export const replaceActualAttributes = (
1053
+ str: string,
1054
+ element: { attributes: any }
1055
+ ) => {
1056
+ if (str) {
1057
+ return str.replace(/\bdisabled\b/gi, "flndisabled");
1058
+ }
1059
+ return str;
1060
+ };
1061
+
1062
+ const addAttributeSplitCombineXpaths = (
1063
+ attributes: NamedNodeMap,
1064
+ targetElemt: HTMLElement | Element,
1065
+ docmt: Document,
1066
+ isTarget: boolean
1067
+ ) => {
1068
+ const attributesArray = Array.prototype.slice.call(attributes);
1069
+ const xpaths: { key: string; value: string }[] = [];
1070
+ try {
1071
+ attributesArray.map((element) => {
1072
+ if (checkBlockedAttributes(element, targetElemt, isTarget)) {
1073
+ const xpth = getCombinationXpath(element, targetElemt);
1074
+ if (xpth) {
1075
+ xpaths.push({
1076
+ key: `split xpath by ${element.name}`,
1077
+ value: xpth,
1078
+ });
1079
+ }
1080
+ }
1081
+ });
1082
+ } catch (error) {
1083
+ console.log(error);
1084
+ }
1085
+
1086
+ return xpaths;
1087
+ };
1088
+
1089
+ export const getReferenceElementsXpath = (
1090
+ domNode: HTMLElement | Element,
1091
+ docmt: Document,
1092
+ isTarget: boolean
1093
+ ) => {
1094
+ let nodeXpath1;
1095
+ const xpaths1 = [];
1096
+ if (
1097
+ domNode.textContent &&
1098
+ (!isTarget || (isTarget && !isNumberExist(domNode.textContent)))
1099
+ ) {
1100
+ if (!reWhiteSpace.test(domNode.textContent)) {
1101
+ nodeXpath1 = isSvg(domNode)
1102
+ ? `*[local-name()='${domNode.tagName}' and ${getTextXpathFunction(
1103
+ domNode
1104
+ )})]`
1105
+ : `${domNode.tagName}[${getTextXpathFunction(domNode)}]`;
1106
+ if (nodeXpath1) {
1107
+ xpaths1.push({ key: "getReferenceElementsXpath", value: nodeXpath1 });
1108
+ }
1109
+ } else {
1110
+ nodeXpath1 = isSvg(domNode)
1111
+ ? `*[local-name()='${domNode.tagName}' and .=${escapeCharacters(
1112
+ getTextContent(domNode)
1113
+ )}]`
1114
+ : `${domNode.tagName}[.=${escapeCharacters(getTextContent(domNode))}]`;
1115
+ if (nodeXpath1) {
1116
+ xpaths1.push({ key: "getReferenceElementsXpath", value: nodeXpath1 });
1117
+ }
1118
+ }
1119
+ }
1120
+
1121
+ if (domNode.attributes) {
1122
+ for (const attrName of domNode.attributes) {
1123
+ if (checkBlockedAttributes(attrName, domNode, isTarget)) {
1124
+ let attrValue = attrName.nodeValue;
1125
+ if (attrValue) {
1126
+ attrValue = attrValue.replace("removePointers", "");
1127
+ const elementName = attrName.name;
1128
+
1129
+ if (elementName === "class") {
1130
+ nodeXpath1 = isSvg(domNode)
1131
+ ? `*[local-name()='${
1132
+ domNode.tagName
1133
+ }' and contains(@${elementName},${escapeCharacters(
1134
+ attrValue
1135
+ )})]`
1136
+ : `${domNode.tagName}[contains(@${elementName},${escapeCharacters(
1137
+ attrValue
1138
+ )})]`;
1139
+ } else {
1140
+ nodeXpath1 = isSvg(domNode)
1141
+ ? `*[local-name()='${
1142
+ domNode.tagName
1143
+ }' and @${elementName}=${escapeCharacters(attrValue)}]`
1144
+ : `${domNode.tagName}[@${elementName}=${escapeCharacters(
1145
+ attrValue
1146
+ )}]`;
1147
+ }
1148
+
1149
+ if (nodeXpath1) {
1150
+ xpaths1.push({
1151
+ key: "getReferenceElementsXpath",
1152
+ value: nodeXpath1,
1153
+ });
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ if (!xpaths1?.length) {
1161
+ const attributesArray = Array.prototype.slice.call(domNode.attributes);
1162
+ if (attributesArray?.length > 1) {
1163
+ const combinationXpath = getAttributeCombinationXpath(
1164
+ domNode,
1165
+ docmt,
1166
+ Array.prototype.slice.call(domNode.attributes),
1167
+ isTarget
1168
+ );
1169
+ if (combinationXpath) {
1170
+ xpaths1.push({
1171
+ key: "getReferenceElementsXpath",
1172
+ value: combinationXpath,
1173
+ });
1174
+ }
1175
+ }
1176
+ }
1177
+
1178
+ if (!xpaths1?.length) {
1179
+ const combinePattern = [];
1180
+ let pattern;
1181
+ const tag = isSvg(domNode);
1182
+ if (domNode.textContent && isTarget && isNumberExist(domNode.textContent)) {
1183
+ const contentRes = [...new Set(domNode.textContent.match(/([^0-9]+)/g))];
1184
+ if (contentRes?.length) {
1185
+ for (let i = 0; i < contentRes?.length; i++) {
1186
+ if (
1187
+ contentRes[i] &&
1188
+ replaceWhiteSpaces(contentRes[i].trim())?.length > 1
1189
+ ) {
1190
+ if (!reWhiteSpace.test(contentRes[i])) {
1191
+ combinePattern.push(
1192
+ `contains(.,${escapeCharacters(
1193
+ replaceWhiteSpaces(contentRes[i])
1194
+ ).trim()})`
1195
+ );
1196
+ } else {
1197
+ combinePattern.push(
1198
+ `contains(.,${escapeCharacters(contentRes[i]).trim()})`
1199
+ );
1200
+ }
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ if (combinePattern?.length) {
1206
+ if (tag) {
1207
+ pattern = `//*[local-name()='${tag}' and ${combinePattern.join(
1208
+ " and "
1209
+ )}]`;
1210
+ } else {
1211
+ pattern = `//${tag}[${combinePattern.join(" and ")}]`;
1212
+ }
1213
+
1214
+ if (pattern)
1215
+ xpaths1.push({ key: "getReferenceElementsXpath", value: pattern });
1216
+ }
1217
+ }
1218
+ }
1219
+
1220
+ if (!xpaths1?.length) {
1221
+ const xpaths = addAttributeSplitCombineXpaths(
1222
+ domNode.attributes,
1223
+ domNode,
1224
+ docmt,
1225
+ isTarget
1226
+ );
1227
+ if (xpaths?.length) {
1228
+ xpaths1.concat(xpaths);
1229
+ }
1230
+ }
1231
+
1232
+ return xpaths1;
1233
+ };
1234
+
1235
+ export function parseXml(xmlStr: string) {
1236
+ if (window.DOMParser) {
1237
+ return new window.DOMParser().parseFromString(xmlStr, "text/xml");
1238
+ }
1239
+
1240
+ return null;
1241
+ }
1242
+
1243
+ export const normalizeXPath = (xpath: string): string => {
1244
+ // Replace text() = "value" or text()='value'
1245
+ xpath = xpath.replace(
1246
+ /text\(\)\s*=\s*(['"])(.*?)\1/g,
1247
+ "normalize-space(.)=$1$2$1"
1248
+ );
1249
+
1250
+ // Replace . = "value" or .='value'
1251
+ xpath = xpath.replace(/\.\s*=\s*(['"])(.*?)\1/g, "normalize-space(.)=$1$2$1");
1252
+
1253
+ return xpath;
1254
+ };
1255
+
1256
+ export const findMatchingParenthesis = (
1257
+ text: string,
1258
+ openPos: number
1259
+ ): number => {
1260
+ let closePos = openPos;
1261
+ let counter = 1;
1262
+ while (counter > 0) {
1263
+ const c = text[++closePos];
1264
+ if (c == "(") {
1265
+ counter++;
1266
+ } else if (c == ")") {
1267
+ counter--;
1268
+ }
1269
+ }
1270
+ return closePos;
1271
+ };
1272
+
1273
+ export const xpathUtils = {
1274
+ parseXml,
1275
+ getReferenceElementsXpath,
1276
+ getAbsoluteXPath,
1277
+ getRelativeXPath,
1278
+ getCombinationXpath,
1279
+ getAttributeCombinationXpath,
1280
+ getElementFromXpath,
1281
+ isSvg,
1282
+ findXpathWithIndex,
1283
+ isNumberExist,
1284
+ getTextContent,
1285
+ getCountOfXPath,
1286
+ normalizeXPath,
1287
+ getShadowRoot,
1288
+ escapeCharacters,
1289
+ removeParenthesis,
1290
+ checkBlockedAttributes,
1291
+ getRelationship,
1292
+ findMatchingParenthesis,
1293
+ deleteGarbageFromInnerText,
1294
+ replaceTempAttributes,
1295
+ createObserver,
1296
+ startObserver,
1297
+ stopObserver,
1298
+ modifiedElementAttributes,
1299
+ };