@devassure/tpp-engine 0.0.0-stage → 1.8.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.
Files changed (69) hide show
  1. package/DEV_README.md +19 -0
  2. package/README.md +6 -3
  3. package/index.js +121 -0
  4. package/package.json +30 -4
  5. package/src/accessibility/combined-a11y-report.js +208 -0
  6. package/src/accessibility/tpp-a11y.js +81 -0
  7. package/src/actions.js +3201 -0
  8. package/src/api/api-auth.js +18 -0
  9. package/src/api/api-service.js +289 -0
  10. package/src/app-config.json +7 -0
  11. package/src/autoheal-actions.js +309 -0
  12. package/src/console-log/console-log-manager.js +39 -0
  13. package/src/console-log/console-log-utils.js +201 -0
  14. package/src/database/mysql/mysql-service.js +23 -0
  15. package/src/database/neo4j/neo4j-service.js +32 -0
  16. package/src/database/oracle/oracle-service.js +48 -0
  17. package/src/database/pg/pg-service.js +29 -0
  18. package/src/downloads/downloads-manager.js +86 -0
  19. package/src/downloads/downloads-utils.js +113 -0
  20. package/src/globalsetup.js +68 -0
  21. package/src/globalteardown.js +57 -0
  22. package/src/hooks/each-hook.js +45 -0
  23. package/src/hooks/hook-utils.js +21 -0
  24. package/src/library/authenticator/authenticator-utils.js +43 -0
  25. package/src/library/csv/csv-utils.js +297 -0
  26. package/src/library/date/date-utils.js +168 -0
  27. package/src/library/emailTesting/TokenConfig.js +46 -0
  28. package/src/library/emailTesting/emailTesting-utils.js +527 -0
  29. package/src/library/excel/excel-utils.js +922 -0
  30. package/src/library/file/file-action-utils.js +129 -0
  31. package/src/library/file/file-utils.js +127 -0
  32. package/src/library/json/json-utils.js +104 -0
  33. package/src/library/math/math-utils.js +283 -0
  34. package/src/library/softAssert/softAssert-utils.js +154 -0
  35. package/src/library/table/table-utils.js +92 -0
  36. package/src/library/text/text-utils.js +45 -0
  37. package/src/library/tpp-library.js +81 -0
  38. package/src/library/web/slider-utils.js +66 -0
  39. package/src/mobile/appium/appium-manager.js +158 -0
  40. package/src/mobile/device-cloud/browserstack-service.js +44 -0
  41. package/src/mobile/device-cloud/commands/browserstack-commands.js +20 -0
  42. package/src/mobile/device-cloud/commands/device-cloud-commands.js +17 -0
  43. package/src/mobile/device-cloud/device-cloud-service.js +29 -0
  44. package/src/mobile/device-cloud/pcloudy-service.js +65 -0
  45. package/src/mobile/mobile-actions.js +1568 -0
  46. package/src/mobile/recorder-utils/css-selector-generator.js +2 -0
  47. package/src/mobile/recorder-utils/finder.js +752 -0
  48. package/src/mobile/recorder-utils/mobile-recorder.js +266 -0
  49. package/src/mobile/web-driver-utils.js +110 -0
  50. package/src/models/tpp-element.js +58 -0
  51. package/src/models/tpp-mobile-element.js +50 -0
  52. package/src/network-log/network-log-manager.js +96 -0
  53. package/src/network-log/network-log-utils.js +361 -0
  54. package/src/types/elements.types.js +24 -0
  55. package/src/utils/ai-utils.js +48 -0
  56. package/src/utils/assert-utils.js +50 -0
  57. package/src/utils/autoheal-utils.js +9 -0
  58. package/src/utils/common-utils.js +120 -0
  59. package/src/utils/cookie-utils.js +70 -0
  60. package/src/utils/json-utils.js +10 -0
  61. package/src/utils/log-utils.js +10 -0
  62. package/src/utils/logger.js +76 -0
  63. package/src/utils/screenshot-utils.js +42 -0
  64. package/src/utils/token-utils.js +18 -0
  65. package/src/utils/vectordb-utils.js +125 -0
  66. package/src/visual-test/tpp-visual-test.js +122 -0
  67. package/src/web/web-executor-agent.js +149 -0
  68. package/src/web/web-executor-data-fetch.js +353 -0
  69. package/test-excel-utils.js +570 -0
