@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.
- package/README.md +18 -2
- package/index.js +121 -0
- package/package.json +30 -4
- package/src/accessibility/combined-a11y-report.js +208 -0
- package/src/accessibility/tpp-a11y.js +81 -0
- package/src/actions.js +3201 -0
- package/src/api/api-auth.js +18 -0
- package/src/api/api-service.js +289 -0
- package/src/app-config.json +7 -0
- package/src/autoheal-actions.js +309 -0
- package/src/console-log/console-log-manager.js +39 -0
- package/src/console-log/console-log-utils.js +201 -0
- package/src/database/mysql/mysql-service.js +23 -0
- package/src/database/neo4j/neo4j-service.js +32 -0
- package/src/database/oracle/oracle-service.js +48 -0
- package/src/database/pg/pg-service.js +29 -0
- package/src/downloads/downloads-manager.js +86 -0
- package/src/downloads/downloads-utils.js +113 -0
- package/src/globalsetup.js +68 -0
- package/src/globalteardown.js +57 -0
- package/src/hooks/each-hook.js +45 -0
- package/src/hooks/hook-utils.js +21 -0
- package/src/library/authenticator/authenticator-utils.js +43 -0
- package/src/library/csv/csv-utils.js +297 -0
- package/src/library/date/date-utils.js +168 -0
- package/src/library/emailTesting/TokenConfig.js +46 -0
- package/src/library/emailTesting/emailTesting-utils.js +527 -0
- package/src/library/excel/excel-utils.js +922 -0
- package/src/library/file/file-action-utils.js +129 -0
- package/src/library/file/file-utils.js +127 -0
- package/src/library/json/json-utils.js +104 -0
- package/src/library/math/math-utils.js +283 -0
- package/src/library/softAssert/softAssert-utils.js +154 -0
- package/src/library/table/table-utils.js +92 -0
- package/src/library/text/text-utils.js +45 -0
- package/src/library/tpp-library.js +81 -0
- package/src/library/web/slider-utils.js +66 -0
- package/src/mobile/appium/appium-manager.js +158 -0
- package/src/mobile/device-cloud/browserstack-service.js +44 -0
- package/src/mobile/device-cloud/commands/browserstack-commands.js +20 -0
- package/src/mobile/device-cloud/commands/device-cloud-commands.js +17 -0
- package/src/mobile/device-cloud/device-cloud-service.js +29 -0
- package/src/mobile/device-cloud/pcloudy-service.js +65 -0
- package/src/mobile/mobile-actions.js +1568 -0
- package/src/mobile/recorder-utils/css-selector-generator.js +2 -0
- package/src/mobile/recorder-utils/finder.js +752 -0
- package/src/mobile/recorder-utils/mobile-recorder.js +266 -0
- package/src/mobile/web-driver-utils.js +110 -0
- package/src/models/tpp-element.js +58 -0
- package/src/models/tpp-mobile-element.js +50 -0
- package/src/network-log/network-log-manager.js +96 -0
- package/src/network-log/network-log-utils.js +361 -0
- package/src/types/elements.types.js +24 -0
- package/src/utils/ai-utils.js +48 -0
- package/src/utils/assert-utils.js +50 -0
- package/src/utils/autoheal-utils.js +9 -0
- package/src/utils/common-utils.js +120 -0
- package/src/utils/cookie-utils.js +70 -0
- package/src/utils/json-utils.js +10 -0
- package/src/utils/log-utils.js +10 -0
- package/src/utils/logger.js +76 -0
- package/src/utils/screenshot-utils.js +42 -0
- package/src/utils/token-utils.js +18 -0
- package/src/utils/vectordb-utils.js +125 -0
- package/src/visual-test/tpp-visual-test.js +122 -0
- package/src/web/web-executor-agent.js +149 -0
- package/src/web/web-executor-data-fetch.js +353 -0
- 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;
|