@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.
- package/DEV_README.md +19 -0
- package/README.md +6 -3
- 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,266 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { FileUtils } = require('../../library/file/file-utils');
|
|
4
|
+
const { getScreenshotPath } = require('../../utils/screenshot-utils');
|
|
5
|
+
const { sleep } = require('../../utils/common-utils');
|
|
6
|
+
|
|
7
|
+
const EVENT_TAG = "[[TPP_EVENT_LOG]]";
|
|
8
|
+
|
|
9
|
+
class MobileRecorderControl {
|
|
10
|
+
|
|
11
|
+
constructor(mobileActions) {
|
|
12
|
+
this.mobileActions = mobileActions;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async openMobileRecorder() {
|
|
16
|
+
console.log(`${EVENT_TAG} [[TPP Mobile Opened]]`)
|
|
17
|
+
await this.printMobileState();
|
|
18
|
+
|
|
19
|
+
const commandFilePath = `.mobile-recorder-commands/commands_${Date.now()}`;
|
|
20
|
+
new FileUtils().writeTextContent(commandFilePath, 'ready')
|
|
21
|
+
console.log(`${EVENT_TAG} [[TPP Mobile Recorder FILE]] ${path.resolve(commandFilePath)}`);
|
|
22
|
+
|
|
23
|
+
async function readCommandFromFile() {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const checkInterval = setInterval(() => {
|
|
26
|
+
if (fs.existsSync(commandFilePath)) {
|
|
27
|
+
const command = fs.readFileSync(commandFilePath, 'utf-8').trim();
|
|
28
|
+
if (command !== 'ready') {
|
|
29
|
+
console.log(`Received command: ${command}`);
|
|
30
|
+
clearInterval(checkInterval);
|
|
31
|
+
resolve(command);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
resolve('stop_recording');
|
|
35
|
+
}
|
|
36
|
+
}, 500); // Check every 500ms
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
while (true) {
|
|
41
|
+
console.log('Waiting for a new command...');
|
|
42
|
+
const command = await readCommandFromFile();
|
|
43
|
+
|
|
44
|
+
if (command === 'stop_recording') {
|
|
45
|
+
console.log('Stopping recording...');
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fs.writeFileSync(commandFilePath, 'ready');
|
|
50
|
+
try {
|
|
51
|
+
await this.processMobileCommand(command);
|
|
52
|
+
} catch(error) {
|
|
53
|
+
this.mobileActions.log(undefined, `[RECORDER-ERROR] ${error?.message || 'Error performing action on device!'}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (fs.existsSync(commandFilePath)) { fs.unlinkSync(commandFilePath); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getWebViewDom() {
|
|
61
|
+
|
|
62
|
+
const finderScript = fs.readFileSync(path.resolve(__dirname, 'finder.js'), 'utf-8');
|
|
63
|
+
const cssSelectorScript = fs.readFileSync(path.resolve(__dirname, 'css-selector-generator.js'), 'utf-8');
|
|
64
|
+
|
|
65
|
+
const structuredTree = await this.mobileActions.driver.executeScript(`
|
|
66
|
+
${cssSelectorScript}
|
|
67
|
+
|
|
68
|
+
${finderScript}
|
|
69
|
+
|
|
70
|
+
function processNode(node, keyPrefix = 'da-node') {
|
|
71
|
+
if (!node || node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
72
|
+
|
|
73
|
+
// Extract attributes and bounds
|
|
74
|
+
const attributes = Array.from(node.attributes || []).reduce((acc, attr) => {
|
|
75
|
+
acc[attr.name] = attr.value;
|
|
76
|
+
return acc;
|
|
77
|
+
}, {});
|
|
78
|
+
|
|
79
|
+
// Calculate bounds for the element
|
|
80
|
+
const getWebBounds = (el) => {
|
|
81
|
+
// const rect = el.getBoundingClientRect();
|
|
82
|
+
// return rect.width > 0 && rect.height > 0
|
|
83
|
+
// ? \`[\${rect.left},\${rect.top}][\${rect.left + rect.width},\${rect.top + rect.height}]\`
|
|
84
|
+
// : '';
|
|
85
|
+
return ''; //TODO fix
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Recursive processing of children
|
|
89
|
+
const children = Array.from(node.children || []).map((child, index) =>
|
|
90
|
+
processNode(child, \`\${keyPrefix}-\${index}\`)
|
|
91
|
+
).filter(child => child); // Filter out null/undefined children
|
|
92
|
+
|
|
93
|
+
const allLocators = tppGetUniqueLocators(node);
|
|
94
|
+
|
|
95
|
+
// Determine label
|
|
96
|
+
let label = allLocators.find(loc => loc?.name && !!loc.name.trim())?.name.replace(tppElementNameRegex, '');
|
|
97
|
+
if(!label) {
|
|
98
|
+
const innerText = node.textContent.trim();
|
|
99
|
+
if (children.length === 0 && innerText && innerText.length < 50 && !innerText.includes('\\n')) {
|
|
100
|
+
label = innerText;
|
|
101
|
+
} else {
|
|
102
|
+
label = attributes['aria-label'] || attributes['alt'] || attributes['src'] || node.tagName.toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Replace special characters with underscores
|
|
107
|
+
label = label.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
key: \`\${keyPrefix}-\${node.tagName.toLowerCase()}\`,
|
|
111
|
+
name: node.tagName || 'Element',
|
|
112
|
+
data: attributes,
|
|
113
|
+
locator: allLocators[0]?.locator,
|
|
114
|
+
locatorType: allLocators[0]?.locatorType,
|
|
115
|
+
bounds: getWebBounds(node),
|
|
116
|
+
links: children.length > 0 ? children : undefined,
|
|
117
|
+
url: '',
|
|
118
|
+
isExpanded: false,
|
|
119
|
+
label: label,
|
|
120
|
+
innerText: children.length > 0 ? '' : node.textContent.trim() || '',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Start processing from the <body> element
|
|
125
|
+
const bodyElement = document.querySelector('body');
|
|
126
|
+
return processNode(bodyElement);
|
|
127
|
+
`, []);
|
|
128
|
+
|
|
129
|
+
new FileUtils().writeTextContent('temp-dom.json', JSON.stringify(structuredTree, null, 4));
|
|
130
|
+
|
|
131
|
+
return structuredTree;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getCurrentPage() {
|
|
135
|
+
if(this.mobileActions.config?.os === 'ios') {
|
|
136
|
+
const context = await this.mobileActions.driver.execute('mobile: deviceInfo', {});
|
|
137
|
+
const viewController = context?.viewController;
|
|
138
|
+
const pageName = viewController?.trim()
|
|
139
|
+
?.replaceAll('ViewController', '')
|
|
140
|
+
?.replaceAll('Controller', '');
|
|
141
|
+
return pageName?.replace(/[^a-zA-Z0-9]/g, "") || 'Unknown';
|
|
142
|
+
}
|
|
143
|
+
const activity = await this.mobileActions.driver.getCurrentActivity();
|
|
144
|
+
const pageName = activity?.trim()
|
|
145
|
+
?.split('.')?.at(-1)
|
|
146
|
+
?.replaceAll('Activity', '');
|
|
147
|
+
return pageName?.replace(/[^a-zA-Z0-9]/g, "") || 'Unknown';
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
async isLandscape() {
|
|
151
|
+
const {height, width} = await this.mobileActions.driver.getWindowSize();
|
|
152
|
+
return width >= height;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async printMobileState(isWebView, retry) {
|
|
156
|
+
try {
|
|
157
|
+
const pageSource = isWebView
|
|
158
|
+
? await this.getWebViewDom()
|
|
159
|
+
: await this.mobileActions.driver.getPageSource();
|
|
160
|
+
if(!pageSource || (typeof pageSource === 'string' && !pageSource.trim())) {
|
|
161
|
+
throw {message: 'Unable to fetch page source from device!'}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const filePath = getScreenshotPath(`recorder_snap_${Date.now()}`);
|
|
165
|
+
await this.mobileActions.addScreenshot(filePath);
|
|
166
|
+
const mobileState = {
|
|
167
|
+
pageSource,
|
|
168
|
+
screenshot: path.resolve(filePath),
|
|
169
|
+
isWebView: isWebView,
|
|
170
|
+
currentPage: await this.getCurrentPage(),
|
|
171
|
+
os: this.mobileActions.config?.os,
|
|
172
|
+
landscape: await this.isLandscape(),
|
|
173
|
+
}
|
|
174
|
+
console.log(`${EVENT_TAG} [[TPP Mobile State]] ${JSON.stringify(mobileState)}`);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
let errorMsg = `Error reading mobile state! ${err?.message || err}`;
|
|
177
|
+
if(retry && retry > 3) {
|
|
178
|
+
throw {message: errorMsg};
|
|
179
|
+
} else {
|
|
180
|
+
console.log(errorMsg);
|
|
181
|
+
await sleep(2000);
|
|
182
|
+
this.printMobileState(isWebView, retry ? retry + 1 : 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//TODO add try catch and throw appropriate error message
|
|
188
|
+
async processMobileCommand(command) {
|
|
189
|
+
let isWebView = false;
|
|
190
|
+
if(!command?.trim()) {
|
|
191
|
+
return;
|
|
192
|
+
} else if(command.trim().startsWith('tap_on_element')) {
|
|
193
|
+
const data = JSON.parse(command.split('tap_on_element').at(-1).trim());
|
|
194
|
+
await this.mobileActions.tap_on_element(data.element);
|
|
195
|
+
await sleep(500); //Wait for UI to load after tap
|
|
196
|
+
isWebView = !!data?.isWebView;
|
|
197
|
+
} else if(command.trim().startsWith('input_value')) {
|
|
198
|
+
const data = JSON.parse(command.split('input_value').at(-1).trim());
|
|
199
|
+
await this.mobileActions.input_value(data.value || '', data.element);
|
|
200
|
+
await sleep(100);
|
|
201
|
+
isWebView = !!data?.isWebView;
|
|
202
|
+
} else if(command.trim().startsWith('clear_text')) {
|
|
203
|
+
const data = JSON.parse(command.split('clear_text').at(-1).trim());
|
|
204
|
+
await this.mobileActions.clear_text(data.element);
|
|
205
|
+
await sleep(100);
|
|
206
|
+
isWebView = !!data?.isWebView;
|
|
207
|
+
} else if(command.trim().startsWith('vertical_scroll_element')) {
|
|
208
|
+
const data = JSON.parse(command.split('vertical_scroll_element').at(-1).trim());
|
|
209
|
+
await this.mobileActions.vertical_scroll_element(data.element);
|
|
210
|
+
await sleep(500); //Wait for UI to load after scroll
|
|
211
|
+
isWebView = !!data?.isWebView;
|
|
212
|
+
} else if(command.trim().startsWith('horizontal_scroll_element')) {
|
|
213
|
+
const data = JSON.parse(command.split('horizontal_scroll_element').at(-1).trim());
|
|
214
|
+
await this.mobileActions.horizontal_scroll_element(data.element);
|
|
215
|
+
await sleep(500); //Wait for UI to load after scroll
|
|
216
|
+
isWebView = !!data?.isWebView;
|
|
217
|
+
} else if(command.trim().startsWith('swipe_up_on_element')) {
|
|
218
|
+
const data = JSON.parse(command.split('swipe_up_on_element').at(-1).trim());
|
|
219
|
+
await this.mobileActions.swipe_up_on_element(data.element);
|
|
220
|
+
await sleep(500); //Wait for UI to load after scroll
|
|
221
|
+
isWebView = !!data?.isWebView;
|
|
222
|
+
} else if(command.trim().startsWith('swipe_down_on_element')) {
|
|
223
|
+
const data = JSON.parse(command.split('swipe_down_on_element').at(-1).trim());
|
|
224
|
+
await this.mobileActions.swipe_down_on_element(data.element);
|
|
225
|
+
await sleep(500); //Wait for UI to load after scroll
|
|
226
|
+
isWebView = !!data?.isWebView;
|
|
227
|
+
} else if(command.trim().startsWith('swipe_left_on_element')) {
|
|
228
|
+
const data = JSON.parse(command.split('swipe_left_on_element').at(-1).trim());
|
|
229
|
+
await this.mobileActions.swipe_left_on_element(data.element);
|
|
230
|
+
await sleep(500); //Wait for UI to load after scroll
|
|
231
|
+
isWebView = !!data?.isWebView;
|
|
232
|
+
} else if(command.trim().startsWith('swipe_right_on_element')) {
|
|
233
|
+
const data = JSON.parse(command.split('swipe_right_on_element').at(-1).trim());
|
|
234
|
+
await this.mobileActions.swipe_right_on_element(data.element);
|
|
235
|
+
await sleep(500); //Wait for UI to load after scroll
|
|
236
|
+
isWebView = !!data?.isWebView;
|
|
237
|
+
} else if(command.trim().startsWith('switch_to_web_view')) {
|
|
238
|
+
await this.mobileActions.switch_to_webview();
|
|
239
|
+
isWebView = true;
|
|
240
|
+
} else if(command.trim().startsWith('switch_to_native_view')) {
|
|
241
|
+
await this.mobileActions.switch_to_native_app();
|
|
242
|
+
} else if(command.trim().startsWith('press_back_button')) {
|
|
243
|
+
await this.mobileActions.press_back_button();
|
|
244
|
+
} else if(command.trim().startsWith('hide_keyboard')) {
|
|
245
|
+
await this.mobileActions.hide_keyboard();
|
|
246
|
+
} else if(command.trim().startsWith('close_app')) {
|
|
247
|
+
await this.mobileActions.close_app();
|
|
248
|
+
} else if(command.trim().startsWith('open_app')) {
|
|
249
|
+
await this.mobileActions.open_app();
|
|
250
|
+
} else if(command.trim().startsWith('type_text')) {
|
|
251
|
+
const data = JSON.parse(command.split('type_text').at(-1).trim());
|
|
252
|
+
await this.mobileActions.type_text(data.text || '');
|
|
253
|
+
await sleep(100);
|
|
254
|
+
} else if(command.trim().startsWith('reload_state')) {
|
|
255
|
+
try {
|
|
256
|
+
const data = JSON.parse(command.split('reload_state').at(-1).trim() || '{}');
|
|
257
|
+
isWebView = !!data?.isWebView;
|
|
258
|
+
} catch (_er) {}
|
|
259
|
+
} else {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await this.printMobileState(isWebView);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
exports.MobileRecorderControl = MobileRecorderControl;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const { isNumber, getBoolean } = require("../utils/common-utils");
|
|
2
|
+
const { getDeviceCloudCapabilties, getDeviceCloudConnection } = require("./device-cloud/device-cloud-service");
|
|
3
|
+
|
|
4
|
+
function getCapabilities(config, app, testInfo, timeouts) {
|
|
5
|
+
return mobileCapabilities(config, app, testInfo, timeouts);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function mobileCapabilities(config, app, testInfo, timeouts) {
|
|
9
|
+
const commonCaps = {
|
|
10
|
+
'appium:newCommandTimeout': timeouts?.inactiveTimeout || 2 * 60,
|
|
11
|
+
'appium:noReset': config?.preferences?.resetData == false,
|
|
12
|
+
'appium:autoLaunch': true,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const osCaps = config?.os === 'ios'
|
|
16
|
+
? {
|
|
17
|
+
...commonCaps,
|
|
18
|
+
platformName: 'iOS',
|
|
19
|
+
'appium:platformVersion': config.device?.os_version,
|
|
20
|
+
...(app.ios.bundleId ? {'appium:bundleId': app.ios.bundleId} : {}),
|
|
21
|
+
'appium:useNewWDA': false,
|
|
22
|
+
'appium:autoAcceptAlerts': true,
|
|
23
|
+
}
|
|
24
|
+
: {
|
|
25
|
+
...commonCaps,
|
|
26
|
+
platformName: 'Android',
|
|
27
|
+
...(app.android.package ? {'appium:appPackage': app.android.package} : {}),
|
|
28
|
+
...(app.android.activity ? {'appium:appActivity': app.android.activity} : {}),
|
|
29
|
+
'appium:disableWindowAnimation': true, // Speeds up interactions
|
|
30
|
+
'appium:autoGrantPermissions': true,
|
|
31
|
+
'appium:autoDismissAlerts': true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if(config.deviceCloud) {
|
|
35
|
+
return {
|
|
36
|
+
...osCaps,
|
|
37
|
+
...getDeviceCloudCapabilties(config, app, testInfo),
|
|
38
|
+
...getUserCapabilities(config.preferences?.capabilities),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if(config?.os === 'ios') {
|
|
43
|
+
let nameLabel = app?.name ? ` - ${app.name}` : '';
|
|
44
|
+
if(config.device?.simulator) {
|
|
45
|
+
if(!app?.ios?.appPath) throw {message: `.app file path is need to test app in simulator ${nameLabel}`};
|
|
46
|
+
} else {
|
|
47
|
+
if(!app?.ios?.ipaPath) throw {message: `.ipa file path is need to test app in device ${nameLabel}`};
|
|
48
|
+
}
|
|
49
|
+
} else if(config?.os === 'android' && !app?.android?.apkPath) {
|
|
50
|
+
let nameLabel = app?.name ? ` - ${app.name}` : '';
|
|
51
|
+
throw { message: `The app doesn't have Android apk details ${nameLabel}` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const localDeviceCaps = config?.os === 'ios'
|
|
55
|
+
? {
|
|
56
|
+
"appium:automationName": "XCUITest",
|
|
57
|
+
'appium:app': config.device?.simulator ? app.ios.appPath : app.ios.ipaPath,
|
|
58
|
+
'appium:showXcodeLog': true,
|
|
59
|
+
'appium:udid': config.device?.id,
|
|
60
|
+
}
|
|
61
|
+
: {
|
|
62
|
+
'appium:automationName': 'UiAutomator2',
|
|
63
|
+
'appium:deviceName': 'Android',
|
|
64
|
+
'appium:app': app.android.apkPath,
|
|
65
|
+
'appium:udid': config.device?.id,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...osCaps,
|
|
70
|
+
...localDeviceCaps,
|
|
71
|
+
...getUserCapabilities(config?.preferences?.capabilities),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getUserCapabilities(caps) {
|
|
76
|
+
if(!caps) return {};
|
|
77
|
+
|
|
78
|
+
let capabilities = {};
|
|
79
|
+
caps.forEach(cap => {
|
|
80
|
+
if(!cap?.name?.trim()) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if(cap.type === 'boolean') {
|
|
85
|
+
capabilities[cap.name] = getBoolean(cap.value);
|
|
86
|
+
} else if(cap.type === 'number') {
|
|
87
|
+
if(!isNumber(cap.value)) {
|
|
88
|
+
throw {message: `Capability ${cap.name} type mismatch. Expected number but found ${cap.value}`};
|
|
89
|
+
}
|
|
90
|
+
capabilities[cap.name] = Number(cap.value);
|
|
91
|
+
} else {
|
|
92
|
+
capabilities[cap.name] = cap.value;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return capabilities;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getConnectionDetails(config) {
|
|
99
|
+
if(config.deviceCloud) {
|
|
100
|
+
return getDeviceCloudConnection(config);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
port: config.appium.port,
|
|
105
|
+
hostname: '127.0.0.1',
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
exports.getCapabilities = getCapabilities;
|
|
110
|
+
exports.getConnectionDetails = getConnectionDetails;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const { LocatorType } = require("../types/elements.types");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
class TppElement {
|
|
5
|
+
|
|
6
|
+
constructor(element, page, fromAi) {
|
|
7
|
+
this.element = element;
|
|
8
|
+
this.page = page;
|
|
9
|
+
this.fromAi = fromAi
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getSelector() {
|
|
13
|
+
if(this.element.locator?.startsWith('/html')) {
|
|
14
|
+
return `/${this.element.locator}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
switch(this.element.locatorType) {
|
|
18
|
+
case LocatorType.id: return `#${this.element.locator}`;
|
|
19
|
+
default: return this.element.locator;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getPageLoc() {
|
|
24
|
+
return this.page.locator(this.getSelector());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
locator() {
|
|
28
|
+
let chainedLocator = this.element.chainedLocator;
|
|
29
|
+
if(chainedLocator?.length) {
|
|
30
|
+
let currentLocator = this.page;
|
|
31
|
+
for(let i=0; i<chainedLocator.length; i++) {
|
|
32
|
+
currentLocator = currentLocator.locator(chainedLocator[i].locator)
|
|
33
|
+
if(chainedLocator[i].containsText) {
|
|
34
|
+
currentLocator = currentLocator.getByText(chainedLocator[i].containsText);
|
|
35
|
+
}
|
|
36
|
+
const eleIndex = (chainedLocator[i].index || 1) - 1;
|
|
37
|
+
currentLocator = currentLocator.nth(eleIndex);
|
|
38
|
+
}
|
|
39
|
+
return currentLocator;
|
|
40
|
+
}
|
|
41
|
+
const loc = this.getPageLoc();
|
|
42
|
+
if(this.fromAi) {
|
|
43
|
+
return loc.nth(0); //AI response could match more than one element.
|
|
44
|
+
} else if(this.element.index != null) {
|
|
45
|
+
return loc.nth(this.element.index);
|
|
46
|
+
}
|
|
47
|
+
return loc;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
label() {
|
|
51
|
+
return JSON.stringify(this.element);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
exports.TppElement = TppElement;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { LocatorType } = require("../types/elements.types");
|
|
2
|
+
|
|
3
|
+
class TppElement {
|
|
4
|
+
|
|
5
|
+
constructor(element, driver) {
|
|
6
|
+
this.element = element;
|
|
7
|
+
this.driver = driver;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getSelector() {
|
|
11
|
+
switch (this.element.locatorType) {
|
|
12
|
+
case 'id': return `id=${this.element.locator}`;
|
|
13
|
+
case 'accessibilityId': return `~${this.element.locator}`;
|
|
14
|
+
case 'className': return `.${this.element.locator}`;
|
|
15
|
+
default: return this.element.locator;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getPageLoc() {
|
|
20
|
+
let uiElement = await this.driver.$(this.getSelector());
|
|
21
|
+
for(let i=0; i<3 && !uiElement.elementId; i++) {
|
|
22
|
+
console.log(`Element ${this.label()} is unstable, retrying...`);
|
|
23
|
+
uiElement = await this.driver.$(this.getSelector());
|
|
24
|
+
}
|
|
25
|
+
return uiElement;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async getCount() {
|
|
29
|
+
try {
|
|
30
|
+
const elements = await this.driver.$$(this.getSelector());
|
|
31
|
+
return elements?.length;
|
|
32
|
+
} catch (_ex) {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async locator() {
|
|
38
|
+
if (this.element.index != null) {
|
|
39
|
+
return (await this.driver.$$(this.getSelector()))[this.element.index];
|
|
40
|
+
}
|
|
41
|
+
return await this.getPageLoc();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
label() {
|
|
45
|
+
return JSON.stringify(this.element);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.TppElement = TppElement;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
class NetworkLogManager {
|
|
2
|
+
|
|
3
|
+
constructor(networkConfig, page) {
|
|
4
|
+
this.enabled = !!networkConfig?.enabled;
|
|
5
|
+
if(!networkConfig?.enabled || !page) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
this.networkLogs = [];
|
|
10
|
+
this.webSocketRequestLogs = [];
|
|
11
|
+
this.webSocketResponseLogs = [];
|
|
12
|
+
this.openWebSockets = [];
|
|
13
|
+
this.closedWebSockets = [];
|
|
14
|
+
this.domains = networkConfig.domains;
|
|
15
|
+
this.startLogTracing(page);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
startLogTracing(page) {
|
|
19
|
+
if(!this.enabled) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const handleResponse = async (response) => {
|
|
23
|
+
const request = response.request();
|
|
24
|
+
const url = request.url();
|
|
25
|
+
const resourceType = request.resourceType();
|
|
26
|
+
|
|
27
|
+
let responseBody;
|
|
28
|
+
try {
|
|
29
|
+
if (response.status() < 300 || response.status() >= 400) {
|
|
30
|
+
responseBody = await response.text();
|
|
31
|
+
}
|
|
32
|
+
} catch(_ex) {}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
(resourceType === 'fetch' || resourceType === 'xhr' || resourceType === 'websocket')
|
|
36
|
+
&& (!this.domains?.length || this.domains.some(domain => url.includes(domain)))
|
|
37
|
+
) {
|
|
38
|
+
const requestData = {
|
|
39
|
+
url,
|
|
40
|
+
requestType: resourceType,
|
|
41
|
+
method: request.method(),
|
|
42
|
+
headers: request.headers(),
|
|
43
|
+
requestPayload: request.postData(),
|
|
44
|
+
status: response.status(),
|
|
45
|
+
responseHeaders: response.headers(),
|
|
46
|
+
responseBody: responseBody,
|
|
47
|
+
};
|
|
48
|
+
this.networkLogs.push(requestData);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleWebSocketData = (url, event, type) => {
|
|
53
|
+
if(!this.domains?.length || this.domains.some(domain => url.includes(domain))) {
|
|
54
|
+
if(type === 'framesent') {
|
|
55
|
+
this.webSocketRequestLogs.push({
|
|
56
|
+
url,
|
|
57
|
+
message: event.payload,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
});
|
|
60
|
+
} else if(type === 'framereceived') {
|
|
61
|
+
this.webSocketResponseLogs.push({
|
|
62
|
+
url,
|
|
63
|
+
message: event.payload,
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleWebSocket = (ws) => {
|
|
71
|
+
this.openWebSockets.push(ws.url());
|
|
72
|
+
ws.on('framesent', event => {
|
|
73
|
+
handleWebSocketData(ws.url(), event, 'framesent');
|
|
74
|
+
});
|
|
75
|
+
ws.on('framereceived', event => {
|
|
76
|
+
handleWebSocketData(ws.url(), event, 'framereceived');
|
|
77
|
+
});
|
|
78
|
+
ws.on('close', () => {
|
|
79
|
+
this.openWebSockets = this.openWebSockets.filter(w => w !== ws.url());
|
|
80
|
+
this.closedWebSockets.push(ws.url());
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
page.on('response', handleResponse);
|
|
85
|
+
|
|
86
|
+
page.on('websocket', ws => handleWebSocket(ws));
|
|
87
|
+
|
|
88
|
+
page.context().on('page', async (newPage) => {
|
|
89
|
+
newPage.on('response', handleResponse);
|
|
90
|
+
newPage.on('websocket', ws => handleWebSocket(ws));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
exports.NetworkLogManager = NetworkLogManager;
|