@@ -0,0 +1,752 @@
1
+ //Source: https://github.com/antonmedv/finder
2
+ var tppLimit;
3
+ (function (tppLimit) {
4
+ tppLimit[tppLimit["All"] = 0] = "All";
5
+ tppLimit[tppLimit["Two"] = 1] = "Two";
6
+ tppLimit[tppLimit["One"] = 2] = "One";
7
+ })(tppLimit || (tppLimit = {}));
8
+ let tppConfig;
9
+ let tppRootDocument;
10
+
11
+ function tppSelector(path) {
12
+ let node = path[0];
13
+ let query = node.name;
14
+ for (let i = 1; i < path.length; i++) {
15
+ const level = path[i].level || 0;
16
+ if (node.level === level - 1) {
17
+ query = `${path[i].name} > ${query}`;
18
+ }
19
+ else {
20
+ query = `${path[i].name} ${query}`;
21
+ }
22
+ node = path[i];
23
+ }
24
+ return query;
25
+ }
26
+
27
+ function tppFindRootDocument(rootNode, defaults) {
28
+ if (rootNode.nodeType === Node.DOCUMENT_NODE) {
29
+ return rootNode;
30
+ }
31
+ if (rootNode === defaults.root) {
32
+ return rootNode.ownerDocument;
33
+ }
34
+ return rootNode;
35
+ }
36
+
37
+ function tppFinder(input, ignoreOptions) {
38
+ if (input.nodeType !== Node.ELEMENT_NODE) {
39
+ throw new Error(`Can't generate CSS selector for non-element node type.`);
40
+ }
41
+ if ("html" === input.tagName.toLowerCase()) {
42
+ return "html";
43
+ }
44
+ let path = tppBottomUpSearch(input, tppLimit.All, ignoreOptions, () => tppBottomUpSearch(input, tppLimit.Two, ignoreOptions, () => tppBottomUpSearch(input, tppLimit.One, ignoreOptions)));
45
+ if (path) {
46
+ const optimized = tppSort(optimize(path, input));
47
+ if (optimized.length > 0) {
48
+ path = optimized[0];
49
+ }
50
+ return tppSelector(path);
51
+ }
52
+ else {
53
+ return undefined;
54
+ }
55
+ }
56
+
57
+ function tppId(input) {
58
+ const elementId = input.getAttribute("id");
59
+ if (elementId && !/\d/.test(elementId) && tppConfig.idName(elementId)) {
60
+ return {
61
+ name: "#" + tptppcssesc(elementId, { isIdentifier: true }),
62
+ penalty: 0,
63
+ };
64
+ }
65
+ return null;
66
+ }
67
+
68
+ function tppBottomUpSearch(input, limit, ignoreOptions, fallback) {
69
+ let path = null;
70
+ let stack = [];
71
+ let current = input;
72
+ let i = 0;
73
+ while (current && current !== tppConfig.root.parentElement) {
74
+ let level = (!ignoreOptions?.id && tppMaybe(tppId(current))) ||
75
+ tppMaybe(...tppAttr(current)) ||
76
+ (!ignoreOptions?.tagName && tppMaybe(tppTagName(current))) ||
77
+ tppMaybe(...tppClassNames(current)) ||
78
+ [tppAny()];
79
+ const nth = tppIndex(current);
80
+ if (limit === tppLimit.All) {
81
+ if (nth) {
82
+ level = level.concat(level.filter(tppDispensableNth).map((node) => tppNthChild(node, nth)));
83
+ }
84
+ }
85
+ else if (limit === tppLimit.Two) {
86
+ level = level.slice(0, 1);
87
+ if (nth) {
88
+ level = level.concat(level.filter(tppDispensableNth).map((node) => tppNthChild(node, nth)));
89
+ }
90
+ }
91
+ else if (limit === tppLimit.One) {
92
+ const [node] = (level = level.slice(0, 1));
93
+ if (nth && tppDispensableNth(node)) {
94
+ level = [tppNthChild(node, nth)];
95
+ }
96
+ }
97
+ for (let node of level) {
98
+ node.level = i;
99
+ }
100
+ stack.push(level);
101
+ if (stack.length >= tppConfig.seedMinLength) {
102
+ path = tppFindUniquePath(stack, fallback);
103
+ if (path) {
104
+ break;
105
+ }
106
+ }
107
+ current = current.parentElement;
108
+ i++;
109
+ }
110
+ if (!path) {
111
+ path = tppFindUniquePath(stack, fallback);
112
+ }
113
+ return path;
114
+ }
115
+
116
+ function tppUnique(path) {
117
+ switch (tppRootDocument.querySelectorAll(tppSelector(path)).length) {
118
+ case 0:
119
+ return false;
120
+ case 1:
121
+ return true;
122
+ default:
123
+ return false;
124
+ }
125
+ }
126
+
127
+ function tppFindUniquePath(stack, fallback) {
128
+ const paths = tppSort(tppCombinations(stack));
129
+ if (paths.length > tppConfig.threshold) {
130
+ return fallback ? fallback() : null;
131
+ }
132
+ for (let candidate of paths) {
133
+ if (tppUnique(candidate)) {
134
+ return candidate;
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function tppPenalty(path) {
141
+ return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0);
142
+ }
143
+
144
+ function tppAttr(input) {
145
+ const attrs = Array.from(input.attributes).filter((attr) => tppConfig.attr(attr.name, attr.value));
146
+ return attrs.map((attr) => ({
147
+ name: "[" +
148
+ tptppcssesc(attr.name, { isIdentifier: true }) +
149
+ '="' +
150
+ tptppcssesc(attr.value) +
151
+ '"]',
152
+ penalty: 0.5,
153
+ }));
154
+ }
155
+
156
+ function tppClassNames(input) {
157
+ const names = Array.from(input.classList).filter(tppConfig.className);
158
+ return names.map((name) => ({
159
+ name: "." + tptppcssesc(name, { isIdentifier: true }),
160
+ penalty: 1,
161
+ }));
162
+ }
163
+ function tppTagName(input) {
164
+ const name = tppSanitiseTagName(input.tagName);
165
+ if (tppConfig.tagName(name)) {
166
+ return {
167
+ name,
168
+ penalty: 2,
169
+ };
170
+ }
171
+ return null;
172
+ }
173
+ function tppAny() {
174
+ return {
175
+ name: "*",
176
+ penalty: 3,
177
+ };
178
+ }
179
+ function tppIndex(input) {
180
+ const parent = input.parentNode;
181
+ if (!parent) {
182
+ return null;
183
+ }
184
+ let child = parent.firstChild;
185
+ if (!child) {
186
+ return null;
187
+ }
188
+ let i = 0;
189
+ while (child) {
190
+ if (child.nodeType === Node.ELEMENT_NODE) {
191
+ i++;
192
+ }
193
+ if (child === input) {
194
+ break;
195
+ }
196
+ child = child.nextSibling;
197
+ }
198
+ return i;
199
+ }
200
+
201
+ function tppNthChild(node, i) {
202
+ return {
203
+ name: node.name + `:nth-child(${i})`,
204
+ penalty: node.penalty + 1,
205
+ };
206
+ }
207
+ function tppDispensableNth(node) {
208
+ return node.name !== "html" && !node.name.startsWith("#");
209
+ }
210
+ function tppMaybe(...level) {
211
+ const list = level.filter(tppNotEmpty);
212
+ if (list.length > 0) {
213
+ return list;
214
+ }
215
+ return null;
216
+ }
217
+ function tppNotEmpty(value) {
218
+ return value !== null && value !== undefined;
219
+ }
220
+ function* tppCombinations(stack, path = []) {
221
+ if (stack.length > 0) {
222
+ for (let node of stack[0]) {
223
+ yield* tppCombinations(stack.slice(1, stack.length), path.concat(node));
224
+ }
225
+ }
226
+ else {
227
+ yield path;
228
+ }
229
+ }
230
+ function tppSort(paths) {
231
+ return Array.from(paths).sort((a, b) => tppPenalty(a) - tppPenalty(b));
232
+ }
233
+ function* optimize(path, input, scope = {
234
+ counter: 0,
235
+ visited: new Map(),
236
+ }) {
237
+ if (path.length > 2 && path.length > tppConfig.optimizedMinLength) {
238
+ for (let i = 1; i < path.length - 1; i++) {
239
+ if (scope.counter > tppConfig.maxNumberOfTries) {
240
+ return; // Okay At least I tried!
241
+ }
242
+ scope.counter += 1;
243
+ const newPath = [...path];
244
+ newPath.splice(i, 1);
245
+ const newPathKey = tppSelector(newPath);
246
+ if (scope.visited.has(newPathKey)) {
247
+ return;
248
+ }
249
+ if (tppUnique(newPath) && tppSame(newPath, input)) {
250
+ yield newPath;
251
+ scope.visited.set(newPathKey, true);
252
+ yield* optimize(newPath, input, scope);
253
+ }
254
+ }
255
+ }
256
+ }
257
+ function tppSame(path, input) {
258
+ return tppRootDocument.querySelector(tppSelector(path)) === input;
259
+ }
260
+ const tppRegexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/;
261
+ const tppRegexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/;
262
+ const tppRegexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g;
263
+ const tppDefaultOptions = {
264
+ escapeEverything: false,
265
+ isIdentifier: false,
266
+ quotes: "single",
267
+ wrap: false,
268
+ };
269
+ function tptppcssesc(string, opt = {}) {
270
+ const options = Object.assign(Object.assign({}, tppDefaultOptions), opt);
271
+ if (options.quotes != "single" && options.quotes != "double") {
272
+ options.quotes = "single";
273
+ }
274
+ const quote = options.quotes == "double" ? '"' : "'";
275
+ const isIdentifier = options.isIdentifier;
276
+ const firstChar = string.charAt(0);
277
+ let output = "";
278
+ let counter = 0;
279
+ const length = string.length;
280
+ while (counter < length) {
281
+ const character = string.charAt(counter++);
282
+ let codePoint = character.charCodeAt(0);
283
+ let value = void 0;
284
+ // If it’s not a printable ASCII character…
285
+ if (codePoint < 0x20 || codePoint > 0x7e) {
286
+ if (codePoint >= 0xd800 && codePoint <= 0xdbff && counter < length) {
287
+ // It’s a high surrogate, and there is a next character.
288
+ const extra = string.charCodeAt(counter++);
289
+ if ((extra & 0xfc00) == 0xdc00) {
290
+ // next character is low surrogate
291
+ codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
292
+ }
293
+ else {
294
+ // It’s an unmatched surrogate; only append this code unit, in case
295
+ // the next code unit is the high surrogate of a surrogate pair.
296
+ counter--;
297
+ }
298
+ }
299
+ value = "\\" + codePoint.toString(16).toUpperCase() + " ";
300
+ }
301
+ else {
302
+ if (options.escapeEverything) {
303
+ if (tppRegexAnySingleEscape.test(character)) {
304
+ value = "\\" + character;
305
+ }
306
+ else {
307
+ value = "\\" + codePoint.toString(16).toUpperCase() + " ";
308
+ }
309
+ }
310
+ else if (/[\t\n\f\r\x0B]/.test(character)) {
311
+ value = "\\" + codePoint.toString(16).toUpperCase() + " ";
312
+ }
313
+ else if (character == "\\" ||
314
+ (!isIdentifier &&
315
+ ((character == '"' && quote == character) ||
316
+ (character == "'" && quote == character))) ||
317
+ (isIdentifier && tppRegexSingleEscape.test(character))) {
318
+ value = "\\" + character;
319
+ }
320
+ else {
321
+ value = character;
322
+ }
323
+ }
324
+ output += value;
325
+ }
326
+ if (isIdentifier) {
327
+ if (/^-[-\d]/.test(output)) {
328
+ output = "\\-" + output.slice(1);
329
+ }
330
+ else if (/\d/.test(firstChar)) {
331
+ output = "\\3" + firstChar + " " + output.slice(1);
332
+ }
333
+ }
334
+ // Remove spaces after `\HEX` escapes that are not followed by a hex digit,
335
+ // since they’re redundant. Note that this is only possible if the escape
336
+ // sequence isn’t preceded by an odd number of backslashes.
337
+ output = output.replace(tppRegexExcessiveSpaces, function ($0, $1, $2) {
338
+ if ($1 && $1.length % 2) {
339
+ // It’s not safe to remove the space, so don’t.
340
+ return $0;
341
+ }
342
+ // Strip the space.
343
+ return ($1 || "") + $2;
344
+ });
345
+ if (!isIdentifier && options.wrap) {
346
+ return quote + output + quote;
347
+ }
348
+ return output;
349
+ }
350
+
351
+ function tppSanitiseTagName(tagName) {
352
+ return tagName.includes(':')
353
+ ? tagName.split(":")?.at(0)?.toLocaleLowerCase()
354
+ : tagName.toLowerCase();
355
+ }
356
+
357
+ //Below is custom code for locators
358
+
359
+ /**
360
+ * Get absolute xPath position from dom element
361
+ * xPath position will does not contain any id, class or attribute, etc selector
362
+ * Because, Some page use random id and class. This function should ignore that kind problem, so we're not using any selector
363
+ *
364
+ * @param {Element} element element to get position
365
+ * @returns {String} xPath string
366
+ */
367
+ function tppGetAbsoluteXPath(element) {
368
+ // Selector
369
+ let selector = '';
370
+ // Loop handler
371
+ let foundRoot;
372
+ // Element handler
373
+ let currentElement = element;
374
+
375
+ // Do action until we reach html element
376
+ do {
377
+ // Get element tag name
378
+ let tagName = tppSanitiseTagName(currentElement.tagName);
379
+ // Get parent element
380
+ const parentElement = currentElement.parentElement;
381
+
382
+ // Count children
383
+ if (parentElement.childElementCount > 1) {
384
+ // Get children of parent element
385
+ const parentsChildren = [...parentElement.children];
386
+ // Count current tag
387
+ let tag = [];
388
+ parentsChildren.forEach(child => {
389
+ if (tppSanitiseTagName(child.tagName) === tagName) tag.push(child) // Append to tag
390
+ })
391
+
392
+ // Is only of type
393
+ if (tag.length === 1) {
394
+ // Append tag to selector
395
+ selector = `/${tagName}${selector}`;
396
+ } else {
397
+ // Get position of current element in tag
398
+ const position = tag.indexOf(currentElement) + 1;
399
+ // Append tag to selector
400
+ selector = `/${tagName}[${position}]${selector}`;
401
+ }
402
+
403
+ } else {
404
+ //* Current element has no siblings
405
+ // Append tag to selector
406
+ selector = `/${tagName}${selector}`;
407
+ }
408
+
409
+ // Set parent element to current element
410
+ currentElement = parentElement;
411
+ // Is root
412
+ foundRoot = parentElement.tagName.toLowerCase() === 'html';
413
+ // Finish selector if found root element
414
+ if(foundRoot) selector = `/html${selector}`;
415
+ }
416
+ while (foundRoot === false);
417
+
418
+ // Return selector
419
+ return selector;
420
+ }
421
+
422
+ function tppUniqueSelector(selector) {
423
+ if(selector.includes("\n")) {
424
+ return false;
425
+ }
426
+
427
+ let count;
428
+ try {
429
+ count = tppRootDocument.querySelectorAll(selector).length
430
+ } catch (_e) {
431
+ return false;
432
+ }
433
+ switch (count) {
434
+ case 0:
435
+ return false;
436
+ case 1:
437
+ return true;
438
+ default:
439
+ return false;
440
+ }
441
+ }
442
+
443
+ function tppUniqueXpath(xpath) {
444
+ if(xpath.includes("\n")) {
445
+ return false;
446
+ }
447
+
448
+ let count;
449
+ try {
450
+ count = tppRootDocument
451
+ .evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
452
+ .snapshotLength;
453
+ } catch (_e) {
454
+ return false;
455
+ }
456
+ switch (count) {
457
+ case 0:
458
+ return false;
459
+ case 1:
460
+ return true;
461
+ default:
462
+ return false;
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Score order
468
+ * Id: 400
469
+ * Tag Name: 300
470
+ * Inner text: 200
471
+ * Attributes: 101-199
472
+ * CSS Regex: 1-99
473
+ */
474
+ function tppLocatorScore(locator, preferredLocators) {
475
+ if(!preferredLocators || !locator) return 0;
476
+ if(preferredLocators.id && tppCheckRegexMatch(locator, ['#.*'])) {
477
+ return 400;
478
+ }
479
+ const attrMatch = preferredLocators.attributes?.findIndex(attr => tppCheckRegexMatch(locator, [`\\[${attr}*`]));
480
+ if(attrMatch >= 0) {
481
+ return 101 + attrMatch;
482
+ }
483
+ const cssMatch = preferredLocators.cssRegex?.findIndex(cssReg => tppCheckRegexMatch(locator, [cssReg]));
484
+ if(cssMatch != null && cssMatch >= 0) {
485
+ return 1 + cssMatch;
486
+ }
487
+ return 0;
488
+ }
489
+
490
+ function checkAndAddCss(locator, list, name, score, preferredLocators) {
491
+ if(locator && tppUniqueSelector(locator)) {
492
+ if(!list.find(loc => loc.locator === locator)) {
493
+ if(!score && preferredLocators) {
494
+ score = tppLocatorScore(locator, preferredLocators)
495
+ }
496
+ list.push({locatorType: 'CSS', locator: locator, name: name, score: score || 0});
497
+ }
498
+ }
499
+ }
500
+
501
+ function tppCheckRegexMatch(locator, regexList) {
502
+ let match = regexList?.find(reg => {
503
+ try {
504
+ const regExp = new RegExp(reg);
505
+ return regExp.test(locator);
506
+ } catch (ex) {
507
+ return false;
508
+ }
509
+ });
510
+ return !!match;
511
+ }
512
+
513
+ function tppGetUniqueLocators(element, preferences) {
514
+ const ignoreLocators = preferences?.locatorPreference?.ignoreLocators;
515
+ const preferredLocators = preferences?.locatorPreference?.preferredLocators;
516
+ let uniqueLocators = [];
517
+
518
+ // Init
519
+ const defaults = {
520
+ root: document.body,
521
+ idName: (name) => true,
522
+ className: (name) => true,
523
+ tagName: (name) => true,
524
+ attr: (name, value) => false,
525
+ seedMinLength: 1,
526
+ optimizedMinLength: 2,
527
+ threshold: 1000,
528
+ maxNumberOfTries: 10000,
529
+ };
530
+ tppConfig = Object.assign(Object.assign({}, defaults));
531
+ tppRootDocument = tppFindRootDocument(tppConfig.root, defaults);
532
+
533
+ // ID
534
+ if(!ignoreLocators?.id) {
535
+ const eleId = element.getAttribute("id");
536
+ if(eleId) checkAndAddCss(`#${eleId}`, uniqueLocators, eleId, preferredLocators?.id ? 400 : 0, preferredLocators);
537
+ }
538
+
539
+ // Check if the element has any data attributes
540
+ const dataAttributes = element.dataset;
541
+ if (Object.keys(dataAttributes).length > 0) {
542
+ Object.keys(dataAttributes).forEach(attribute => {
543
+ if(ignoreLocators?.attributes?.includes(`data-${attribute}`)) {
544
+ return;
545
+ }
546
+ const loc = `[data-${attribute}='${dataAttributes[attribute]}']`;
547
+ checkAndAddCss(loc, uniqueLocators, dataAttributes[attribute], 0, preferredLocators);
548
+ });
549
+ }
550
+
551
+ // Attributes
552
+ const defaultAttrs = ['placeholder', 'label', 'name', 'title', 'aria-label', 'alt', 'type'];
553
+ const attributes = [...new Set(defaultAttrs.concat(preferredLocators?.attributes || []))]
554
+ attributes.forEach(attr => {
555
+ if(ignoreLocators?.attributes?.includes(attr)) {
556
+ return;
557
+ }
558
+ const attrValue = element.getAttribute(attr);
559
+ if(attrValue) checkAndAddCss(`[${attr}='${attrValue}']`, uniqueLocators, attrValue, 0, preferredLocators);
560
+ })
561
+
562
+ // Tag name locator
563
+ let tagName;
564
+ if(!ignoreLocators?.tagName) {
565
+ tagName = tppSanitiseTagName(element.tagName)
566
+ if(tagName && tppUniqueSelector(tagName)) {
567
+ uniqueLocators.push({locatorType: 'CSS', locator: tagName, score: preferredLocators?.tagName ? 300 : 0});
568
+ }
569
+ }
570
+
571
+ // Inner text xpath is used only if the node the is the leaf
572
+ if(!ignoreLocators?.innerText) {
573
+ let xpath;
574
+ let label;
575
+ const textContent = element.textContent;
576
+ if (textContent && textContent.length < 50 && !textContent.includes("\n")) {
577
+ xpath = tagName ? `//${tagName}[text()="${textContent}"]` : `//*[text()="${textContent}"]`;
578
+ label = textContent.trim();
579
+ } else {
580
+ const innerText = element.innerText;
581
+ if (innerText && innerText.length < 50) {
582
+ xpath = tagName ? `//${tagName}[normalize-space()="${innerText}"]` : `//*[normalize-space()="${innerText}"]`;
583
+ label = innerText.trim();
584
+ }
585
+ }
586
+ if(xpath && tppUniqueXpath(xpath)) uniqueLocators.push({locatorType: 'XPATH', locator: xpath, name: label, score: preferredLocators?.innerText ? 200 : 0});
587
+ }
588
+
589
+ const ignoreId = ignoreLocators?.id ? ["#*"] : [];
590
+ const ignoreAttrs = ignoreLocators?.attributes?.map(attr => `[${attr}]`) || [];
591
+ const ignoreAttrsWithValue = ignoreLocators?.attributes?.map(attr => `[${attr}=*]`) || [];
592
+ const defaultBlackList = ['style', 'src', 'value']
593
+ .filter(attr => !preferredLocators?.attributes?.includes(attr))
594
+ .map(attr => `[${attr}=*]`);
595
+ const cssSelector = CssSelectorGenerator.getCssSelector(
596
+ element,
597
+ {
598
+ maxCombinations: 10,
599
+ blacklist: [
600
+ ...defaultBlackList,
601
+ ...ignoreLocators?.cssRegex || [],
602
+ ...ignoreId,
603
+ ...ignoreAttrs, ...ignoreAttrsWithValue,
604
+ ],
605
+ },
606
+ );
607
+ if(cssSelector && cssSelector.split('>').length < 4) checkAndAddCss(cssSelector, uniqueLocators, undefined, 0, preferredLocators);
608
+
609
+ let finderCss = tppFinder(element, ignoreLocators)?.trim();
610
+ if(finderCss) checkAndAddCss(finderCss, uniqueLocators, undefined, 0, preferredLocators);
611
+
612
+ // Check if the element has a class
613
+ if (element.classList.length > 0) {
614
+ element.classList.forEach(className => {
615
+ const loc = `.${className}`;
616
+ if(!/\d/.test(loc)) checkAndAddCss(loc, uniqueLocators, undefined, 0, preferredLocators);
617
+ });
618
+ }
619
+
620
+ //Remove ignore regex
621
+ uniqueLocators = uniqueLocators.filter(loc => !tppCheckRegexMatch(loc.locator, ignoreLocators?.cssRegex || []))
622
+ //Sort by score
623
+ uniqueLocators = uniqueLocators.sort((a, b) => (b.score || 0) - (a.score || 0));
624
+
625
+ if(uniqueLocators.length === 0) {
626
+ uniqueLocators.push({locatorType: 'XPATH', locator: tppGetAbsoluteXPath(element)});
627
+ }
628
+
629
+ return uniqueLocators;
630
+ }
631
+
632
+ function tppGetImageName(srcValue) {
633
+ if(!srcValue) return srcValue;
634
+ const parts = srcValue.split('/');
635
+ const file = parts[parts.length -1];
636
+ return file.split('.')[0];
637
+ }
638
+
639
+ const tppElementNameRegex = /[^a-zA-Z0-9_]/g;
640
+ function getTppEventElement(eventElement, eventType, preferences) {
641
+ const allLocators = tppGetUniqueLocators(eventElement, preferences);
642
+ const mainLocator = allLocators[0].locator;
643
+ const elementType = eventElement.getAttribute('role') || eventElement.nodeName;
644
+ const isTextInput = eventType === 'input' || elementType === 'input' || elementType === 'textarea' || elementType === 'textbox'
645
+ const name = (
646
+ allLocators.find(loc => loc?.name && !!loc.name.trim())?.name.replace(tppElementNameRegex, '')
647
+ || (!isTextInput && eventElement.innerText?.trim()?.replace(tppElementNameRegex, ''))
648
+ || eventElement.getAttribute('aria-label')?.replace(tppElementNameRegex, '')
649
+ || eventElement.getAttribute('alt')?.replace(tppElementNameRegex, '')
650
+ || tppGetImageName(eventElement.getAttribute('src'))?.replace(tppElementNameRegex, '')
651
+ || mainLocator?.replace(tppElementNameRegex, '')
652
+ );
653
+ console.log(document.title);
654
+ const tppEvent = {
655
+ target: mainLocator,
656
+ locatorType: allLocators[0].locatorType,
657
+ backupLocators: allLocators.length > 1 ? allLocators.slice(1) : [],
658
+ elementType: elementType,
659
+ inputType: eventElement.getAttribute('type') || '',
660
+ name: name,
661
+ value: eventElement.value,
662
+ page: (document.title || document.location?.pathname || document.href?.split("/")?.at(-1))?.replaceAll(' ', ''),
663
+ time: Date.now(),
664
+ innerText: eventElement.innerText?.replaceAll('\n', '\\n') || '',
665
+ };
666
+ return tppEvent;
667
+ }
668
+
669
+ function isTppFileInputEvent(eventElement, eventType) {
670
+ return eventType === 'input'
671
+ && eventElement.nodeName.toLowerCase() === 'input'
672
+ && eventElement.getAttribute('type') === 'file'
673
+ }
674
+
675
+ function isTppFileInputClickEvent(eventElement, eventType) {
676
+ return eventType === 'click'
677
+ && eventElement.nodeName.toLowerCase() === 'input'
678
+ && eventElement.getAttribute('type') === 'file'
679
+ }
680
+
681
+ function getTppEvent(event, eventType, preferences) {
682
+ const eventElement = event.target;
683
+ if(eventType === 'pointermove'
684
+ && (
685
+ eventElement.tagName?.trim()?.toLowerCase() !== 'canvas'
686
+ || !event.pressure //pressure is not zero when clicked and moved
687
+ )
688
+ ) {
689
+ return undefined;
690
+ }
691
+ const tppEvent = {
692
+ ...getTppEventElement(eventElement, eventType, preferences),
693
+ action: eventType,
694
+ inputActionType: event.inputType,
695
+ inputData: event.data,
696
+ key: event.key,
697
+ keyCode: event.code,
698
+ ctrlKey: event.ctrlKey,
699
+ altKey: event.altKey,
700
+ metaKey: event.metaKey,
701
+ shiftKey: event.shiftKey,
702
+ };
703
+ if(isTppFileInputClickEvent(eventElement, eventType)) {
704
+ tppEvent.message = 'Click on file input might not be needed for uploading files. Please check and remove step after saving test!';
705
+ } else if(isTppFileInputEvent(eventElement, eventType)) {
706
+ tppEvent.action = 'file_input';
707
+ const files = Array.from(eventElement.files, file => file.name);
708
+ tppEvent.value = JSON.stringify(files);
709
+ tppEvent.message = 'Update to full file paths after saving test!';
710
+ }
711
+ if(eventType === 'pointermove') { //Pressure and element tag are filtered above
712
+ tppEvent.action = 'click_and_move_on_element';
713
+ const {x: elementX, y: elementY} = eventElement.getBoundingClientRect();
714
+ tppEvent.payload = {
715
+ x: Math.floor(event.x - elementX),
716
+ y: Math.floor(event.y - elementY),
717
+ diff: {x: event.movementX, y: event.movementY},
718
+ }
719
+ }
720
+ if((eventType === 'click' || eventType === 'contextmenu') && eventElement.tagName?.trim()?.toLowerCase() === 'canvas') {
721
+ tppEvent.action = eventType === 'contextmenu'
722
+ ? 'right_click_at_point_on_element'
723
+ : 'click_at_point_on_element';
724
+ const {x: elementX, y: elementY} = eventElement.getBoundingClientRect();
725
+ tppEvent.payload = {x: Math.floor(event.x - elementX), y: Math.floor(event.y - elementY)};
726
+ }
727
+ return tppEvent;
728
+ }
729
+
730
+ function tppOnPageLoad(tabId, windowId) {
731
+ const perfEntries = performance.getEntriesByType('navigation');
732
+ if (perfEntries.length > 0) {
733
+ const navigationEntry = perfEntries[0];
734
+ if (navigationEntry.type !== 'navigate' || !document.referrer) {
735
+ const newUrl = window.location.href;
736
+ if(newUrl.includes('chrome://newtab') || newUrl.includes('edge://newtab')) {
737
+ return;
738
+ }
739
+ const payload = {
740
+ action: 'open_url',
741
+ payload: {
742
+ url: newUrl,
743
+ navigationType: navigationEntry.type,
744
+ },
745
+ windowId: windowId,
746
+ tabId: tabId,
747
+ };
748
+ window.onTppEvent ? window.onTppEvent(`[[TPP Event]] ${JSON.stringify(payload)}`)
749
+ : console.error(`[[TPP Event]] ${JSON.stringify(payload)}`);
750
+ }
751
+ }
752
+ }