@devassure/tpp-engine 0.0.0-stage → 1.8.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 (68) hide show
  1. package/README.md +18 -2
  2. package/index.js +121 -0
  3. package/package.json +30 -4
  4. package/src/accessibility/combined-a11y-report.js +208 -0
  5. package/src/accessibility/tpp-a11y.js +81 -0
  6. package/src/actions.js +3201 -0
  7. package/src/api/api-auth.js +18 -0
  8. package/src/api/api-service.js +289 -0
  9. package/src/app-config.json +7 -0
  10. package/src/autoheal-actions.js +309 -0
  11. package/src/console-log/console-log-manager.js +39 -0
  12. package/src/console-log/console-log-utils.js +201 -0
  13. package/src/database/mysql/mysql-service.js +23 -0
  14. package/src/database/neo4j/neo4j-service.js +32 -0
  15. package/src/database/oracle/oracle-service.js +48 -0
  16. package/src/database/pg/pg-service.js +29 -0
  17. package/src/downloads/downloads-manager.js +86 -0
  18. package/src/downloads/downloads-utils.js +113 -0
  19. package/src/globalsetup.js +68 -0
  20. package/src/globalteardown.js +57 -0
  21. package/src/hooks/each-hook.js +45 -0
  22. package/src/hooks/hook-utils.js +21 -0
  23. package/src/library/authenticator/authenticator-utils.js +43 -0
  24. package/src/library/csv/csv-utils.js +297 -0
  25. package/src/library/date/date-utils.js +168 -0
  26. package/src/library/emailTesting/TokenConfig.js +46 -0
  27. package/src/library/emailTesting/emailTesting-utils.js +527 -0
  28. package/src/library/excel/excel-utils.js +922 -0
  29. package/src/library/file/file-action-utils.js +129 -0
  30. package/src/library/file/file-utils.js +127 -0
  31. package/src/library/json/json-utils.js +104 -0
  32. package/src/library/math/math-utils.js +283 -0
  33. package/src/library/softAssert/softAssert-utils.js +154 -0
  34. package/src/library/table/table-utils.js +92 -0
  35. package/src/library/text/text-utils.js +45 -0
  36. package/src/library/tpp-library.js +81 -0
  37. package/src/library/web/slider-utils.js +66 -0
  38. package/src/mobile/appium/appium-manager.js +158 -0
  39. package/src/mobile/device-cloud/browserstack-service.js +44 -0
  40. package/src/mobile/device-cloud/commands/browserstack-commands.js +20 -0
  41. package/src/mobile/device-cloud/commands/device-cloud-commands.js +17 -0
  42. package/src/mobile/device-cloud/device-cloud-service.js +29 -0
  43. package/src/mobile/device-cloud/pcloudy-service.js +65 -0
  44. package/src/mobile/mobile-actions.js +1568 -0
  45. package/src/mobile/recorder-utils/css-selector-generator.js +2 -0
  46. package/src/mobile/recorder-utils/finder.js +752 -0
  47. package/src/mobile/recorder-utils/mobile-recorder.js +266 -0
  48. package/src/mobile/web-driver-utils.js +110 -0
  49. package/src/models/tpp-element.js +58 -0
  50. package/src/models/tpp-mobile-element.js +50 -0
  51. package/src/network-log/network-log-manager.js +96 -0
  52. package/src/network-log/network-log-utils.js +361 -0
  53. package/src/types/elements.types.js +24 -0
  54. package/src/utils/ai-utils.js +48 -0
  55. package/src/utils/assert-utils.js +50 -0
  56. package/src/utils/autoheal-utils.js +9 -0
  57. package/src/utils/common-utils.js +120 -0
  58. package/src/utils/cookie-utils.js +70 -0
  59. package/src/utils/json-utils.js +10 -0
  60. package/src/utils/log-utils.js +10 -0
  61. package/src/utils/logger.js +76 -0
  62. package/src/utils/screenshot-utils.js +42 -0
  63. package/src/utils/token-utils.js +18 -0
  64. package/src/utils/vectordb-utils.js +125 -0
  65. package/src/visual-test/tpp-visual-test.js +122 -0
  66. package/src/web/web-executor-agent.js +149 -0
  67. package/src/web/web-executor-data-fetch.js +353 -0
  68. package/test-excel-utils.js +570 -0
@@ -0,0 +1,353 @@
1
+ const { getScreenshotPath } = require("../utils/screenshot-utils");
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const fetchWebState = async (page) => {
6
+
7
+ let filePath = '';
8
+ try {
9
+ allElementsData = await page.evaluate(() => {
10
+
11
+ const isInteractive = (element) => {
12
+ const interactiveTags = new Set([
13
+ 'a', 'button', 'input', 'label', 'menu', 'select', 'textarea',
14
+ ]);
15
+ const interactiveRoles = new Set([
16
+ 'button', 'checkbox', 'combobox', 'link', 'list', 'listbox', 'listitem', 'menu', 'menuitem',
17
+ 'option', 'progressbar', 'radio', 'search', 'searchbox','switch', 'tab', 'textbox', 'toolbar',
18
+ ]);
19
+
20
+ const style = window.getComputedStyle(element);
21
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
22
+
23
+ //TODO check size conditions
24
+ if (element.offsetWidth === 0 || element.offsetHeight === 0) return false;
25
+
26
+ return element.hasAttribute('tabIndex')
27
+ || interactiveTags.has(element.tagName.toLowerCase())
28
+ || interactiveRoles.has(element.getAttribute('role')?.toLowerCase())
29
+ || element.hasAttribute('onclick')
30
+ || hasEventListener(element)
31
+ || hasAttatchedEvents(element);
32
+ };
33
+
34
+ function hasEventListener(element) {
35
+ // Check if getEventListeners is available (Chrome DevTools only)
36
+ if (typeof getEventListeners === 'function') {
37
+ const events = getEventListeners(element);
38
+ if (Object.keys(events).length > 0) {
39
+ return true;
40
+ }
41
+ }
42
+
43
+ // Check for inline event handler (e.g., onclick)
44
+ if (element.onclick) {
45
+ return true;
46
+ }
47
+
48
+ return false;
49
+ };
50
+
51
+ function hasAttatchedEvents(element) {
52
+
53
+ function hasEventListeners(element) {
54
+ const eventTypes = ['click', 'mousedown', 'mouseup', 'touchstart', 'touchend', 'focus', 'blur', 'keydown', 'keyup'];
55
+ for (const eventType of eventTypes) {
56
+ const listeners = element[`on${eventType}`];
57
+ if (listeners) {
58
+ return true;
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+
64
+ return element.onclick !== null ||
65
+ element.getAttribute('onclick') !== null ||
66
+ element.hasAttribute('ng-click') ||
67
+ element.hasAttribute('@click') ||
68
+ element.hasAttribute('v-on:click') || hasEventListeners(element);
69
+ }
70
+
71
+ //TODO! Adding this causes noise as multiple nested elements are included.
72
+ //Check how to sanitise before adding this condition.
73
+ function hasCursorPointer(element) {
74
+ const computedStyle = window.getComputedStyle(element);
75
+ return computedStyle?.cursor === 'pointer';
76
+ };
77
+
78
+ function isElementVisible(el) {
79
+ const style = window.getComputedStyle(el);
80
+ const tagName = el.tagName.toLowerCase();
81
+ // Check for hidden styles and dimensions
82
+ return el.checkVisibility() &&
83
+ style.display !== 'none' &&
84
+ style.visibility !== 'hidden' &&
85
+ (tagName === 'select' || tagName === 'input' || parseFloat(style.opacity) > 0.1)
86
+ }
87
+
88
+ function isElementInViewport(el) {
89
+ const rect = el.getBoundingClientRect();
90
+ return (
91
+ rect.top >= 0 &&
92
+ rect.left >= 0 &&
93
+ rect.top <= window.innerHeight &&
94
+ rect.left <= window.innerWidth
95
+ );
96
+ }
97
+
98
+ function isElementNotCovered(el, visibilityThreshold = 0.5, sampleDensity = 4) {
99
+ const rect = el.getBoundingClientRect();
100
+ const stepX = Math.max(2, rect.width / sampleDensity);
101
+ const stepY = Math.max(2, rect.height / sampleDensity);
102
+
103
+ let visiblePoints = 0;
104
+ let totalPoints = 0;
105
+
106
+ for (let y = rect.top; y <= rect.bottom; y += stepY) {
107
+ for (let x = rect.left; x <= rect.right; x += stepX) {
108
+ const elementsAtPoint = document.elementsFromPoint(x, y);
109
+
110
+ if (elementsAtPoint.length === 0) continue;
111
+
112
+ const topElement = elementsAtPoint[0];
113
+
114
+ if (topElement !== el && !isInteractive(topElement)) {
115
+ return true; // If the top element is non-interactive and not 'el', it's covered
116
+ }
117
+
118
+ // Check if the first interactive element is this element
119
+ const firstInteractive = elementsAtPoint.find(isInteractive);
120
+ if (firstInteractive === el) {
121
+ visiblePoints++;
122
+ }
123
+
124
+ totalPoints++;
125
+ }
126
+ }
127
+
128
+ return totalPoints > 0 && (visiblePoints / totalPoints) >= visibilityThreshold;
129
+ }
130
+
131
+ const getAccuratePosition = (el) => {
132
+ const rect = el.getBoundingClientRect();
133
+ let x = Math.round(rect.left + rect.width / 2);
134
+ let y = Math.round(rect.top + rect.height / 2);
135
+
136
+ // Handle inline elements (e.g., word-wrapped links)
137
+ if (el.tagName.toLowerCase() === 'a') {
138
+ const rects = el.getClientRects();
139
+ if (rects.length > 0) {
140
+ x = Math.round(rects[0].left + rects[0].width / 2); // First line’s middle position
141
+ y = Math.round(rects[0].top + rects[0].height / 2); // First line’s middle position
142
+ }
143
+ }
144
+
145
+ return { x, y, width: rect.width, height: rect.height, top: rect.top, left: rect.left, right: rect.right, bottom: rect.bottom };
146
+ };
147
+
148
+ // Filter and sort elements by visual importance
149
+ const filterInteractiveElements = (allElements) =>
150
+ Array.from(allElements).filter(el => (isInteractive(el) && isElementInViewport(el) && isElementVisible(el) && isElementNotCovered(el)));
151
+
152
+ let allElementsData = [];
153
+ const allElements = document.querySelectorAll('*');
154
+ const interactiveElements = filterInteractiveElements(allElements);
155
+ for(let index=0; index < interactiveElements.length; index++) {
156
+ let el = interactiveElements[index];
157
+ const rect = el.getBoundingClientRect();
158
+ const tagName = el.tagName.toLowerCase();
159
+ const role = el.getAttribute('role') || undefined;
160
+ let placeHolderProp = el.getAttribute('placeholder') ? {placeholder: el.getAttribute('placeholder')} : {};
161
+ let labelProp = el.getAttribute('label') ? {label: el.getAttribute('label')} : {};
162
+ let ariaLabelProp = el.getAttribute('aria-label') ? {ariaLabel: el.getAttribute('aria-label')} : {};
163
+ let inputType = el.getAttribute('type') ? {buttonType: el.getAttribute('type')} : {};
164
+
165
+ // Color coding system
166
+ const colors = ['#0066cc', '#00aa00', '#ff9900', '#6600cc', '#cc0000']
167
+ let color = colors[index % colors.length];
168
+
169
+ // Add highlight border
170
+ el.setAttribute('tpp-data-border', el.style.border);
171
+ el.setAttribute('tpp-data-border-radius', el.style.borderRadius);
172
+ el.style.border = `2px solid ${color}`;
173
+ el.style.borderRadius = '3px';
174
+
175
+ // Create index badge
176
+ const label = document.createElement('div');
177
+
178
+ const labelWidth = 30;
179
+ const labelHeight = 20;
180
+
181
+ // Determine if the element is "large enough" for inner placement
182
+ const isLargeElement = rect.width > 100 && rect.height > 50;
183
+
184
+ if (isLargeElement) {
185
+ // Place the label inside the element (inner-right)
186
+ label.style.left = `${rect.right - labelWidth}px`;
187
+ label.style.top = `${rect.top}px`;
188
+ } else {
189
+ // Place it outside but keep it within the viewport
190
+ const adjustedLeft = Math.min(rect.right, window.innerWidth - labelWidth);
191
+ label.style.left = `${adjustedLeft}px`;
192
+
193
+ // Ensure it doesn’t overflow the bottom of the screen
194
+ const adjustedTop = Math.min(rect.top, window.innerHeight - labelHeight);
195
+ label.style.top = `${adjustedTop}px`;
196
+ }
197
+
198
+ label.textContent = index;
199
+ label.style.position = 'absolute';
200
+ label.style.color = '#ffffff';
201
+ label.style.fontSize = '12px';
202
+ label.style.fontWeight = 'bold';
203
+ label.style.zIndex = '10000';
204
+ label.style.backgroundColor = color;
205
+ label.style.padding = '2px 8px';
206
+ label.style.borderRadius = '4px';
207
+ label.setAttribute('tpp-data-index-label', `${index}`);
208
+ document.body.appendChild(label);
209
+
210
+ let elementData = {
211
+ index,
212
+ type: tagName,
213
+ role,
214
+ text: el.textContent?.slice(0, 50) || '',
215
+ ...getAccuratePosition(el),
216
+ ...placeHolderProp,
217
+ ...labelProp,
218
+ ...ariaLabelProp,
219
+ ...inputType,
220
+ value: el.value,
221
+ }
222
+
223
+ // Add all aria attributes
224
+ const ariaAttributes = Array.from(el.attributes)
225
+ .filter(attr => attr.name.includes('aria'))
226
+ .reduce((acc, attr) => {
227
+ acc[attr.name] = attr.value;
228
+ return acc;
229
+ }, {});
230
+ elementData = { ...elementData, ...ariaAttributes };
231
+
232
+ // Store element metadata
233
+ allElementsData.push(elementData);
234
+ }
235
+ return allElementsData;
236
+ });
237
+
238
+ filePath = getScreenshotPath(`web_executor_agent_snap_${Date.now()}`);
239
+ await page.screenshot({ animations: 'disabled', path: filePath, timeout: 5000, });
240
+
241
+ } catch (ex) {
242
+ throw new Error(`Failed to fetch web state: ${ex.message}`);
243
+ } finally {
244
+ //Remove styles
245
+ await page.evaluate(() => {
246
+ document.querySelectorAll('[tpp-data-border]').forEach(el => {
247
+ el.style.border = el.getAttribute('tpp-data-border') || '';
248
+ el.style.borderRadius = el.getAttribute('tpp-data-border-radius') || '';
249
+ el.removeAttribute('tpp-data-border');
250
+ el.removeAttribute('tpp-data-border-radius');
251
+ });
252
+
253
+ document.querySelectorAll('[tpp-data-index-label]').forEach(el => el.remove());
254
+ })
255
+ }
256
+
257
+ return {
258
+ screenshot: filePath ? path.resolve(filePath) : '',
259
+ elementsData: allElementsData,
260
+ }
261
+
262
+ };
263
+
264
+ async function getLocatorForScreenshot(page, tppEvent) {
265
+ if (!tppEvent.target) tppEvent.target = tppEvent.locator
266
+ let mainLocator = tppEvent.target.replace('${rowId}', tppEvent.params?.rowId || 1).replace('${colId}', tppEvent.params?.colId || 1);
267
+ let elementHandle;
268
+ try {
269
+ elementHandle = await page.$(mainLocator);
270
+ } catch (e) {
271
+ try {
272
+ elementHandle = await page.$(`xpath=${mainLocator}`);
273
+ } catch (e) {}
274
+ }
275
+
276
+ if (elementHandle) return elementHandle;
277
+
278
+ for (let backup of tppEvent?.backupLocators || []) {
279
+ let backupLocator = backup.locator.replace('${rowId}', tppEvent.params?.rowId || 1).replace('${colId}', tppEvent.params?.colId || 1);
280
+ try {
281
+ elementHandle = await page.$(backupLocator);
282
+ } catch (e) {
283
+
284
+ try {
285
+ elementHandle = await page.$(`xpath=${backupLocator}`);
286
+ } catch (e) {}
287
+ }
288
+ if (elementHandle) {
289
+ return elementHandle;
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+
295
+ async function takeElementScreenshot(page, locator, targetPath, id, logger) {
296
+ try {
297
+ const screenshotPath = path.join(targetPath, 'autoheal_snaps', `${id}.png`);
298
+ const dir = path.dirname(screenshotPath);
299
+ if (!fs.existsSync(dir)) {
300
+ fs.mkdirSync(dir, { recursive: true });
301
+ }
302
+ const elementHandle = await getLocatorForScreenshot(page, locator);
303
+ const attrs = await page.evaluate((element) => {
304
+ const eventElement = window.getTppEventElement
305
+ ? window.getTppEventElement(element, 'click', {})
306
+ : null;
307
+ return eventElement.attrs || "";
308
+ }, elementHandle);
309
+ if (!elementHandle) {
310
+ return null;
311
+ }
312
+ const boundingBox = await elementHandle.boundingBox();
313
+ if (!boundingBox) return;
314
+ await page.evaluate((box) => {
315
+ const overlay = document.createElement('div');
316
+ overlay.id = 'tpp-data-screenshot-overlay';
317
+ overlay.style.cssText = `
318
+ position: fixed;
319
+ left: ${box.x}px;
320
+ top: ${box.y}px;
321
+ width: ${box.width}px;
322
+ height: ${box.height}px;
323
+ border: 3px dashed #ff1493;
324
+ background: rgba(255, 20, 147, 0.1);
325
+ pointer-events: none;
326
+ z-index: 9999;
327
+ box-sizing: border-box;
328
+ `;
329
+ document.body.appendChild(overlay);
330
+ }, boundingBox);
331
+ // scroll the eleemnt handle into view
332
+ await elementHandle.scrollIntoViewIfNeeded();
333
+ const cdpSession = await page.context().newCDPSession(page);
334
+ await cdpSession.send('Page.enable');
335
+ await cdpSession.send('Runtime.enable');
336
+ const screenshot = await cdpSession.send('Page.captureScreenshot', {
337
+ format: 'png',
338
+ });
339
+ await page.evaluate(() => {
340
+ const overlay = document.getElementById('tpp-data-screenshot-overlay');
341
+ if(overlay) overlay.remove();
342
+ });
343
+ fs.writeFileSync(screenshotPath, screenshot.data, 'base64');
344
+ logger.log(`[AUTOHEAL_DATA] ${JSON.stringify({ screenshotPath, element: locator.label, testLabel: logger.testLabel, attrs, })}`);
345
+ await elementHandle.dispose();
346
+ return screenshotPath;
347
+ } catch (err) {
348
+ return null;
349
+ }
350
+ }
351
+
352
+ exports.fetchWebState = fetchWebState;
353
+ exports.takeElementScreenshot = takeElementScreenshot;