@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
package/src/actions.js
ADDED
|
@@ -0,0 +1,3201 @@
|
|
|
1
|
+
const { TppElement } = require("./models/tpp-element");
|
|
2
|
+
const { expect } = require("@playwright/test");
|
|
3
|
+
const { TppA11y, checkA11yAttachments } = require("./accessibility/tpp-a11y");
|
|
4
|
+
const { TppVisualCompare } = require("./visual-test/tpp-visual-test");
|
|
5
|
+
const { ApiService } = require("./api/api-service")
|
|
6
|
+
const { ansiRegex } = require("./utils/log-utils");
|
|
7
|
+
const { compare } = require("./utils/assert-utils");
|
|
8
|
+
const { MySQLService } = require("./database/mysql/mysql-service");
|
|
9
|
+
const { writeStorageStateToFile, loadStorageStateFromFile } = require("./utils/cookie-utils");
|
|
10
|
+
const { PostgreSQLService } = require("./database/pg/pg-service");
|
|
11
|
+
const { OracleService } = require("./database/oracle/oracle-service");
|
|
12
|
+
const { EachTestHook } = require("./hooks/each-hook");
|
|
13
|
+
const { TppLibrary } = require("./library/tpp-library");
|
|
14
|
+
const { Logger, LOG_PREFIX } = require("./utils/logger");
|
|
15
|
+
const { SliderUtils } = require("./library/web/slider-utils");
|
|
16
|
+
const { getScreenshotPath, getScreenshotLog } = require("./utils/screenshot-utils");
|
|
17
|
+
const { sleep, generateRandomString } = require("./utils/common-utils");
|
|
18
|
+
const { FileUtils } = require("./library/file/file-utils");
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { WebExecutorAgent } = require("./web/web-executor-agent");
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const WebSocket = require('ws');
|
|
24
|
+
const TokenConfig = require('./library/emailTesting/TokenConfig');
|
|
25
|
+
const config = require('./app-config.json');
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
const { fetchWebState, takeElementScreenshot } = require('./web/web-executor-data-fetch');
|
|
28
|
+
const { aiWebSocketCall, AI_ENDPOINTS } = require('./utils/ai-utils');
|
|
29
|
+
const { AutohealActions } = require("./autoheal-actions");
|
|
30
|
+
const { Neo4jService } = require("./database/neo4j/neo4j-service");
|
|
31
|
+
const { getAutohealPath } = require('./utils/autoheal-utils');
|
|
32
|
+
|
|
33
|
+
const WEBAPP_SOCKET_URL = config.webAppSocketUrl;
|
|
34
|
+
|
|
35
|
+
class Actions {
|
|
36
|
+
|
|
37
|
+
constructor(page, request, fromAi, retry, totalRetries, customConfig, webComponents, mobileActions) {
|
|
38
|
+
this.request = request
|
|
39
|
+
this.page = page;
|
|
40
|
+
this.testType = !!mobileActions ? 'mobile' : 'web';
|
|
41
|
+
this.networkLogManager = webComponents?.networkLogManager;
|
|
42
|
+
this.consoleLogManager = webComponents?.consoleLogManager;
|
|
43
|
+
this.downloadsManager = webComponents?.downloadsManager;
|
|
44
|
+
this.extensionId = webComponents?.extensionId;
|
|
45
|
+
this.mobile = mobileActions;
|
|
46
|
+
this.fromAi = fromAi;
|
|
47
|
+
this.retry = retry;
|
|
48
|
+
this.totalRetries = totalRetries;
|
|
49
|
+
this.dialogFns = undefined;
|
|
50
|
+
this.library = new TppLibrary(this.caller, undefined, webComponents, customConfig, (label) => this.addScreenshot(label), page);
|
|
51
|
+
this.customConfig = customConfig;
|
|
52
|
+
this.curr_browser = undefined;
|
|
53
|
+
this.autoheal = new AutohealActions(this);
|
|
54
|
+
this.fallbackLocatorCache = {};
|
|
55
|
+
this.executionPreferences = this.customConfig?.executionPreference || {};
|
|
56
|
+
this.webAppPreferences = this.customConfig?.webAppPreference || {};
|
|
57
|
+
this.testData = undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
newChild(name) {
|
|
61
|
+
const webComponents = this.testType === 'web'
|
|
62
|
+
? {
|
|
63
|
+
networkLogManager: this.networkLogManager,
|
|
64
|
+
consoleLogManager: this.consoleLogManager,
|
|
65
|
+
downloadsManager: this.downloadsManager,
|
|
66
|
+
extensionId: this.extensionId,
|
|
67
|
+
}
|
|
68
|
+
: undefined;
|
|
69
|
+
const childMobile = this.mobile?.newChild(name);
|
|
70
|
+
const childActions = new Actions(this.page, this.request, this.fromAi, this.retry, this.totalRetries, this.customConfig, webComponents, childMobile);
|
|
71
|
+
childActions.testLabel = this.testLabel;
|
|
72
|
+
childActions.caller = this.caller + " :: " + name;
|
|
73
|
+
childActions.blacklistedLogs = this.blacklistedLogs;
|
|
74
|
+
childActions.library = new TppLibrary(childActions.caller, this.blacklistedLogs, webComponents, this.customConfig, (label) => this.addScreenshot(label), this.page);
|
|
75
|
+
childActions.embedToIframeMap = this.embedToIframeMap;
|
|
76
|
+
childActions.curr_browser = this.curr_browser;
|
|
77
|
+
if(this.remoteBrowserPort) childActions.remoteBrowserPort = this.remoteBrowserPort;
|
|
78
|
+
childActions.testData = this.testData;
|
|
79
|
+
childActions.tppTest = this.tppTest;
|
|
80
|
+
return childActions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setBlacklistedLogs(blacklistedLogs) {
|
|
84
|
+
this.blacklistedLogs = blacklistedLogs;
|
|
85
|
+
this.library?.setBlacklistedLogs(blacklistedLogs);
|
|
86
|
+
this.mobile?.setBlacklistedLogs(blacklistedLogs);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setCaller(testType, caller, tppTest, testDataId) {
|
|
90
|
+
this.testType = testType;
|
|
91
|
+
this.testLabel = caller;
|
|
92
|
+
this.tppTest = tppTest;
|
|
93
|
+
//TODO fix caller init for API and Web
|
|
94
|
+
if(testType === 'web') {
|
|
95
|
+
const projId = tppTest.info()?.project?.name || tppTest.info()?._test?._projectId?.trim() || '';
|
|
96
|
+
let browserName;
|
|
97
|
+
if(["CHROME", "CHROMIUM", "EDGE", "SAFARI", "FIREFOX"].includes(projId.split(" ")[0])) {
|
|
98
|
+
browserName = projId.split(" ")[0];
|
|
99
|
+
} else {
|
|
100
|
+
browserName = projId.replaceAll(" ", "_");
|
|
101
|
+
}
|
|
102
|
+
this.curr_browser = browserName;
|
|
103
|
+
const xy = this.page.context()?._options?.viewport;
|
|
104
|
+
this.caller = browserName && xy
|
|
105
|
+
? `${caller} > ${browserName}-${xy.width}x${xy.height}`
|
|
106
|
+
: browserName ? `${caller} > ${browserName}`
|
|
107
|
+
: caller;
|
|
108
|
+
} else {
|
|
109
|
+
this.caller = caller;
|
|
110
|
+
}
|
|
111
|
+
if(!!testDataId) {
|
|
112
|
+
this.caller = `${this.caller} > ${testDataId}`;
|
|
113
|
+
}
|
|
114
|
+
this.mobile?.setCaller(this.caller);
|
|
115
|
+
this.library?.setCaller(this.caller);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setRemoteBrowserPort(port) {
|
|
119
|
+
this.remoteBrowserPort = port;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setTestData(testData) {
|
|
123
|
+
this.testData = testData;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async initializeConfig() {
|
|
127
|
+
if (!this.config) {
|
|
128
|
+
this.config = await TokenConfig.getConfig();
|
|
129
|
+
}
|
|
130
|
+
return this.config;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getAuthHeaders() {
|
|
134
|
+
const config = await this.initializeConfig();
|
|
135
|
+
return {
|
|
136
|
+
'Authorization': `${config.token_type} ${config.access_token}`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getOfflineStatus() {
|
|
141
|
+
const config = await this.initializeConfig();
|
|
142
|
+
return config.offline;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async startWebExecutorAgent(port) {
|
|
146
|
+
this.remoteBrowserPort = port;
|
|
147
|
+
try {
|
|
148
|
+
await (new WebExecutorAgent(this)).openWebExecutorAgent();
|
|
149
|
+
} catch(error) {
|
|
150
|
+
this.log(undefined, `[EXECUTOR-AGENT-ERROR] ${error?.message || 'Unable to read the web page!'}`);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
comment(text) {
|
|
156
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: [user_log] ${text}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async wait_for_flutter_app(timeout) {
|
|
160
|
+
const logLabel = `Wait for Flutter app within ${timeout} ms`;
|
|
161
|
+
this.startLog(logLabel);
|
|
162
|
+
|
|
163
|
+
if(timeout <= 0) {
|
|
164
|
+
timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await this.page.waitForSelector('flt-glass-pane, flutter-view', {
|
|
169
|
+
state: 'visible',
|
|
170
|
+
timeout,
|
|
171
|
+
});
|
|
172
|
+
} catch (ex) {
|
|
173
|
+
console.log("Error waiting for Flutter app", ex);
|
|
174
|
+
this.failureLog(logLabel);
|
|
175
|
+
throw {message: `Flutter app not loaded within ${timeout} ms`};
|
|
176
|
+
}
|
|
177
|
+
this.endLog(logLabel);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async unlock_flutter_dom() {
|
|
181
|
+
const logLabel = `Unlock Flutter DOM`;
|
|
182
|
+
this.startLog(logLabel);
|
|
183
|
+
|
|
184
|
+
let timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await this.page.waitForSelector('flt-glass-pane, flutter-view', {
|
|
188
|
+
state: 'visible',
|
|
189
|
+
timeout,
|
|
190
|
+
});
|
|
191
|
+
} catch (ex) {
|
|
192
|
+
console.log("Error waiting for Flutter app", ex);
|
|
193
|
+
this.failureLog(logLabel);
|
|
194
|
+
throw {message: `Flutter app not loaded within ${timeout} ms`};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await this.page.evaluate(() => {
|
|
199
|
+
let pane = document.querySelector('flt-glass-pane');
|
|
200
|
+
if(pane?.shadowRoot) {
|
|
201
|
+
pane = pane.shadowRoot;
|
|
202
|
+
}
|
|
203
|
+
let semantics = pane?.querySelector('flt-semantics-placeholder');
|
|
204
|
+
if(!semantics) {
|
|
205
|
+
semantics = document.querySelector('flt-semantics-placeholder');
|
|
206
|
+
}
|
|
207
|
+
semantics?.click();
|
|
208
|
+
});
|
|
209
|
+
} catch (ex) {
|
|
210
|
+
console.log("Error unlocking Flutter DOM", ex);
|
|
211
|
+
this.failureLog(logLabel);
|
|
212
|
+
throw {message: `Error unlocking Flutter DOM - ${ex.message}`};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.endLog(logLabel);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async unlockFlutterDom() {
|
|
219
|
+
this.page.on('load', async page => {
|
|
220
|
+
try {
|
|
221
|
+
await page.waitForSelector('flt-glass-pane, flutter-view', {
|
|
222
|
+
state: 'visible',
|
|
223
|
+
timeout: 1000 * (this.customConfig?.timeouts?.urlLoadTimeout || 20),
|
|
224
|
+
});
|
|
225
|
+
await page.evaluate(() => {
|
|
226
|
+
let pane = document.querySelector('flt-glass-pane');
|
|
227
|
+
if(pane?.shadowRoot) {
|
|
228
|
+
pane = pane.shadowRoot;
|
|
229
|
+
}
|
|
230
|
+
let semantics = pane?.querySelector('flt-semantics-placeholder');
|
|
231
|
+
if(!semantics) {
|
|
232
|
+
semantics = document.querySelector('flt-semantics-placeholder');
|
|
233
|
+
}
|
|
234
|
+
semantics?.click();
|
|
235
|
+
});
|
|
236
|
+
} catch (ex) {
|
|
237
|
+
console.log("Flutter DOM error", ex);
|
|
238
|
+
throw {message: `Unable to unlock Flutter DOM`};
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async get_test_info() {
|
|
244
|
+
const logLabel = `Get test info`;
|
|
245
|
+
this.startLog(logLabel);
|
|
246
|
+
let data = this.customConfig.testInfo
|
|
247
|
+
|| {
|
|
248
|
+
status: 'running',
|
|
249
|
+
retry: 0,
|
|
250
|
+
maxRetries: 0,
|
|
251
|
+
threadIndex: 0
|
|
252
|
+
};
|
|
253
|
+
this.endLog(logLabel);
|
|
254
|
+
return data;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async get_test_data() {
|
|
258
|
+
const logLabel = `Get test data`;
|
|
259
|
+
this.startLog(logLabel);
|
|
260
|
+
let data = this.testData;
|
|
261
|
+
this.endLog(logLabel);
|
|
262
|
+
return data;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async open_url(url) {
|
|
266
|
+
const logLabel = `Open url - ${url}`;
|
|
267
|
+
this.startLog(logLabel);
|
|
268
|
+
await this.page.goto(url);
|
|
269
|
+
await this.page.waitForLoadState();
|
|
270
|
+
this.endLog(logLabel);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
handleOfflineMode(){
|
|
274
|
+
throw {message: `Open ppt or pdf in browser action should not be proceeded with offline`}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async renderPresentationFromFile(filePath, validExtensions, logPrefix) {
|
|
278
|
+
const logLabel = `${logPrefix} - ${filePath}`;
|
|
279
|
+
const logger = new Logger(this.caller, this.blacklistedLogs);
|
|
280
|
+
this.startLog(logLabel);
|
|
281
|
+
|
|
282
|
+
const offline = await this.getOfflineStatus();
|
|
283
|
+
if (offline) {
|
|
284
|
+
this.handleOfflineMode();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
filePath = new FileUtils().getFilePath(filePath);
|
|
289
|
+
|
|
290
|
+
if (!fs.existsSync(filePath)) {
|
|
291
|
+
throw { message: `File not found: ${filePath}` };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const fileExt = path.extname(filePath).toLowerCase();
|
|
295
|
+
if (!validExtensions.includes(fileExt)) {
|
|
296
|
+
throw { message: `Invalid file type. Allowed types: ${validExtensions.join(', ')}: ${filePath}` };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const authHeaders = await this.getAuthHeaders();
|
|
301
|
+
let htmlContent = '';
|
|
302
|
+
|
|
303
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
304
|
+
const fileSizeInMB = fileBuffer.length / (1024 * 1024);
|
|
305
|
+
|
|
306
|
+
if (fileSizeInMB > 25) {
|
|
307
|
+
throw { message: `File size exceeds 25MB limit: ${fileSizeInMB.toFixed(2)}MB` };
|
|
308
|
+
}
|
|
309
|
+
const fileName = path.basename(filePath);
|
|
310
|
+
|
|
311
|
+
const clientId = crypto.randomUUID();
|
|
312
|
+
|
|
313
|
+
const token = authHeaders.Authorization;
|
|
314
|
+
const wsUrl = `${WEBAPP_SOCKET_URL}/cable?authorization=${encodeURIComponent(token)}&client_id=${clientId}`;
|
|
315
|
+
const ws = new WebSocket(wsUrl);
|
|
316
|
+
|
|
317
|
+
const wsPromise = new Promise((resolve, reject) => {
|
|
318
|
+
ws.onopen = () => {
|
|
319
|
+
|
|
320
|
+
ws.send(JSON.stringify({
|
|
321
|
+
command: 'subscribe',
|
|
322
|
+
identifier: JSON.stringify({
|
|
323
|
+
channel: 'PresentationChannel'
|
|
324
|
+
})
|
|
325
|
+
}));
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
let subscribed = false;
|
|
329
|
+
|
|
330
|
+
ws.onmessage = (event) => {
|
|
331
|
+
try {
|
|
332
|
+
const response = JSON.parse(event.data);
|
|
333
|
+
|
|
334
|
+
if (response.type === 'confirm_subscription') {
|
|
335
|
+
subscribed = true;
|
|
336
|
+
|
|
337
|
+
const fileData = {
|
|
338
|
+
fileName: fileName,
|
|
339
|
+
fileContent: fileBuffer.toString('base64'),
|
|
340
|
+
fileType: fileExt
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
ws.send(JSON.stringify({
|
|
344
|
+
command: 'message',
|
|
345
|
+
identifier: JSON.stringify({
|
|
346
|
+
channel: 'PresentationChannel'
|
|
347
|
+
}),
|
|
348
|
+
data: JSON.stringify({
|
|
349
|
+
action: 'process_file',
|
|
350
|
+
file_data: fileData
|
|
351
|
+
})
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
else if (response.message) {
|
|
356
|
+
const message = response.message;
|
|
357
|
+
|
|
358
|
+
if (message.type === 'error') {
|
|
359
|
+
ws.close();
|
|
360
|
+
reject({ message: `${logPrefix}: ${message.data.error}` });
|
|
361
|
+
} else if (message.type === 'complete') {
|
|
362
|
+
htmlContent = message.data.html_content;
|
|
363
|
+
ws.close();
|
|
364
|
+
resolve(htmlContent);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
else {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
} catch (e) {
|
|
372
|
+
throw { message: `${logPrefix}: ${e.message}` };
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
ws.onerror = (error) => {
|
|
377
|
+
ws.close();
|
|
378
|
+
reject({ message: `${logPrefix}: Connection Failed` });
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
ws.onclose = (event) => {
|
|
382
|
+
if (!event.wasClean && !htmlContent) {
|
|
383
|
+
reject({ message: `${logPrefix}: Connection closed unexpectedly (code: ${event.code})` });
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
htmlContent = await wsPromise;
|
|
389
|
+
|
|
390
|
+
await this.page.goto('about:blank');
|
|
391
|
+
await this.page.setContent(htmlContent, { waitUntil: 'networkidle' });
|
|
392
|
+
await this.page.evaluate(() => { document.title = "File Viewer"; });
|
|
393
|
+
|
|
394
|
+
} catch (error) {
|
|
395
|
+
logger.failureLog(logLabel);
|
|
396
|
+
throw { message: `${logPrefix}: ${error.message}` };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this.endLog(logLabel);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async open_ppt_in_browser(filePath) {
|
|
403
|
+
const validExtensions = ['.ppt', '.pptx'];
|
|
404
|
+
return this.renderPresentationFromFile(filePath, validExtensions, 'Open PPT in browser');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async open_pdf_in_browser(filePath) {
|
|
408
|
+
const validExtensions = ['.pdf'];
|
|
409
|
+
return this.renderPresentationFromFile(filePath, validExtensions, 'Open PDF in browser');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async save_page_as_pdf() {
|
|
413
|
+
const logLabel = `Save page as PDF`;
|
|
414
|
+
this.startLog(logLabel);
|
|
415
|
+
const filePath = `downloads/${generateRandomString(10)}.pdf`;
|
|
416
|
+
try {
|
|
417
|
+
await this.page.pdf({ path: filePath });
|
|
418
|
+
} catch (error) {
|
|
419
|
+
this.failureLog(logLabel);
|
|
420
|
+
throw { message: error.message || 'Unable to save page as PDF' };
|
|
421
|
+
}
|
|
422
|
+
const fullPath = path.resolve(filePath);
|
|
423
|
+
if(!fs.existsSync(fullPath)) {
|
|
424
|
+
this.failureLog(logLabel);
|
|
425
|
+
throw { message: 'Unable to save page as PDF' };
|
|
426
|
+
}
|
|
427
|
+
this.log(logLabel, `Page saved as PDF at ${fullPath}`);
|
|
428
|
+
this.endLog(logLabel);
|
|
429
|
+
return fullPath;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async get_browser_details() {
|
|
433
|
+
const logLabel = `Get browser details`;
|
|
434
|
+
this.startLog(logLabel);
|
|
435
|
+
|
|
436
|
+
const knownBrowsers = ["CHROME", "CHROMIUM", "EDGE", "SAFARI", "FIREFOX"];
|
|
437
|
+
const headless = this.page.context().browser()["_options"]?.headless ?? false;
|
|
438
|
+
|
|
439
|
+
let browserName = "UNKNOWN";
|
|
440
|
+
let resolution = null;
|
|
441
|
+
let emulatedDevice = null;
|
|
442
|
+
let viewport = this.page.context()?._options?.viewport;
|
|
443
|
+
|
|
444
|
+
const matchedBrowser = knownBrowsers.find(browser =>
|
|
445
|
+
this.curr_browser.toUpperCase().includes(browser)
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
if (matchedBrowser) {
|
|
449
|
+
browserName = matchedBrowser;
|
|
450
|
+
if (viewport) {
|
|
451
|
+
resolution = { x: viewport.width, y: viewport.height };
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
browserName = "emulated_browser";
|
|
455
|
+
emulatedDevice = this.curr_browser;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this.endLog(logLabel);
|
|
459
|
+
return browserName === "emulated_browser"
|
|
460
|
+
? { browserName, emulatedDevice, headless }
|
|
461
|
+
: { browserName, resolution, headless};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async get_environment_name(){
|
|
465
|
+
const logLabel = `Get environment name`;
|
|
466
|
+
this.startLog(logLabel);
|
|
467
|
+
const environment = this.customConfig?.environment?.trim();
|
|
468
|
+
this.endLog(logLabel);
|
|
469
|
+
return environment ? environment : "NO_ENV_SELECTED";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async open_chrome_extension_html(htmlPath) {
|
|
473
|
+
const logLabel = `Open Chrome extension html - ${htmlPath}`;
|
|
474
|
+
this.startLog(logLabel);
|
|
475
|
+
if(this.extensionId === 'TPP_EXTN_NOT_LOADED') {
|
|
476
|
+
throw {message: 'Chrome extension ID not found. Extension will still work. If you want to Open UI html and test this extension, please raise a request to DevAssure support!'};
|
|
477
|
+
} else if(!this.extensionId) {
|
|
478
|
+
throw {message: 'Extension not enabled! Enable Chrome extension in Project Preferences > Web App Settings and upload you extension.'}
|
|
479
|
+
}
|
|
480
|
+
let filePath = htmlPath.startsWith('/') ? htmlPath : `/${htmlPath}`;
|
|
481
|
+
const pageUrl = `chrome-extension://${this.extensionId}${filePath}`
|
|
482
|
+
await this.page.goto(pageUrl);
|
|
483
|
+
await this.page.waitForLoadState();
|
|
484
|
+
this.endLog(logLabel);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
get_page_url() {
|
|
488
|
+
const logLabel = `Get page url`;
|
|
489
|
+
this.startLog(logLabel);
|
|
490
|
+
this.endLog(logLabel);
|
|
491
|
+
return this.page.url();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async reload_page() {
|
|
495
|
+
const logLabel = `Reload page`;
|
|
496
|
+
this.startLog(logLabel);
|
|
497
|
+
await this.page.reload();
|
|
498
|
+
this.endLog(logLabel);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async go_back() {
|
|
502
|
+
const logLabel = `Go back`;
|
|
503
|
+
this.startLog(logLabel);
|
|
504
|
+
await this.page.goBack();
|
|
505
|
+
this.endLog(logLabel);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async go_forward() {
|
|
509
|
+
const logLabel = `Go forward`;
|
|
510
|
+
this.startLog(logLabel);
|
|
511
|
+
await this.page.goForward();
|
|
512
|
+
this.endLog(logLabel);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async wait_for_page_load(duration) {
|
|
516
|
+
const logLabel = `Wait for page to load within ${duration} ms`;
|
|
517
|
+
this.startLog(logLabel);
|
|
518
|
+
await this.page.waitForLoadState('domcontentloaded', {timeout: duration || 10000});
|
|
519
|
+
this.endLog(logLabel);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async wait_for_url(urlRegex) {
|
|
523
|
+
const logLabel = `Wait for URL - ${urlRegex}`;
|
|
524
|
+
this.startLog(logLabel);
|
|
525
|
+
let regex = urlRegex;
|
|
526
|
+
try {
|
|
527
|
+
regex = new RegExp(urlRegex);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
}
|
|
530
|
+
await this.page.waitForURL(regex)
|
|
531
|
+
this.endLog(logLabel);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async wait_for_url_ms(urlRegex, timeout) {
|
|
535
|
+
const logLabel = `Wait for URL - ${urlRegex}, within ${timeout} ms`;
|
|
536
|
+
this.startLog(logLabel);
|
|
537
|
+
let regex = urlRegex;
|
|
538
|
+
try {
|
|
539
|
+
regex = new RegExp(urlRegex);
|
|
540
|
+
} catch (e) {
|
|
541
|
+
}
|
|
542
|
+
await this.page.waitForURL(regex, {timeout: timeout})
|
|
543
|
+
this.endLog(logLabel);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async get_cookie_value(url, cookieKey) {
|
|
547
|
+
const logLabel = `Get cookie value. Url - ${url}. Cookie key - ${cookieKey}`;
|
|
548
|
+
this.startLog(logLabel);
|
|
549
|
+
const cookies = await this.page.context().cookies(url);
|
|
550
|
+
let value = '';
|
|
551
|
+
if(!cookies?.length) {
|
|
552
|
+
this.log(logLabel, `No Cookies found for url ${url}`);
|
|
553
|
+
} else {
|
|
554
|
+
const cookie = cookies.find(cookie => cookie.name === cookieKey);
|
|
555
|
+
if(!cookie) {
|
|
556
|
+
this.log(logLabel, `No Cookie with key ${cookieKey} found`);
|
|
557
|
+
} else {
|
|
558
|
+
value = cookie?.value || '';
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
this.endLog(logLabel);
|
|
562
|
+
return value;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async set_cookie_value(url, cookieKey, cookieValue) {
|
|
566
|
+
const logLabel = `Set cookie value. Url - ${url}. Cookie key - ${cookieKey}. Cookie value - ${cookieValue}`;
|
|
567
|
+
this.startLog(logLabel);
|
|
568
|
+
const cookieData = {name: cookieKey, value: cookieValue, url: url};
|
|
569
|
+
if(this.remoteBrowserPort) {
|
|
570
|
+
const commandData = JSON.stringify(cookieData);
|
|
571
|
+
console.log(`[[TPP]] [[TPP Remote-Execution COMMAND add_cookie]] ${commandData}`);
|
|
572
|
+
await sleep(5000);
|
|
573
|
+
} else {
|
|
574
|
+
await this.page.context().addCookies([cookieData]);
|
|
575
|
+
}
|
|
576
|
+
this.endLog(logLabel);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async remove_cookies() {
|
|
580
|
+
const logLabel = `Remove all cookies`;
|
|
581
|
+
this.startLog(logLabel);
|
|
582
|
+
await this.page.context().clearCookies();
|
|
583
|
+
this.endLog(logLabel);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async save_storage_state(label) {
|
|
587
|
+
const logLabel = `Save storage state to file - ${label}`;
|
|
588
|
+
this.startLog(logLabel);
|
|
589
|
+
await writeStorageStateToFile(this.page, label);
|
|
590
|
+
this.endLog(logLabel);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async load_storage_state(label) {
|
|
594
|
+
const logLabel = `Load storage state from file - ${label}`;
|
|
595
|
+
this.startLog(logLabel);
|
|
596
|
+
const newPage = await loadStorageStateFromFile(
|
|
597
|
+
this.page,
|
|
598
|
+
label,
|
|
599
|
+
new Logger(this.caller, this.blacklistedLogs),
|
|
600
|
+
logLabel,
|
|
601
|
+
this.remoteBrowserPort,
|
|
602
|
+
this.customConfig,
|
|
603
|
+
);
|
|
604
|
+
if(newPage) {
|
|
605
|
+
this.networkLogManager?.startLogTracing(newPage);
|
|
606
|
+
this.consoleLogManager?.startLogTracing(newPage);
|
|
607
|
+
this.downloadsManager?.handleDownloads(newPage);
|
|
608
|
+
Object.assign(this.page, newPage);
|
|
609
|
+
}
|
|
610
|
+
this.endLog(logLabel);
|
|
611
|
+
return !!newPage;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async is_element_clickable(element, timeout) {
|
|
615
|
+
const logLabel = `Is element - ${element.label} clickable within ${timeout} ms`;
|
|
616
|
+
this.startLog(logLabel);
|
|
617
|
+
if(timeout === -1) timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
618
|
+
const tppElement = new TppElement(element, this.page, this.fromAi);
|
|
619
|
+
try {
|
|
620
|
+
await tppElement.locator().click({trial: true, timeout: timeout})
|
|
621
|
+
} catch (ex) {
|
|
622
|
+
if(!ex.name?.includes('TimeoutError')) {
|
|
623
|
+
this.log(logLabel, `Returning false! Exception - ${ex}`);
|
|
624
|
+
}
|
|
625
|
+
this.endLog(logLabel);
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
this.endLog(logLabel);
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async is_element_visible(element, timeout) {
|
|
633
|
+
const logLabel = `Is element - ${element.label} visible within ${timeout} ms`;
|
|
634
|
+
this.startLog(logLabel);
|
|
635
|
+
if(timeout === -1) timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
636
|
+
const tppElement = new TppElement(element, this.page, this.fromAi);
|
|
637
|
+
try {
|
|
638
|
+
await tppElement.locator().waitFor({state: "visible", timeout: timeout});
|
|
639
|
+
} catch (ex) {
|
|
640
|
+
if(!ex.name?.includes('TimeoutError')) {
|
|
641
|
+
this.log(logLabel, `Returning false! Exception - ${ex}`);
|
|
642
|
+
}
|
|
643
|
+
this.endLog(logLabel);
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
this.endLog(logLabel);
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async is_element_present(element, timeout) {
|
|
651
|
+
const logLabel = `Is element - ${element.label} present within ${timeout} ms`;
|
|
652
|
+
this.startLog(logLabel);
|
|
653
|
+
if(timeout === -1) timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
654
|
+
const tppElement = new TppElement(element, this.page, this.fromAi);
|
|
655
|
+
try {
|
|
656
|
+
await tppElement.locator().waitFor({state: "attached", timeout: timeout});
|
|
657
|
+
} catch (ex) {
|
|
658
|
+
if(!ex.name?.includes('TimeoutError')) {
|
|
659
|
+
this.log(logLabel, `Returning false! Exception - ${ex}`);
|
|
660
|
+
}
|
|
661
|
+
this.endLog(logLabel);
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
this.endLog(logLabel);
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async is_element_not_present(element, timeout) {
|
|
669
|
+
const logLabel = `Is element - ${element.label} not present within ${timeout} ms`;
|
|
670
|
+
this.startLog(logLabel);
|
|
671
|
+
if(timeout === -1) timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
672
|
+
const tppElement = new TppElement(element, this.page, this.fromAi);
|
|
673
|
+
try {
|
|
674
|
+
await expect(tppElement.locator()).toHaveCount(0, {timeout: timeout});
|
|
675
|
+
} catch (ex) {
|
|
676
|
+
if(!ex.name?.includes('TimeoutError')) {
|
|
677
|
+
this.log(logLabel, `Returning false! Exception - ${ex}`);
|
|
678
|
+
}
|
|
679
|
+
this.endLog(logLabel);
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
this.endLog(logLabel);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async is_nth_element_present(element, n) {
|
|
687
|
+
const tppElement = new TppElement(element, this.page, this.fromAi);
|
|
688
|
+
try {
|
|
689
|
+
await tppElement.locator().nth(n).waitFor({state: "attached", timeout: 10000});
|
|
690
|
+
} catch (e) {
|
|
691
|
+
this.log(logLabel, `Returning false! Exception - ${e}`);
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async is_element_checked(element) {
|
|
698
|
+
const logLabel = `Is Element Checked - ${element.label}`;
|
|
699
|
+
this.startLog(logLabel);
|
|
700
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
701
|
+
const checked = await tppElement.locator().isChecked();
|
|
702
|
+
this.endLog(logLabel);
|
|
703
|
+
return checked;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async wait_for_single_element(element, timeOutSeconds) {
|
|
707
|
+
const tppElement = new TppElement(element, this.page, this.fromAi);
|
|
708
|
+
let count = await tppElement.locator().count();
|
|
709
|
+
let i = 0;
|
|
710
|
+
while(count != 1 && i < timeOutSeconds) {
|
|
711
|
+
await this.page.waitForTimeout(1000);
|
|
712
|
+
count = await tppElement.locator().count();
|
|
713
|
+
i++;
|
|
714
|
+
}
|
|
715
|
+
return count === 1;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
isNavigationError(exception) {
|
|
719
|
+
return exception?.message?.includes("Execution context was destroyed")
|
|
720
|
+
&& exception?.message?.includes("most likely because of a navigation");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// If expected count is 1, wait when multiple elements are matching
|
|
724
|
+
async tpp_element_or_backup(element, logLabel, expectedCount, timeOutSeconds, autohealCallback, ignoreFailure) {
|
|
725
|
+
let i = 0;
|
|
726
|
+
let ex;
|
|
727
|
+
let fallback = await this.getFallbackLocators(element);
|
|
728
|
+
if (fallback) {
|
|
729
|
+
this.log(logLabel, `Using cached fallback locator for element: ${element.label}`);
|
|
730
|
+
if (element.params) {
|
|
731
|
+
fallback.locator = fallback.locator.replace('${rowId}', element.params.rowId).replace('${colId}', element.params.colId);
|
|
732
|
+
fallback.backupLocators = fallback.backupLocators.map(backup => {
|
|
733
|
+
return {
|
|
734
|
+
...backup,
|
|
735
|
+
locator: backup.locator.replace('${rowId}', element.params.rowId).replace('${colId}', element.params.colId)
|
|
736
|
+
};
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return { autoheal: false, element: new TppElement(fallback, this.page, this.fromAi) };
|
|
740
|
+
}
|
|
741
|
+
while(i < 7) {
|
|
742
|
+
try {
|
|
743
|
+
let res = await this.getTppElementOrBackup(element, logLabel, expectedCount, timeOutSeconds);
|
|
744
|
+
if (res.success) {
|
|
745
|
+
return res;
|
|
746
|
+
} else {
|
|
747
|
+
ex = await this.throwElementNotPresent(element, logLabel, expectedCount === 1);
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
} catch (e) {
|
|
751
|
+
i++;
|
|
752
|
+
if(i < 7 && this.isNavigationError(e)) {
|
|
753
|
+
this.log(logLabel, `Page reloaded or redirected while checking for element. Retrying... #${i}`);
|
|
754
|
+
await sleep(2000);
|
|
755
|
+
} else {
|
|
756
|
+
ex = e;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (autohealCallback && element.snaps.length) {
|
|
762
|
+
this.log(logLabel, `${this.autohealLog(element)} :: [BEGIN]`);
|
|
763
|
+
const heal = await this.getAiAction(element, logLabel, ex ? ex.message: "" , autohealCallback);
|
|
764
|
+
if (!heal.success) {
|
|
765
|
+
this.failureLog(logLabel);
|
|
766
|
+
throw { message: `${ex?.message ? ex?.message + ".": "" } Fallback to auto-healing. Auto-heal Failed. AI Response - ${heal.message}` };
|
|
767
|
+
}
|
|
768
|
+
this.log(logLabel, `${this.autohealLog(element)} :: [END]`);
|
|
769
|
+
return { autoheal: true, element: undefined, message: heal.message || heal.verify };
|
|
770
|
+
}
|
|
771
|
+
if (!ignoreFailure && ex) {
|
|
772
|
+
this.failureLog(logLabel);
|
|
773
|
+
throw ex;
|
|
774
|
+
};
|
|
775
|
+
return { autoheal: false, element: new TppElement(element, this.page, this.fromAi) };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
autohealLog(element) {
|
|
779
|
+
return `Auto-healing for element: ${element.label}`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async getTppElementOrBackup(element, logLabel, expectedCount, timeOutSeconds) {
|
|
783
|
+
if(element.chainedLocator?.length || expectedCount === 0) {
|
|
784
|
+
return { success: true, element: new TppElement(element, this.page, this.fromAi) };
|
|
785
|
+
}
|
|
786
|
+
const checkFn = (ele) => expectedCount === 1
|
|
787
|
+
? this.wait_for_single_element(ele, timeOutSeconds || (element.backupLocators?.length ? 10 : this.customConfig?.timeouts?.actionTimeout || 30))
|
|
788
|
+
: this.is_nth_element_present(ele, 0);
|
|
789
|
+
|
|
790
|
+
if(await checkFn(element)) {
|
|
791
|
+
return { success: true, element: new TppElement(element, this.page, this.fromAi) };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
let i = 0;
|
|
795
|
+
let tppElement = new TppElement(element, this.page, this.fromAi);
|
|
796
|
+
while(i < element.backupLocators.length) {
|
|
797
|
+
new Logger(this.caller, this.blacklistedLogs).elementBackupRetryLog(tppElement, i, logLabel);
|
|
798
|
+
const backup = element.backupLocators[i]
|
|
799
|
+
tppElement = new TppElement(
|
|
800
|
+
{...element, ...backup},
|
|
801
|
+
this.page,
|
|
802
|
+
this.fromAi
|
|
803
|
+
);
|
|
804
|
+
if(await checkFn(tppElement.element)) {
|
|
805
|
+
new Logger(this.caller, this.blacklistedLogs).logBackupLocatorUsed(tppElement);
|
|
806
|
+
return { success: true, element: tppElement};
|
|
807
|
+
}
|
|
808
|
+
i++;
|
|
809
|
+
}
|
|
810
|
+
return { success: false, element: new TppElement(element, this.page, this.fromAi)}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async click_on_element(element) {
|
|
814
|
+
const logLabel = `Click on element - ${element.label}`;
|
|
815
|
+
this.startLog(logLabel);
|
|
816
|
+
let callback = async (x, y) => await this.autoheal.click_on_element(x, y, logLabel, element)
|
|
817
|
+
const { autoheal, element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
818
|
+
if (autoheal) {
|
|
819
|
+
this.endLog(logLabel);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
824
|
+
await tppElement.locator().click();
|
|
825
|
+
} catch (e) {
|
|
826
|
+
if(await tppElement.locator().isVisible() && await tppElement.locator().isEnabled()) {
|
|
827
|
+
try {
|
|
828
|
+
await tppElement.locator().click({force: true});
|
|
829
|
+
} catch (e) {
|
|
830
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
this.endLog(logLabel);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async click_on_element_if_present(element) {
|
|
840
|
+
const logLabel = `Click on element - ${element.label} if present`;
|
|
841
|
+
this.startLog(logLabel);
|
|
842
|
+
let tppElement;
|
|
843
|
+
try {
|
|
844
|
+
let { element: element1 } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, undefined, true);
|
|
845
|
+
tppElement = element1;
|
|
846
|
+
if(await tppElement.locator().count()>0) {
|
|
847
|
+
this.log(logLabel, 'Element present.');
|
|
848
|
+
try {
|
|
849
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
850
|
+
await tppElement.locator().click();
|
|
851
|
+
} catch (e) {
|
|
852
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
this.log(logLabel, 'Element not present.');
|
|
856
|
+
}
|
|
857
|
+
} catch (e) {
|
|
858
|
+
this.log(logLabel, 'Element not present.');
|
|
859
|
+
} finally {
|
|
860
|
+
this.endLog(logLabel);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async click_on_element_if_visible(element) {
|
|
866
|
+
const logLabel = `Click on element - ${element.label} if visible`;
|
|
867
|
+
this.startLog(logLabel);
|
|
868
|
+
let tppElement;
|
|
869
|
+
try {
|
|
870
|
+
const { element: element1 } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, undefined, true);
|
|
871
|
+
tppElement = element1;
|
|
872
|
+
await this.checkElementVisibility(tppElement, logLabel);
|
|
873
|
+
} catch (e) {
|
|
874
|
+
this.log(logLabel, 'Element is not visible.');
|
|
875
|
+
this.endLog(logLabel);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
this.log(logLabel, 'Element visible.');
|
|
879
|
+
try {
|
|
880
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
881
|
+
await tppElement.locator().click();
|
|
882
|
+
} catch (e) {
|
|
883
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
884
|
+
}
|
|
885
|
+
this.endLog(logLabel);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async click_on_nth_element(element, n) {
|
|
889
|
+
const logLabel = `Click on nth element - ${element.label}, n - ${n}`;
|
|
890
|
+
this.startLog(logLabel);
|
|
891
|
+
n = Number(n);
|
|
892
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 2);
|
|
893
|
+
try {
|
|
894
|
+
await tppElement.locator().nth(n).click();
|
|
895
|
+
} catch (e) {
|
|
896
|
+
if(await tppElement.locator().nth(n).isVisible() && await tppElement.locator().nth(n).isEnabled()) {
|
|
897
|
+
try {
|
|
898
|
+
await tppElement.locator().nth(n).click({force: true});
|
|
899
|
+
} catch (e) {
|
|
900
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
this.endLog(logLabel);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async double_click_on_element(element) {
|
|
910
|
+
const logLabel = `Double Click on element - ${element.label}`;
|
|
911
|
+
this.startLog(logLabel);
|
|
912
|
+
let callback = async (x, y) => await this.autoheal.double_click_on_element(x, y, logLabel, element)
|
|
913
|
+
const { autoheal, element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
914
|
+
if (autoheal) {
|
|
915
|
+
this.endLog(logLabel);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
920
|
+
await tppElement.locator().dblclick();
|
|
921
|
+
} catch (e) {
|
|
922
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
923
|
+
}
|
|
924
|
+
this.endLog(logLabel);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async right_click_on_element(element) {
|
|
928
|
+
const logLabel = `Right Click on element - ${element.label}`;
|
|
929
|
+
this.startLog(logLabel);
|
|
930
|
+
let callback = async (x, y) => await this.autoheal.right_click_on_element(x, y, logLabel, element)
|
|
931
|
+
const { autoheal, element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
932
|
+
if (autoheal) {
|
|
933
|
+
this.endLog(logLabel);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
938
|
+
await tppElement.locator().click({button: 'right'});
|
|
939
|
+
} catch (e) {
|
|
940
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
941
|
+
}
|
|
942
|
+
this.endLog(logLabel);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async click_at_point_on_element(x, y, element) {
|
|
946
|
+
const logLabel = `Click at point (${x}, ${y}) of element - ${element.label}`;
|
|
947
|
+
this.startLog(logLabel);
|
|
948
|
+
let callback = async (x, y) => await this.autoheal.click_on_element(x, y);
|
|
949
|
+
const { autoheal, element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
950
|
+
if (autoheal) {
|
|
951
|
+
this.endLog(logLabel);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
956
|
+
await tppElement.locator().waitFor({state: "visible"});
|
|
957
|
+
const box = await tppElement.locator().boundingBox();
|
|
958
|
+
if(!box) {
|
|
959
|
+
throw {message: 'Unable to find element position!'}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const clickX = x < 0 ? box.x + box.width + x : box.x + x;
|
|
963
|
+
const clickY = y < 0 ? box.y + box.height + y : box.y + y;
|
|
964
|
+
|
|
965
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
966
|
+
await page.mouse.click(clickX, clickY);
|
|
967
|
+
|
|
968
|
+
} catch (e) {
|
|
969
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
970
|
+
}
|
|
971
|
+
this.endLog(logLabel);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async click_at_point_on_element_and_move(x, y, toX, toY, element) {
|
|
975
|
+
const logLabel = `Click and hold at point (${x}, ${y}) move to (${toX}, ${toY}) on element - ${element.label} and release`;
|
|
976
|
+
this.startLog(logLabel);
|
|
977
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
978
|
+
const boundingBox = await tppElement.locator().boundingBox();
|
|
979
|
+
this.takeAutohealScreenshot(tppElement);
|
|
980
|
+
|
|
981
|
+
const startX = boundingBox.x + x;
|
|
982
|
+
const startY = boundingBox.y + y;
|
|
983
|
+
const endX = boundingBox.x + toX;
|
|
984
|
+
const endY = boundingBox.y + toY;
|
|
985
|
+
|
|
986
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
987
|
+
|
|
988
|
+
await page.mouse.move(startX, startY);
|
|
989
|
+
await page.mouse.down();
|
|
990
|
+
await page.mouse.move(endX, endY, { steps: 5 });
|
|
991
|
+
await page.mouse.up();
|
|
992
|
+
this.endLog(logLabel);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async right_click_at_point_on_element(x, y, element) {
|
|
996
|
+
const logLabel = `Right Click at point (${x}, ${y}) on element - ${element.label}`;
|
|
997
|
+
this.startLog(logLabel);
|
|
998
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
999
|
+
try {
|
|
1000
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1001
|
+
await tppElement.locator().click({button: 'right', position: {x: x, y: y}});
|
|
1002
|
+
} catch (e) {
|
|
1003
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
1004
|
+
}
|
|
1005
|
+
this.endLog(logLabel);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async double_click_at_point_on_element(x, y, element) {
|
|
1009
|
+
const logLabel = `Double Click at point (${x}, ${y}) on element - ${element.label}`;
|
|
1010
|
+
this.startLog(logLabel);
|
|
1011
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1012
|
+
try {
|
|
1013
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1014
|
+
await tppElement.locator().dblclick({position: {x: x, y: y}});
|
|
1015
|
+
} catch (e) {
|
|
1016
|
+
await this.handleClickException(e, tppElement, logLabel);
|
|
1017
|
+
}
|
|
1018
|
+
this.endLog(logLabel);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async input_value(value, element) {
|
|
1022
|
+
const logLabel = `Input - ${value}, on element ${element.label}`;
|
|
1023
|
+
this.startLog(logLabel);
|
|
1024
|
+
let callback = async (x, y) => await this.autoheal.input_value(value, x, y, logLabel, element)
|
|
1025
|
+
const { autoheal, element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1026
|
+
if (autoheal) {
|
|
1027
|
+
this.endLog(logLabel);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
try {
|
|
1031
|
+
const options = this.fromAi ? {delay: 50} : {};
|
|
1032
|
+
await tppElement.locator().type(`${value}`, {...options});
|
|
1033
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1036
|
+
}
|
|
1037
|
+
this.endLog(logLabel);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async type_text(text, userOptions) {
|
|
1041
|
+
const logLabel = `Type text - ${text}`;
|
|
1042
|
+
this.startLog(logLabel);
|
|
1043
|
+
const options = this.fromAi ? {delay: 50} : {};
|
|
1044
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
1045
|
+
await page.keyboard.type(`${text}`, {...options, ...(userOptions || {}) });
|
|
1046
|
+
this.endLog(logLabel);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async clear_text(element) {
|
|
1050
|
+
const logLabel = `Clear text from element ${element.label}`;
|
|
1051
|
+
this.startLog(logLabel);
|
|
1052
|
+
let callback = async (x, y) => await this.autoheal.clear_text(x, y, logLabel, element)
|
|
1053
|
+
const { autoheal, element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1054
|
+
if (autoheal) {
|
|
1055
|
+
this.endLog(logLabel);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
await tppElement.locator().clear();
|
|
1060
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1063
|
+
}
|
|
1064
|
+
this.endLog(logLabel);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async press_keys_on_element(keys, element) {
|
|
1068
|
+
const logLabel = `Press Keyboard Keys - ${keys}, on element ${element.label}`;
|
|
1069
|
+
this.startLog(logLabel);
|
|
1070
|
+
let callback = async (x, y) => await this.autoheal.press_keys_on_element(keys, x, y, logLabel, element)
|
|
1071
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1072
|
+
if (autoheal) {
|
|
1073
|
+
this.endLog(logLabel);
|
|
1074
|
+
return message;
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
await tppElement.locator().press(keys);
|
|
1078
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1081
|
+
}
|
|
1082
|
+
this.endLog(logLabel);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async press_keys(keys) {
|
|
1086
|
+
const logLabel = `Press Keyboard Keys - ${keys}`;
|
|
1087
|
+
this.startLog(logLabel);
|
|
1088
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
1089
|
+
await page.keyboard.press(keys);
|
|
1090
|
+
this.endLog(logLabel);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async select_value(value, element) {
|
|
1094
|
+
const logLabel = `Select - ${value}, on element ${element.label}`;
|
|
1095
|
+
this.startLog(logLabel);
|
|
1096
|
+
let callback = async (x, y) => await this.autoheal.select_value(value, x, y, logLabel, element)
|
|
1097
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1098
|
+
if (autoheal) {
|
|
1099
|
+
this.endLog(logLabel);
|
|
1100
|
+
return message;
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
const options = this.fromAi ? {delay: 50} : {};
|
|
1104
|
+
await tppElement.locator().selectOption(`${value}`, {...options});
|
|
1105
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1108
|
+
}
|
|
1109
|
+
this.endLog(logLabel);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
async pick_value_in_slider(element, value, start, end) {
|
|
1113
|
+
const logLabel = `Pick Value on slider element ${element.label}, value = ${value}, start = ${start}, end = ${end}`;
|
|
1114
|
+
this.startLog(logLabel);
|
|
1115
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1116
|
+
try {
|
|
1117
|
+
await new SliderUtils(this.page).pickValueInSlider(tppElement, value, start, end);
|
|
1118
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1121
|
+
}
|
|
1122
|
+
this.endLog(logLabel);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async pick_value_in_slider_with_step(element, value, start, end, step) {
|
|
1126
|
+
const logLabel = `Pick Value on slider element ${element.label}, value = ${value}, start = ${start}, end = ${end}, step = ${step}`;
|
|
1127
|
+
this.startLog(logLabel);
|
|
1128
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1129
|
+
try {
|
|
1130
|
+
await new SliderUtils(this.page).pickValueInSlider(tppElement, value, start, end, step);
|
|
1131
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1134
|
+
}
|
|
1135
|
+
this.endLog(logLabel);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async click_slider_at_ratio(element, ratio) {
|
|
1139
|
+
const logLabel = `Click slider element ${element.label}, at ratio ${ratio}`;
|
|
1140
|
+
this.startLog(logLabel);
|
|
1141
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1142
|
+
try {
|
|
1143
|
+
await new SliderUtils(this.page).clickSliderAtRatio(tppElement, ratio);
|
|
1144
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1147
|
+
}
|
|
1148
|
+
this.endLog(logLabel);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async set_geo_location(latitude, longitude) {
|
|
1152
|
+
const logLabel = `Set Geolocation - ${latitude} , ${longitude}`;
|
|
1153
|
+
this.startLog(logLabel);
|
|
1154
|
+
await this.page.context().setGeolocation({latitude: latitude, longitude: longitude});
|
|
1155
|
+
this.endLog(logLabel);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async set_element_attribute(key, value, element) {
|
|
1159
|
+
const logLabel = `Set attribute - ${key} with value - ${value}, on element ${element.label}`;
|
|
1160
|
+
this.startLog(logLabel);
|
|
1161
|
+
let callback = async (x, y) => await this.autoheal.set_element_attribute(key, value, x, y, logLabel, element)
|
|
1162
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1163
|
+
if (autoheal) {
|
|
1164
|
+
this.endLog(logLabel);
|
|
1165
|
+
return message;
|
|
1166
|
+
}
|
|
1167
|
+
try {
|
|
1168
|
+
await tppElement.locator().evaluate((node, {key, value}) => {
|
|
1169
|
+
node.setAttribute(key, value);
|
|
1170
|
+
}, {key, value});
|
|
1171
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1172
|
+
this.endLog(logLabel);
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
async get_element_attribute(key, element) {
|
|
1179
|
+
const logLabel = `Get attribute ${key} from element ${element.label}`;
|
|
1180
|
+
this.startLog(logLabel);
|
|
1181
|
+
let callback = async (x, y) => await this.autoheal.get_element_attribute(key, x, y, logLabel, element)
|
|
1182
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1183
|
+
if (autoheal) {
|
|
1184
|
+
this.endLog(logLabel);
|
|
1185
|
+
return message;
|
|
1186
|
+
}
|
|
1187
|
+
try {
|
|
1188
|
+
const val = await tppElement.locator().getAttribute(key, {timeout: 10000});
|
|
1189
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1190
|
+
this.endLog(logLabel);
|
|
1191
|
+
return val;
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async is_element_attribute_present(key, element) {
|
|
1198
|
+
const logLabel = `Is attribute ${key} present in element ${element.label}`;
|
|
1199
|
+
this.startLog(logLabel);
|
|
1200
|
+
let callback = async (x, y) => await this.autoheal.is_element_attribute_present(key, x, y, logLabel, element)
|
|
1201
|
+
const { autoheal, element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1202
|
+
if (autoheal) {
|
|
1203
|
+
this.endLog(logLabel);
|
|
1204
|
+
return callback;
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
const val = await tppElement.locator().getAttribute(key, {timeout: 10000});
|
|
1208
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1209
|
+
this.endLog(logLabel);
|
|
1210
|
+
return !!val;
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
async set_input_files(files, element) {
|
|
1217
|
+
const logLabel = `Set input files - ${files} on element ${element.label}`;
|
|
1218
|
+
this.startLog(logLabel);
|
|
1219
|
+
let callback = async (x, y) => await this.autoheal.set_input_files(files, x, y, logLabel, element)
|
|
1220
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1221
|
+
if (autoheal) {
|
|
1222
|
+
this.endLog(logLabel);
|
|
1223
|
+
return message;
|
|
1224
|
+
}
|
|
1225
|
+
const fileUtils = new FileUtils();
|
|
1226
|
+
const filePaths = files.map(file => fileUtils.getFilePath(file));
|
|
1227
|
+
try {
|
|
1228
|
+
await tppElement.locator().setInputFiles(filePaths);
|
|
1229
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1232
|
+
}
|
|
1233
|
+
this.endLog(logLabel);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async scroll_element_into_view(element) {
|
|
1237
|
+
const logLabel = `Scroll element ${element.label} into view`;
|
|
1238
|
+
this.startLog(logLabel);
|
|
1239
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1240
|
+
try {
|
|
1241
|
+
await tppElement.locator().scrollIntoViewIfNeeded();
|
|
1242
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1245
|
+
}
|
|
1246
|
+
this.endLog(logLabel);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
isWebKitMobile() {
|
|
1250
|
+
let browserConfig = this.tppTest.info()?.project?.use;
|
|
1251
|
+
return (browserConfig?.isMobile || browserConfig?.hasTouch) && browserConfig?.defaultBrowserType === 'webkit';
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
async scroll_to_end_of_page() {
|
|
1255
|
+
const logLabel = 'Scroll to end of the page';
|
|
1256
|
+
this.startLog(logLabel);
|
|
1257
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
1258
|
+
|
|
1259
|
+
if(this.isWebKitMobile()) {
|
|
1260
|
+
await page.evaluate(() => window.scrollTo(0, 20000));
|
|
1261
|
+
} else {
|
|
1262
|
+
await page.mouse.wheel(0, 20000); //TODO check if there is any other approach
|
|
1263
|
+
}
|
|
1264
|
+
this.endLog(logLabel);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async scroll_by_xy(x, y) {
|
|
1268
|
+
const logLabel = `Scroll page by pixels x: ${x}, y: ${y}`;
|
|
1269
|
+
this.startLog(logLabel);
|
|
1270
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
1271
|
+
if(this.isWebKitMobile()) {
|
|
1272
|
+
await page.evaluate(([x, y]) => window.scrollTo(x, y), [x, y]);
|
|
1273
|
+
} else {
|
|
1274
|
+
await page.mouse.wheel(x, y);
|
|
1275
|
+
}
|
|
1276
|
+
this.endLog(logLabel);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
async hover_on_element(element) {
|
|
1280
|
+
const logLabel = `Hover on element - ${element.label}`;
|
|
1281
|
+
this.startLog(logLabel);
|
|
1282
|
+
let callback = async (x, y) => await this.autoheal.hover_on_element(x, y, logLabel, element)
|
|
1283
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1284
|
+
if (autoheal) {
|
|
1285
|
+
this.endLog(logLabel);
|
|
1286
|
+
return message;
|
|
1287
|
+
}
|
|
1288
|
+
try {
|
|
1289
|
+
await tppElement.locator().hover();
|
|
1290
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1291
|
+
} catch (e) {
|
|
1292
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1293
|
+
}
|
|
1294
|
+
this.endLog(logLabel);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
async drag_and_drop_on_element(element, targetElement) {
|
|
1298
|
+
const logLabel = `Drag element - ${element.label} and drop on - ${targetElement.label}`;
|
|
1299
|
+
this.startLog(logLabel);
|
|
1300
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1301
|
+
const { element: tppTargetElement } = await this.tpp_element_or_backup(targetElement, logLabel, 1);
|
|
1302
|
+
await tppElement.locator().dragTo(tppTargetElement.locator());
|
|
1303
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1304
|
+
await this.takeAutohealScreenshot(tppTargetElement);
|
|
1305
|
+
this.endLog(logLabel);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
async switch_to_iframe(element) {
|
|
1309
|
+
const logLabel = `Switch to iframe - ${element.label}`;
|
|
1310
|
+
this.startLog(logLabel);
|
|
1311
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1312
|
+
const iframe = await this.page.waitForSelector(tppElement.getSelector());
|
|
1313
|
+
// Switch to the iframe context
|
|
1314
|
+
const frame = await iframe.contentFrame();
|
|
1315
|
+
if(!frame) {
|
|
1316
|
+
this.failureLog(logLabel);
|
|
1317
|
+
throw {
|
|
1318
|
+
message: `iframe is not loaded: ${tppElement.element.label}`,
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
// Wait for the iframe to be fully loaded
|
|
1322
|
+
await frame.waitForLoadState('load');
|
|
1323
|
+
this.page = frame;
|
|
1324
|
+
this.endLog(logLabel);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async switch_to_embed(element) {
|
|
1328
|
+
const logLabel = `Switch to embed - ${element.label}`;
|
|
1329
|
+
this.startLog(logLabel);
|
|
1330
|
+
|
|
1331
|
+
let newClassName;
|
|
1332
|
+
const existingClassMapping = this.embedToIframeMap?.find(classMap => classMap.element === element.label);
|
|
1333
|
+
if(existingClassMapping) {
|
|
1334
|
+
newClassName = existingClassMapping.frameClass;
|
|
1335
|
+
} else {
|
|
1336
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1337
|
+
const embed = await this.page.waitForSelector(tppElement.getSelector());
|
|
1338
|
+
newClassName = `devassure_embed_iframe_${Date.now()}`;
|
|
1339
|
+
|
|
1340
|
+
//Changing the embed to an iframe element
|
|
1341
|
+
await embed.evaluate((node, {newClassName}) => {
|
|
1342
|
+
const iframe = document.createElement('iframe');
|
|
1343
|
+
for (const attr of node.attributes) {
|
|
1344
|
+
iframe.setAttribute(attr.name, attr.value);
|
|
1345
|
+
}
|
|
1346
|
+
iframe.className = node.className;
|
|
1347
|
+
iframe.classList.add(newClassName);
|
|
1348
|
+
iframe.innerHTML = node.innerHTML;
|
|
1349
|
+
node.replaceWith(iframe);
|
|
1350
|
+
}, {newClassName});
|
|
1351
|
+
this.embedToIframeMap = [...(this.embedToIframeMap || []), {element: element.label, frameClass: newClassName}];
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Switch to the iframe context
|
|
1355
|
+
let iframe = await this.page.waitForSelector(`.${newClassName}`);
|
|
1356
|
+
const frame = await iframe.contentFrame();
|
|
1357
|
+
if(!frame) {
|
|
1358
|
+
this.failureLog(logLabel);
|
|
1359
|
+
throw {
|
|
1360
|
+
message: `Failed swtiching to embed!`,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
// Wait for the iframe to be fully loaded
|
|
1364
|
+
await frame.waitForLoadState('load');
|
|
1365
|
+
this.page = frame;
|
|
1366
|
+
this.endLog(logLabel);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async switch_to_parent_frame() {
|
|
1370
|
+
const logLabel = `Switch to Parent frame`;
|
|
1371
|
+
this.startLog(logLabel);
|
|
1372
|
+
this.page = this.page.page();
|
|
1373
|
+
await this.page.waitForLoadState('load');
|
|
1374
|
+
this.endLog(logLabel);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
async open_new_tab() {
|
|
1378
|
+
const logLabel = `Open new Tab`;
|
|
1379
|
+
this.startLog(logLabel);
|
|
1380
|
+
this.page = await this.page.context().newPage();
|
|
1381
|
+
this.library.setPage(this.page);
|
|
1382
|
+
this.endLog(logLabel);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
async close_current_tab() {
|
|
1386
|
+
const logLabel = `Close current tab`;
|
|
1387
|
+
this.startLog(logLabel);
|
|
1388
|
+
await this.page.close();
|
|
1389
|
+
const pages = this.page.context().pages();
|
|
1390
|
+
if(pages.length > 0) {
|
|
1391
|
+
this.page = pages[0];
|
|
1392
|
+
this.library.setPage(this.page);
|
|
1393
|
+
}
|
|
1394
|
+
this.endLog(logLabel);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
//Index starts from 1
|
|
1398
|
+
async close_tab(index) {
|
|
1399
|
+
const logLabel = `Close tab ${index}`;
|
|
1400
|
+
this.startLog(logLabel);
|
|
1401
|
+
let pages = this.page.context().pages();
|
|
1402
|
+
if(pages.length < index) {
|
|
1403
|
+
this.failureLog(logLabel);
|
|
1404
|
+
throw {
|
|
1405
|
+
message: `Only ${pages.length} tabs are opened ---- Trying to close tab with index ${index}`,
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
const isCurrentPage = this.page === pages[index-1];
|
|
1409
|
+
await pages[index-1].close();
|
|
1410
|
+
pages = this.page.context().pages();
|
|
1411
|
+
if(pages.length > 0 && isCurrentPage) {
|
|
1412
|
+
this.page = pages[0];
|
|
1413
|
+
this.library.setPage(this.page);
|
|
1414
|
+
}
|
|
1415
|
+
this.endLog(logLabel);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
//Index starts from 1
|
|
1419
|
+
async switch_to_tab_i(index) {
|
|
1420
|
+
const logLabel = `Switch to Tab - ${index}`;
|
|
1421
|
+
this.startLog(logLabel);
|
|
1422
|
+
let pages = [];
|
|
1423
|
+
const timeout = this.customConfig?.timeouts?.urlLoadTimeout || 30;
|
|
1424
|
+
let count = 0;
|
|
1425
|
+
while(pages.length < index) {
|
|
1426
|
+
if(count === timeout) {
|
|
1427
|
+
this.failureLog(logLabel);
|
|
1428
|
+
throw {
|
|
1429
|
+
message: `Only ${pages.length} tabs are opened ---- Trying to switch to tab with index ${index}`,
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
pages = this.page.context().pages();
|
|
1433
|
+
await this.page.waitForTimeout(1000);
|
|
1434
|
+
count++;
|
|
1435
|
+
}
|
|
1436
|
+
this.page = pages[index-1];
|
|
1437
|
+
this.library.setPage(this.page);
|
|
1438
|
+
await this.page.waitForLoadState('load');
|
|
1439
|
+
this.endLog(logLabel);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
async switch_to_tab_title(title) {
|
|
1443
|
+
const logLabel = `Switch to Tab - ${title}`;
|
|
1444
|
+
this.startLog(logLabel);
|
|
1445
|
+
let pages = [];
|
|
1446
|
+
const timeout = this.customConfig?.timeouts?.urlLoadTimeout || 30;
|
|
1447
|
+
let count = 0;
|
|
1448
|
+
while(true) {
|
|
1449
|
+
let success;
|
|
1450
|
+
for(let i=0; i < pages.length; i++) {
|
|
1451
|
+
try {
|
|
1452
|
+
if(await pages[i].title() === title) {
|
|
1453
|
+
this.page = pages[i];
|
|
1454
|
+
this.library.setPage(this.page);
|
|
1455
|
+
success = true;
|
|
1456
|
+
break;
|
|
1457
|
+
}
|
|
1458
|
+
} catch(e) {
|
|
1459
|
+
if(this.isNavigationError(e)) {
|
|
1460
|
+
this.log(logLabel, `Page reloaded or redirected while checking for title. Retrying...`);
|
|
1461
|
+
await sleep(2000);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if(success) break;
|
|
1466
|
+
if(count === timeout) {
|
|
1467
|
+
this.failureLog(logLabel);
|
|
1468
|
+
throw {
|
|
1469
|
+
message: `Tab with title ${title} is not present`,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
pages = this.page.context().pages();
|
|
1473
|
+
await this.page.waitForTimeout(1000);
|
|
1474
|
+
count++;
|
|
1475
|
+
}
|
|
1476
|
+
this.endLog(logLabel);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
async get_tab_names() {
|
|
1480
|
+
const logLabel = `Get tab names`;
|
|
1481
|
+
this.startLog(logLabel);
|
|
1482
|
+
const pages = this.page.context().pages();
|
|
1483
|
+
const titles = await Promise.all(pages.map(page => page.title()));
|
|
1484
|
+
this.endLog(logLabel);
|
|
1485
|
+
return titles;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async get_tab_count() {
|
|
1489
|
+
const logLabel = `Get tab count`;
|
|
1490
|
+
this.startLog(logLabel);
|
|
1491
|
+
const count = this.page.context().pages().length;
|
|
1492
|
+
this.endLog(logLabel);
|
|
1493
|
+
return count;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async open_new_window() {
|
|
1497
|
+
const logLabel = `Open new Window`;
|
|
1498
|
+
this.startLog(logLabel);
|
|
1499
|
+
const browser = this.page.context().browser();
|
|
1500
|
+
if(!browser) {
|
|
1501
|
+
this.failureLog(logLabel);
|
|
1502
|
+
throw {
|
|
1503
|
+
message: `Error opening a new window`,
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
if(this.remoteBrowserPort) {
|
|
1507
|
+
console.log(`[[TPP]] [[TPP Remote-Execution COMMAND launch_new_window]]`);
|
|
1508
|
+
await sleep(4000);
|
|
1509
|
+
this.page = browser.contexts()?.at(-1)?.pages()?.at(0);
|
|
1510
|
+
this.library.setPage(this.page);
|
|
1511
|
+
} else {
|
|
1512
|
+
|
|
1513
|
+
let recordProp = {};
|
|
1514
|
+
const recordVideo = this.customConfig?.advancedRunConfig?.recordVideo;
|
|
1515
|
+
if(!!recordVideo?.trim() && recordVideo !== 'OFF') {
|
|
1516
|
+
recordProp = {recordVideo: {dir: 'tpp_temp_videos'}};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
this.page = await (await browser.newContext({
|
|
1520
|
+
ignoreHTTPSErrors: true,
|
|
1521
|
+
...recordProp,
|
|
1522
|
+
})).newPage();
|
|
1523
|
+
this.library.setPage(this.page);
|
|
1524
|
+
}
|
|
1525
|
+
this.networkLogManager?.startLogTracing(this.page);
|
|
1526
|
+
this.consoleLogManager?.startLogTracing(this.page);
|
|
1527
|
+
this.downloadsManager?.handleDownloads(this.page);
|
|
1528
|
+
this.endLog(logLabel);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
async close_current_window() {
|
|
1532
|
+
const logLabel = `Close current Window`;
|
|
1533
|
+
this.startLog(logLabel);
|
|
1534
|
+
const browser = this.page.context().browser();
|
|
1535
|
+
if(!browser) {
|
|
1536
|
+
this.failureLog(logLabel);
|
|
1537
|
+
throw {
|
|
1538
|
+
message: `Error closing window`,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
await this.page.context().close();
|
|
1542
|
+
const contexts = browser.contexts();
|
|
1543
|
+
if(contexts.length > 0 && contexts[0]?.pages().length > 0) {
|
|
1544
|
+
this.page = contexts[0].pages()[0];
|
|
1545
|
+
this.library.setPage(this.page);
|
|
1546
|
+
}
|
|
1547
|
+
this.endLog(logLabel);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
//Index starts from 1
|
|
1551
|
+
async close_window(index) {
|
|
1552
|
+
const logLabel = `Close Window ${index}`;
|
|
1553
|
+
this.startLog(logLabel);
|
|
1554
|
+
const browser = this.page.context().browser();
|
|
1555
|
+
if(!browser) {
|
|
1556
|
+
this.failureLog(logLabel);
|
|
1557
|
+
throw {
|
|
1558
|
+
message: `Error closing window`,
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
let contexts = browser.contexts();
|
|
1562
|
+
if(contexts.length < index) {
|
|
1563
|
+
this.failureLog(logLabel);
|
|
1564
|
+
throw {
|
|
1565
|
+
message: `Only ${pages.length} windows are opened ---- Trying to close window with index ${index}`,
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
const isCurrentContext = this.page.context() === contexts[index-1];
|
|
1569
|
+
await contexts[index - 1].close();
|
|
1570
|
+
contexts = browser.contexts();
|
|
1571
|
+
if(isCurrentContext && contexts.length > 0 && contexts[0]?.pages().length > 0) {
|
|
1572
|
+
this.page = contexts[0].pages()[0];
|
|
1573
|
+
this.library.setPage(this.page);
|
|
1574
|
+
}
|
|
1575
|
+
this.endLog(logLabel);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
async close_all_windows() {
|
|
1579
|
+
const logLabel = `Close All Windows`;
|
|
1580
|
+
this.startLog(logLabel);
|
|
1581
|
+
const browser = this.page.context().browser();
|
|
1582
|
+
if(!browser) {
|
|
1583
|
+
this.failureLog(logLabel);
|
|
1584
|
+
throw {
|
|
1585
|
+
message: `Error closing windows`,
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
let contexts = browser.contexts();
|
|
1589
|
+
for (let i = 0; i < contexts.length; i++) {
|
|
1590
|
+
await contexts[i].close();
|
|
1591
|
+
}
|
|
1592
|
+
this.endLog(logLabel);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async switch_to_window_i(index) {
|
|
1596
|
+
const logLabel = `Switch to Window - ${index}`;
|
|
1597
|
+
this.startLog(logLabel);
|
|
1598
|
+
const browser = this.page.context().browser();
|
|
1599
|
+
if(!browser) {
|
|
1600
|
+
this.failureLog(logLabel);
|
|
1601
|
+
throw {
|
|
1602
|
+
message: `Error siwtching windows`,
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
let contexts = [];
|
|
1606
|
+
const timeout = this.customConfig?.timeouts?.urlLoadTimeout || 30;
|
|
1607
|
+
let count = 0;
|
|
1608
|
+
while(contexts.length < index) {
|
|
1609
|
+
if(count === timeout) {
|
|
1610
|
+
this.failureLog(logLabel);
|
|
1611
|
+
throw {
|
|
1612
|
+
message: `Only ${pages.length} windows are opened ---- Trying to switch to window with index ${index}`,
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
contexts = browser.contexts();
|
|
1616
|
+
await this.page.waitForTimeout(1000);
|
|
1617
|
+
count++;
|
|
1618
|
+
}
|
|
1619
|
+
this.page = contexts[index-1].pages()[0];
|
|
1620
|
+
this.library.setPage(this.page);
|
|
1621
|
+
await this.page.waitForLoadState('load');
|
|
1622
|
+
this.endLog(logLabel);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
async addDialogListener() {
|
|
1626
|
+
this.dialogFns = [];
|
|
1627
|
+
this.page.once('dialog', dialog => {
|
|
1628
|
+
this.dialogFns?.forEach(async fn => fn(dialog));
|
|
1629
|
+
this.dialogFns = undefined;
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async wait_for_dialog() {
|
|
1634
|
+
const logLabel = `Wait for dialog`;
|
|
1635
|
+
this.startLog(logLabel);
|
|
1636
|
+
let timeout = this.customConfig?.timeouts?.actionTimeout || 30;
|
|
1637
|
+
while(!!this.dialogFns) {
|
|
1638
|
+
if(count === timeout) {
|
|
1639
|
+
this.failureLog(logLabel);
|
|
1640
|
+
throw {
|
|
1641
|
+
message: `Error waiting for dialog. Dialog was not opened after ${timeout}s!`
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
await this.page.waitForTimeout(1000);
|
|
1645
|
+
count++;
|
|
1646
|
+
}
|
|
1647
|
+
this.endLog(logLabel);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
async dismiss_dialog() {
|
|
1651
|
+
if(!this.dialogFns) this.addDialogListener();
|
|
1652
|
+
this.dialogFns.push((dialog) => {
|
|
1653
|
+
const logLabel = `Dismiss Dialog`;
|
|
1654
|
+
this.startLog(logLabel);
|
|
1655
|
+
dialog.dismiss().catch(() => {})
|
|
1656
|
+
this.endLog(logLabel);
|
|
1657
|
+
})
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
async accept_dialog() {
|
|
1661
|
+
if(!this.dialogFns) this.addDialogListener();
|
|
1662
|
+
this.dialogFns.push((dialog) => {
|
|
1663
|
+
const logLabel = `Accept Dialog`;
|
|
1664
|
+
this.startLog(logLabel);
|
|
1665
|
+
dialog.accept().catch(() => {})
|
|
1666
|
+
this.endLog(logLabel);
|
|
1667
|
+
})
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
async accept_dialog_with_input(input) {
|
|
1671
|
+
if(!this.dialogFns) this.addDialogListener();
|
|
1672
|
+
this.dialogFns.push((dialog) => {
|
|
1673
|
+
const logLabel = `Accept Dialog with input - ${input}`;
|
|
1674
|
+
this.startLog(logLabel);
|
|
1675
|
+
dialog.accept(input).catch(() => {})
|
|
1676
|
+
this.endLog(logLabel);
|
|
1677
|
+
})
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
async verify_dialog_message(comparator, expected) {
|
|
1681
|
+
if(!this.dialogFns) this.addDialogListener();
|
|
1682
|
+
this.dialogFns.push((dialog) => {
|
|
1683
|
+
const logLabel = `Validate Dialog message, ${comparator}, expected: ${expected}`;
|
|
1684
|
+
this.startLog(logLabel);
|
|
1685
|
+
const msg = dialog.message();
|
|
1686
|
+
if(!compare(msg, comparator, expected)) {
|
|
1687
|
+
this.failureLog(logLabel);
|
|
1688
|
+
throw {
|
|
1689
|
+
message: `Dialog message is not as expected. actual: ${msg} ---- comparator: ${comparator} ---- expected: ${expected}`,
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
this.endLog(logLabel);
|
|
1693
|
+
})
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
async get_input_element_value(element) {
|
|
1697
|
+
const logLabel = `Get value of input element- ${element.label}`;
|
|
1698
|
+
this.startLog(logLabel);
|
|
1699
|
+
let callback = async (x, y) => await this.autoheal.get_input_element_value(x, y, logLabel, element)
|
|
1700
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1701
|
+
if (autoheal) {
|
|
1702
|
+
this.endLog(logLabel);
|
|
1703
|
+
return message;
|
|
1704
|
+
}
|
|
1705
|
+
const value = await tppElement.locator().inputValue();
|
|
1706
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1707
|
+
this.endLog(logLabel);
|
|
1708
|
+
return value;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
async get_element_text(element) {
|
|
1712
|
+
const logLabel = `Get text of - ${element.label}`;
|
|
1713
|
+
this.startLog(logLabel);
|
|
1714
|
+
let callback = async (x, y) => await this.autoheal.get_element_text(x, y, logLabel, element)
|
|
1715
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
1716
|
+
if (autoheal) {
|
|
1717
|
+
this.endLog(logLabel);
|
|
1718
|
+
return message;
|
|
1719
|
+
}
|
|
1720
|
+
const text = await tppElement.locator().innerText();
|
|
1721
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1722
|
+
this.endLog(logLabel);
|
|
1723
|
+
return text;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
async get_element_count(element) {
|
|
1727
|
+
const logLabel = `Get count of - ${element.label}`;
|
|
1728
|
+
this.startLog(logLabel);
|
|
1729
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 0);
|
|
1730
|
+
const count = await tppElement.locator().count();
|
|
1731
|
+
this.endLog(logLabel);
|
|
1732
|
+
return count;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
async get_element_dimensions(element) {
|
|
1736
|
+
const logLabel = `Get dimensions of - ${element.label}`;
|
|
1737
|
+
this.startLog(logLabel);
|
|
1738
|
+
let callback = async (x, y) => await this.autoheal.get_element_dimensions(x, y, logLabel, element)
|
|
1739
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 0, undefined, callback);
|
|
1740
|
+
if (autoheal) {
|
|
1741
|
+
this.endLog(logLabel);
|
|
1742
|
+
return message;
|
|
1743
|
+
}
|
|
1744
|
+
const dimensions = await tppElement.locator().boundingBox();
|
|
1745
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1746
|
+
this.endLog(logLabel);
|
|
1747
|
+
return dimensions;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
get_nth_element(element, n) {
|
|
1751
|
+
const logLabel = `Get nth element of ${element.label}. n = ${n}`;
|
|
1752
|
+
n = Number(n);
|
|
1753
|
+
this.startLog(logLabel);
|
|
1754
|
+
if(n < 0) {
|
|
1755
|
+
this.failureLog(logLabel);
|
|
1756
|
+
throw {
|
|
1757
|
+
message: `index n = ${n} is invalid. should be >= 0`,
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
this.endLog(logLabel);
|
|
1761
|
+
return {...element, index: n};
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
async verify_element_text(element, text) {
|
|
1765
|
+
const logLabel = `Verify if text of - ${element.label} == ${text.replaceAll("\n", " ")}`;
|
|
1766
|
+
this.startLog(logLabel);
|
|
1767
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, () => {});
|
|
1768
|
+
if (autoheal) {
|
|
1769
|
+
this.endLog(logLabel);
|
|
1770
|
+
return message;
|
|
1771
|
+
}
|
|
1772
|
+
try {
|
|
1773
|
+
let timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
1774
|
+
await expect(tppElement.locator()).toHaveText(text, {timeout: timeout});
|
|
1775
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1776
|
+
} catch (e) {
|
|
1777
|
+
await this.checkElementVisibility(tppElement, logLabel);
|
|
1778
|
+
this.handleExpectException(e, logLabel)
|
|
1779
|
+
}
|
|
1780
|
+
this.endLog(logLabel);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
async verify_if_element_present(element) {
|
|
1784
|
+
const logLabel = `Verify if element - ${element.label} is present`;
|
|
1785
|
+
this.startLog(logLabel);
|
|
1786
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, () => {});
|
|
1787
|
+
if (autoheal) {
|
|
1788
|
+
this.endLog(logLabel);
|
|
1789
|
+
return message;
|
|
1790
|
+
}
|
|
1791
|
+
try {
|
|
1792
|
+
await tppElement.locator().waitFor({state: "attached"});
|
|
1793
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1794
|
+
} catch (e) {
|
|
1795
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1796
|
+
}
|
|
1797
|
+
this.endLog(logLabel);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
async verify_if_element_visible(element) {
|
|
1801
|
+
const logLabel = `Verify if element - ${element.label} is visible`;
|
|
1802
|
+
this.startLog(logLabel);
|
|
1803
|
+
const { autoheal, element: tppElement, message } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, () => {});
|
|
1804
|
+
if (autoheal) {
|
|
1805
|
+
this.endLog(logLabel);
|
|
1806
|
+
return message;
|
|
1807
|
+
}
|
|
1808
|
+
try {
|
|
1809
|
+
await tppElement.locator().waitFor({state: "visible"});
|
|
1810
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1811
|
+
} catch (e) {
|
|
1812
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1813
|
+
}
|
|
1814
|
+
this.endLog(logLabel);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
handle_disappear_exceptiom(exception, tppElement, logLabel) {
|
|
1818
|
+
const exceptionLines = exception.toString().split('\n');
|
|
1819
|
+
const message = exceptionLines[0].replace(ansiRegex(), '');
|
|
1820
|
+
if(message.includes('toHaveCount')) {
|
|
1821
|
+
this.failureLog(logLabel);
|
|
1822
|
+
throw {
|
|
1823
|
+
message: `Element is present -- ${exceptionLines[2]} -- ${exceptionLines[4]?.replace(ansiRegex(), '')}`,
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
this.handleExpectException(exception, tppElement, logLabel);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
async wait_for_element_to_disappear(element) {
|
|
1830
|
+
const logLabel = `Wait for elment - ${element.label} to disappear`;
|
|
1831
|
+
this.startLog(logLabel);
|
|
1832
|
+
const tppElement = new TppElement(element, this.page, this.fromAi);
|
|
1833
|
+
try {
|
|
1834
|
+
let timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
1835
|
+
await expect(tppElement.locator()).toHaveCount(0, {timeout: timeout});
|
|
1836
|
+
} catch (e) {
|
|
1837
|
+
this.handle_disappear_exceptiom(e, tppElement, logLabel);
|
|
1838
|
+
}
|
|
1839
|
+
this.endLog(logLabel);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
async wait_for_element_to_disappear_ms(element, duration) {
|
|
1843
|
+
const logLabel = `Wait for elment - ${element.label} to disappear within ${duration} ms`;
|
|
1844
|
+
this.startLog(logLabel);
|
|
1845
|
+
const tppElement = new TppElement(element, this.page, this.fromAi);
|
|
1846
|
+
try {
|
|
1847
|
+
await expect(tppElement.locator()).toHaveCount(0, {timeout: duration});
|
|
1848
|
+
} catch (e) {
|
|
1849
|
+
this.handle_disappear_exceptiom(e, tppElement, logLabel);
|
|
1850
|
+
}
|
|
1851
|
+
this.endLog(logLabel);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
async wait_for_element(element) {
|
|
1855
|
+
const logLabel = `Wait for - ${element.label} to be present`;
|
|
1856
|
+
this.startLog(logLabel);
|
|
1857
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1858
|
+
try {
|
|
1859
|
+
await tppElement.locator().waitFor({state: "attached"});
|
|
1860
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1861
|
+
} catch (e) {
|
|
1862
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1863
|
+
}
|
|
1864
|
+
this.endLog(logLabel);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
async wait_for_element_ms(element, duration) {
|
|
1868
|
+
const logLabel = `Wait for - ${element.label} to be present within ${duration} ms`;
|
|
1869
|
+
this.startLog(logLabel);
|
|
1870
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, duration/1000);
|
|
1871
|
+
try {
|
|
1872
|
+
await tppElement.locator().waitFor({state: "attached", timeout: duration});
|
|
1873
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1874
|
+
} catch (e) {
|
|
1875
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1876
|
+
}
|
|
1877
|
+
this.endLog(logLabel);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
async wait_for_element_visible(element) {
|
|
1881
|
+
const logLabel = `Wait for - ${element.label} to be visible`;
|
|
1882
|
+
this.startLog(logLabel);
|
|
1883
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1884
|
+
try {
|
|
1885
|
+
await tppElement.locator().waitFor({state: "visible"});
|
|
1886
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1887
|
+
} catch (e) {
|
|
1888
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1889
|
+
}
|
|
1890
|
+
this.endLog(logLabel);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
async wait_for_element_visible_ms(element, duration) {
|
|
1894
|
+
const logLabel = `Wait for - ${element.label} to be visible within ${duration} ms`;
|
|
1895
|
+
this.startLog(logLabel);
|
|
1896
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, duration/1000);
|
|
1897
|
+
try {
|
|
1898
|
+
await tppElement.locator().waitFor({state: "visible", timeout: duration});
|
|
1899
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1900
|
+
} catch (e) {
|
|
1901
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
1902
|
+
}
|
|
1903
|
+
this.endLog(logLabel);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
async verify_element_count(element, count) {
|
|
1907
|
+
const logLabel = `Verify if count of - ${element.label} == ${count}`;
|
|
1908
|
+
this.startLog(logLabel);
|
|
1909
|
+
count = Number(count);
|
|
1910
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, count);
|
|
1911
|
+
try {
|
|
1912
|
+
let timeout = (this.customConfig?.timeouts?.actionTimeout || 30) * 1000;
|
|
1913
|
+
await expect(tppElement.locator()).toHaveCount(count, {timeout: timeout});
|
|
1914
|
+
} catch (e) {
|
|
1915
|
+
this.handleExpectException(e, logLabel)
|
|
1916
|
+
}
|
|
1917
|
+
this.endLog(logLabel);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
async get_page_title() {
|
|
1921
|
+
const logLabel = `Get page title`;
|
|
1922
|
+
this.startLog(logLabel);
|
|
1923
|
+
let retry = 0;
|
|
1924
|
+
let title;
|
|
1925
|
+
while(retry < 7) {
|
|
1926
|
+
try {
|
|
1927
|
+
title = await this.page.title();
|
|
1928
|
+
} catch(e) {
|
|
1929
|
+
if(this.isNavigationError(e)) {
|
|
1930
|
+
this.log(logLabel, `Page reloaded or redirected while checking for title. Retrying...`);
|
|
1931
|
+
await sleep(2000);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
retry++;
|
|
1935
|
+
await sleep(1000);
|
|
1936
|
+
}
|
|
1937
|
+
if(title == null) {
|
|
1938
|
+
this.failureLog(logLabel);
|
|
1939
|
+
throw {message: `Error getting page title`};
|
|
1940
|
+
}
|
|
1941
|
+
this.endLog(logLabel);
|
|
1942
|
+
return title;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
async verify_page_title(text) {
|
|
1946
|
+
const logLabel = `Verify if title of page is equal to ${text}`;
|
|
1947
|
+
this.startLog(logLabel);
|
|
1948
|
+
try {
|
|
1949
|
+
let timeout = (this.customConfig?.timeouts?.urlLoadTimeout || 30) * 1000;
|
|
1950
|
+
await expect(this.page).toHaveTitle(text, {timeout: timeout});
|
|
1951
|
+
} catch (e) {
|
|
1952
|
+
this.failureLog(logLabel);
|
|
1953
|
+
throw {
|
|
1954
|
+
message: `Text is not as expected. actual: ${await this.page.title()} ---- expected: ${text}`
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
this.endLog(logLabel);
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
async verify_page_a11y() {
|
|
1961
|
+
const logLabel = `Verify accessibility of page`;
|
|
1962
|
+
this.startLog(logLabel);
|
|
1963
|
+
try {
|
|
1964
|
+
await new TppA11y(this.page, this.caller).analysePage();
|
|
1965
|
+
} catch (ex) {
|
|
1966
|
+
this.failureLog(logLabel);
|
|
1967
|
+
throw ex;
|
|
1968
|
+
}
|
|
1969
|
+
this.endLog(logLabel);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
async verify_page_a11y_soft_assert(softAssert, assertMessage) {
|
|
1973
|
+
const logLabel = `Soft Assert accessibility of page`;
|
|
1974
|
+
this.startLog(logLabel);
|
|
1975
|
+
try {
|
|
1976
|
+
let logMessage = (msg) => new Logger(this.caller, this.blacklistedLogs).log(logLabel, msg);
|
|
1977
|
+
await new TppA11y(this.page, this.caller, {softAssert, assertMessage, logMessage}).analysePage();
|
|
1978
|
+
} catch (ex) {
|
|
1979
|
+
this.failureLog(logLabel);
|
|
1980
|
+
throw ex;
|
|
1981
|
+
}
|
|
1982
|
+
this.endLog(logLabel);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
async verify_element_a11y(element) {
|
|
1986
|
+
const logLabel = `Verify accessibility of ${element.label}`;
|
|
1987
|
+
this.startLog(logLabel);
|
|
1988
|
+
try {
|
|
1989
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
1990
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
1991
|
+
await new TppA11y(this.page, this.caller).analyseElement(tppElement);
|
|
1992
|
+
} catch (ex) {
|
|
1993
|
+
this.failureLog(logLabel);
|
|
1994
|
+
throw ex;
|
|
1995
|
+
}
|
|
1996
|
+
this.endLog(logLabel);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
async visual_compare_page(label) {
|
|
2000
|
+
const logLabel = `Visual comparison of page to base snapshot - ${label}`;
|
|
2001
|
+
this.startLog(logLabel);
|
|
2002
|
+
try {
|
|
2003
|
+
await new TppVisualCompare(this.page, this.caller, this.customConfig?.timeouts).comparePage(label);
|
|
2004
|
+
} catch (ex) {
|
|
2005
|
+
this.failureLog(logLabel);
|
|
2006
|
+
throw ex;
|
|
2007
|
+
}
|
|
2008
|
+
this.endLog(logLabel);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
async visual_compare_element(element, label) {
|
|
2012
|
+
const logLabel = `Visual comparison of ${element.label} to base snapshot - ${label}`;
|
|
2013
|
+
this.startLog(logLabel);
|
|
2014
|
+
try {
|
|
2015
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
2016
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
2017
|
+
await new TppVisualCompare(this.page, this.caller, this.customConfig?.timeouts).compareElement(tppElement, label);
|
|
2018
|
+
} catch (ex) {
|
|
2019
|
+
this.failureLog(logLabel);
|
|
2020
|
+
throw ex;
|
|
2021
|
+
}
|
|
2022
|
+
this.endLog(logLabel);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
async visual_compare_page_soft_assert(label, softAssert, assertMessage) {
|
|
2026
|
+
const logLabel = `Visual comparison of page to base snapshot - ${label} Soft Assert`;
|
|
2027
|
+
this.startLog(logLabel);
|
|
2028
|
+
try {
|
|
2029
|
+
let logMessage = (msg) => new Logger(this.caller, this.blacklistedLogs).log(logLabel, msg);
|
|
2030
|
+
await new TppVisualCompare(this.page, this.caller, this.customConfig?.timeouts, {softAssert, assertMessage, logMessage})
|
|
2031
|
+
.comparePage(label);
|
|
2032
|
+
} catch (ex) {
|
|
2033
|
+
this.failureLog(logLabel);
|
|
2034
|
+
throw ex;
|
|
2035
|
+
}
|
|
2036
|
+
this.endLog(logLabel);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
async visual_compare_element_soft_assert(element, label, softAssert, assertMessage) {
|
|
2040
|
+
const logLabel = `Visual comparison of ${element.label} to base snapshot - ${label} Soft Assert`;
|
|
2041
|
+
this.startLog(logLabel);
|
|
2042
|
+
try {
|
|
2043
|
+
const { element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1);
|
|
2044
|
+
await this.takeAutohealScreenshot(tppElement);
|
|
2045
|
+
let logMessage = (msg) => new Logger(this.caller, this.blacklistedLogs).log(logLabel, msg);
|
|
2046
|
+
await new TppVisualCompare(this.page, this.caller, this.customConfig?.timeouts, {softAssert, assertMessage, logMessage})
|
|
2047
|
+
.compareElement(tppElement, label);
|
|
2048
|
+
} catch (ex) {
|
|
2049
|
+
this.failureLog(logLabel);
|
|
2050
|
+
throw ex;
|
|
2051
|
+
}
|
|
2052
|
+
this.endLog(logLabel);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
async advanced_visual_comparison(data) {
|
|
2056
|
+
const logLabel = `Advanced Visual Comparison`;
|
|
2057
|
+
this.startLog(logLabel);
|
|
2058
|
+
try {
|
|
2059
|
+
const tppElement = data?.element
|
|
2060
|
+
? (await this.tpp_element_or_backup(data.element, logLabel, 1)).element
|
|
2061
|
+
: undefined;
|
|
2062
|
+
let logMessage = (msg) => new Logger(this.caller, this.blacklistedLogs).log(logLabel, msg);
|
|
2063
|
+
await new TppVisualCompare(this.page, this.caller, this.customConfig?.timeouts, {logMessage})
|
|
2064
|
+
.advancedCompare(tppElement, data);
|
|
2065
|
+
} catch (ex) {
|
|
2066
|
+
this.failureLog(logLabel);
|
|
2067
|
+
throw ex;
|
|
2068
|
+
}
|
|
2069
|
+
this.endLog(logLabel);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
async select_date_in_date_picker(data) {
|
|
2073
|
+
const logLabel = `Select date in date picker`;
|
|
2074
|
+
this.startLog(logLabel);
|
|
2075
|
+
try {
|
|
2076
|
+
// Get format, default to 'MM/DD/YYYY'
|
|
2077
|
+
const format = data.format || 'MM/DD/YYYY';
|
|
2078
|
+
|
|
2079
|
+
// Use DateUtils to calculate target date
|
|
2080
|
+
const targetDate = this.library.date.getTargetDate({
|
|
2081
|
+
date: data.date,
|
|
2082
|
+
offsetDays: data.offsetDays,
|
|
2083
|
+
format: format
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
// Calculate month difference using DateUtils
|
|
2087
|
+
const diff = this.library.date.getMonthDifference(targetDate);
|
|
2088
|
+
// this.log(logLabel, `Month difference: ${diff}`);
|
|
2089
|
+
|
|
2090
|
+
// Get date formats for picker matching
|
|
2091
|
+
const dateFormats = this.library.date.getDatePickerFormats(targetDate, format);
|
|
2092
|
+
|
|
2093
|
+
// Navigate to the correct month
|
|
2094
|
+
if (diff > 0) {
|
|
2095
|
+
// Click next button diff times
|
|
2096
|
+
for (let i = 0; i < diff; i++) {
|
|
2097
|
+
if (!data.nextMonthButtonLocator) {
|
|
2098
|
+
throw new Error('nextMonthButtonLocator is required to navigate to future months');
|
|
2099
|
+
}
|
|
2100
|
+
await this.click_on_element(data.nextMonthButtonLocator);
|
|
2101
|
+
await sleep(200); // Small delay between clicks
|
|
2102
|
+
}
|
|
2103
|
+
} else if (diff < 0) {
|
|
2104
|
+
// Click previous button |diff| times
|
|
2105
|
+
for (let i = diff; i < 0; i++) {
|
|
2106
|
+
if (!data.prevMonthButtonLocator) {
|
|
2107
|
+
throw new Error('prevMonthButtonLocator is required to navigate to past months');
|
|
2108
|
+
}
|
|
2109
|
+
await this.click_on_element(data.prevMonthButtonLocator);
|
|
2110
|
+
await sleep(200); // Small delay between clicks
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Wait a bit for date picker to update after navigation
|
|
2115
|
+
await sleep(300);
|
|
2116
|
+
|
|
2117
|
+
let dateClicked = false;
|
|
2118
|
+
|
|
2119
|
+
// Strategy: Use dateLocator if provided, otherwise use dynamic search
|
|
2120
|
+
if (data.dateLocator) {
|
|
2121
|
+
|
|
2122
|
+
this.log(logLabel, 'Using provided dateLocator with text-based filtering to click date');
|
|
2123
|
+
|
|
2124
|
+
const baseElement = new TppElement(data.dateLocator, this.page, this.fromAi);
|
|
2125
|
+
const baseLocator = baseElement.locator();
|
|
2126
|
+
|
|
2127
|
+
// Try each date format with text filtering within the provided dateLocator
|
|
2128
|
+
for (const dateFormat of dateFormats) {
|
|
2129
|
+
if (dateClicked) break;
|
|
2130
|
+
|
|
2131
|
+
try {
|
|
2132
|
+
// Filter the base locator by text to find the specific date
|
|
2133
|
+
const dateElementWithText = baseLocator.getByText(dateFormat, { exact: false }).first();
|
|
2134
|
+
|
|
2135
|
+
// Check if element exists and is visible
|
|
2136
|
+
if (await dateElementWithText.count() > 0) {
|
|
2137
|
+
await dateElementWithText.waitFor({ state: 'visible', timeout: 2000 }).catch(() => {});
|
|
2138
|
+
if (await dateElementWithText.isVisible()) {
|
|
2139
|
+
await dateElementWithText.click();
|
|
2140
|
+
dateClicked = true;
|
|
2141
|
+
this.log(logLabel, `Successfully clicked date "${dateFormat}" using provided dateLocator with text filter`);
|
|
2142
|
+
break;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
} catch (e) {
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
if (!dateClicked) {
|
|
2152
|
+
// Dynamically identify and click the date element
|
|
2153
|
+
// Common date picker container and cell selectors
|
|
2154
|
+
const datePickerContainers = [
|
|
2155
|
+
'.calendar', '.datepicker', '.react-datepicker', '.flatpickr-calendar',
|
|
2156
|
+
'[role="grid"]', '[role="table"]', '.mat-calendar-body',
|
|
2157
|
+
'.ant-picker-dropdown', '.rc-calendar', '[class*="calendar"]', '[class*="datepicker"]'
|
|
2158
|
+
];
|
|
2159
|
+
|
|
2160
|
+
const dateCellSelectors = [
|
|
2161
|
+
'td[role="gridcell"]', 'td.calendar-day', 'td',
|
|
2162
|
+
'button[role="gridcell"]', 'button.calendar-day',
|
|
2163
|
+
'.calendar-day', '[role="gridcell"]', 'button', 'div[role="gridcell"]'
|
|
2164
|
+
];
|
|
2165
|
+
|
|
2166
|
+
for (const dateFormat of dateFormats) {
|
|
2167
|
+
if (dateClicked) break;
|
|
2168
|
+
|
|
2169
|
+
// Strategy 1: Search within date picker containers first
|
|
2170
|
+
for (const container of datePickerContainers) {
|
|
2171
|
+
if (dateClicked) break;
|
|
2172
|
+
|
|
2173
|
+
for (const cellSelector of dateCellSelectors) {
|
|
2174
|
+
try {
|
|
2175
|
+
const dateElement = this.page.locator(container).locator(cellSelector).getByText(dateFormat, { exact: false }).first();
|
|
2176
|
+
|
|
2177
|
+
// Check if element exists and is visible
|
|
2178
|
+
if (await dateElement.count() > 0) {
|
|
2179
|
+
await dateElement.waitFor({ state: 'visible', timeout: 2000 }).catch(() => {});
|
|
2180
|
+
if (await dateElement.isVisible()) {
|
|
2181
|
+
await dateElement.click();
|
|
2182
|
+
dateClicked = true;
|
|
2183
|
+
this.log(logLabel, `Successfully clicked date "${dateFormat}" found in container ${container} with selector ${cellSelector}`);
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
} catch (e) {
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// Strategy 2: Direct search for date cells (if container search failed)
|
|
2194
|
+
if (!dateClicked) {
|
|
2195
|
+
for (const cellSelector of dateCellSelectors) {
|
|
2196
|
+
try {
|
|
2197
|
+
const dateElement = this.page.locator(cellSelector).getByText(dateFormat, { exact: false }).first();
|
|
2198
|
+
|
|
2199
|
+
// Check if element exists and is visible
|
|
2200
|
+
if (await dateElement.count() > 0) {
|
|
2201
|
+
await dateElement.waitFor({ state: 'visible', timeout: 2000 }).catch(() => {});
|
|
2202
|
+
if (await dateElement.isVisible()) {
|
|
2203
|
+
await dateElement.click();
|
|
2204
|
+
dateClicked = true;
|
|
2205
|
+
this.log(logLabel, `Successfully clicked date "${dateFormat}" found with selector ${cellSelector}`);
|
|
2206
|
+
break;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
} catch (e) {
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Strategy 3: Broader text search (fallback)
|
|
2216
|
+
if (!dateClicked) {
|
|
2217
|
+
try {
|
|
2218
|
+
const dateElement = this.page.getByText(dateFormat, { exact: false }).first();
|
|
2219
|
+
|
|
2220
|
+
// Check if element exists and is visible
|
|
2221
|
+
if (await dateElement.count() > 0) {
|
|
2222
|
+
await dateElement.waitFor({ state: 'visible', timeout: 2000 }).catch(() => {});
|
|
2223
|
+
if (await dateElement.isVisible()) {
|
|
2224
|
+
await dateElement.click();
|
|
2225
|
+
dateClicked = true;
|
|
2226
|
+
this.log(logLabel, `Successfully clicked date "${dateFormat}" found via text search`);
|
|
2227
|
+
break;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// If dynamic search failed, throw error
|
|
2237
|
+
if (!dateClicked) {
|
|
2238
|
+
throw new Error(`Unable to find or click date "${targetDate.format(format)}". Dynamic search failed. Consider providing dateLocator for more reliable date selection.`);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
} catch (ex) {
|
|
2243
|
+
this.failureLog(logLabel);
|
|
2244
|
+
throw new Error(`Failed to select date in date picker: ${ex?.message || ex}`);
|
|
2245
|
+
}
|
|
2246
|
+
this.endLog(logLabel);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
async run_js_script(code) {
|
|
2251
|
+
const logLabel = `Run JavaScript in browser`;
|
|
2252
|
+
this.startLog(logLabel);
|
|
2253
|
+
try {
|
|
2254
|
+
const result = await this.page.evaluate((code) => {
|
|
2255
|
+
return eval(code);
|
|
2256
|
+
}, code);
|
|
2257
|
+
this.endLog(logLabel);
|
|
2258
|
+
return result;
|
|
2259
|
+
} catch(ex) {
|
|
2260
|
+
this.failureLog(logLabel);
|
|
2261
|
+
throw ex;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
async checkElementVisibility(tppElement, label) {
|
|
2266
|
+
const isVisible = await tppElement.locator().isVisible();
|
|
2267
|
+
if(!isVisible) {
|
|
2268
|
+
let message = 'Element is not visible';
|
|
2269
|
+
const count = await tppElement.locator().count()
|
|
2270
|
+
if(count > 1) {
|
|
2271
|
+
message = 'More than one element is matching in the DOM';
|
|
2272
|
+
} else if(count === 0) {
|
|
2273
|
+
message = 'Element is not present';
|
|
2274
|
+
}
|
|
2275
|
+
throw {
|
|
2276
|
+
message: `${message}: ${tppElement.element.label} : ${tppElement.getSelector()}${new Logger().getIndexLog(tppElement)}`,
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
async call_api(api_request) {
|
|
2282
|
+
const path = !api_request.path?.trim() ? '' : `/${api_request.path.trim()}`;
|
|
2283
|
+
const logLabel = `Calling api, method: ${api_request.method}, path: ${api_request.baseUrl}${path}`;
|
|
2284
|
+
this.startLog(logLabel);
|
|
2285
|
+
const apiService = new ApiService(this.request);
|
|
2286
|
+
const response = await apiService.apiCall(api_request);
|
|
2287
|
+
if(this.customConfig?.apiSettings?.testLogs) {
|
|
2288
|
+
let logs = await apiService.getApiDetailsPretty(api_request, response);
|
|
2289
|
+
this.print_log(logs);
|
|
2290
|
+
}
|
|
2291
|
+
this.endLog(logLabel);
|
|
2292
|
+
return response;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
async call_api_with_json(api_request,data) {
|
|
2296
|
+
const path = !api_request.path?.trim() ? '' : `/${api_request.path.trim()}`;
|
|
2297
|
+
const logLabel = `Calling api with json, method: ${api_request.method}, path: ${api_request.baseUrl}${path}`;
|
|
2298
|
+
this.startLog(logLabel);
|
|
2299
|
+
const apiService = new ApiService(this.request);
|
|
2300
|
+
const response = await apiService.apiCallwithJson(api_request,data);
|
|
2301
|
+
if(this.testType === 'api') {
|
|
2302
|
+
if(api_request.devassureMetadata && typeof api_request.devassureMetadata === "object") {
|
|
2303
|
+
this.print_log("---------------------------------TEST DETAILS---------------------------------");
|
|
2304
|
+
Object.entries(api_request.devassureMetadata || {}).forEach(([key, value]) => {
|
|
2305
|
+
this.print_log(`${key}: ${value}`);
|
|
2306
|
+
});
|
|
2307
|
+
this.print_log("_______________________________________________________________________________");
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
if(this.customConfig?.apiSettings?.testLogs) {
|
|
2311
|
+
let logs = await apiService.getApiDetailsPretty(api_request, response);
|
|
2312
|
+
this.print_log(logs);
|
|
2313
|
+
}
|
|
2314
|
+
this.endLog(logLabel);
|
|
2315
|
+
return response;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
async verify_http_response_status(response, comparator, expectedStatus) {
|
|
2319
|
+
const logLabel = `Validate API response status, ${comparator}, expectedStatus: ${expectedStatus}`;
|
|
2320
|
+
this.startLog(logLabel);
|
|
2321
|
+
new ApiService(this.request).validateStatus(response, comparator, expectedStatus);
|
|
2322
|
+
this.endLog(logLabel);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
async verify_http_response_text(response, comparator, expected) {
|
|
2326
|
+
const logLabel = `Validate API response text, ${comparator}, expected: ${expected}`;
|
|
2327
|
+
this.startLog(logLabel);
|
|
2328
|
+
await new ApiService(this.request).validateText(response, comparator, expected);
|
|
2329
|
+
this.endLog(logLabel);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
async verify_http_response_json(response, jsonKey, comparator, expected) {
|
|
2333
|
+
const logLabel = `Validate API response text, key: ${jsonKey}, ${comparator}, expectedValue: ${expected}`;
|
|
2334
|
+
this.startLog(logLabel);
|
|
2335
|
+
await new ApiService(this.request).validateJson(response, jsonKey, comparator, expected);
|
|
2336
|
+
this.endLog(logLabel);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
async get_http_response_url(apiResponse) {
|
|
2340
|
+
const logLabel = `Get API response URL`;
|
|
2341
|
+
this.startLog(logLabel);
|
|
2342
|
+
const status = new ApiService(this.request).getResponseUrl(apiResponse);
|
|
2343
|
+
this.endLog(logLabel);
|
|
2344
|
+
return status;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
async get_http_response_staus(apiResponse) {
|
|
2348
|
+
const logLabel = `Get API response status`;
|
|
2349
|
+
this.startLog(logLabel);
|
|
2350
|
+
const status = new ApiService(this.request).getResponseStaus(apiResponse);
|
|
2351
|
+
this.endLog(logLabel);
|
|
2352
|
+
return status;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
async get_http_response_json(apiResponse) {
|
|
2356
|
+
const logLabel = `Get API response json`;
|
|
2357
|
+
this.startLog(logLabel);
|
|
2358
|
+
const data = await new ApiService(this.request).getResponseBodyJson(apiResponse);
|
|
2359
|
+
this.endLog(logLabel);
|
|
2360
|
+
return data;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
async get_http_response_text(apiResponse) {
|
|
2364
|
+
const logLabel = `Get API response text`;
|
|
2365
|
+
this.startLog(logLabel);
|
|
2366
|
+
const data = await new ApiService(this.request).getResponseBodyText(apiResponse);
|
|
2367
|
+
this.endLog(logLabel);
|
|
2368
|
+
return data;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
async get_http_response_headers(apiResponse) {
|
|
2372
|
+
const logLabel = `Get API response headers`;
|
|
2373
|
+
this.startLog(logLabel);
|
|
2374
|
+
const headers = new ApiService(this.request).getResponseHeaders(apiResponse);
|
|
2375
|
+
this.endLog(logLabel);
|
|
2376
|
+
return headers;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
async get_http_response_header_value(apiResponse, key) {
|
|
2380
|
+
const logLabel = `Get API response header value for key: ${key}`;
|
|
2381
|
+
this.startLog(logLabel);
|
|
2382
|
+
const value = new ApiService(this.request).getResponseHeaderValue(apiResponse, key);
|
|
2383
|
+
this.endLog(logLabel);
|
|
2384
|
+
return value;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
async invoke_query(dbConfig, query, params, paramValues) {
|
|
2388
|
+
const logLabel = `Invoking DB query, config: ${dbConfig.connection_type}, query: ${query}`;
|
|
2389
|
+
let response;
|
|
2390
|
+
this.startLog(logLabel);
|
|
2391
|
+
try {
|
|
2392
|
+
if(dbConfig.connection_type == "mysql") {
|
|
2393
|
+
response = await new MySQLService().executeQuery(dbConfig, query, params, paramValues);
|
|
2394
|
+
}
|
|
2395
|
+
else if(dbConfig.connection_type == "postgresql") {
|
|
2396
|
+
response = await new PostgreSQLService().executeQuery(dbConfig, query, params, paramValues);
|
|
2397
|
+
} else if (dbConfig.connection_type == "neo4j") {
|
|
2398
|
+
response = await new Neo4jService().executeQuery(dbConfig, query, params, paramValues);
|
|
2399
|
+
} else if (dbConfig.connection_type == "oracle") {
|
|
2400
|
+
response = await new OracleService().executeQuery(dbConfig, query, params, paramValues);
|
|
2401
|
+
}
|
|
2402
|
+
this.endLog(logLabel);
|
|
2403
|
+
} catch (err) {
|
|
2404
|
+
this.log(logLabel, err.message || err);
|
|
2405
|
+
this.failureLog(logLabel);
|
|
2406
|
+
throw {message: err.message || err || 'Error executing DB query!'}
|
|
2407
|
+
}
|
|
2408
|
+
return response;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
async verify_db_query_response(queryResponse, rowIndex, columnName, comparator, expected) {
|
|
2412
|
+
const logLabel = `Validate DB Query response, key: ${rowIndex},${columnName}, ${comparator}, expectedValue: ${expected}`;
|
|
2413
|
+
this.startLog(logLabel);
|
|
2414
|
+
if(!queryResponse || queryResponse.length <= rowIndex) {
|
|
2415
|
+
this.failureLog(logLabel);
|
|
2416
|
+
throw {
|
|
2417
|
+
message: `Row index ${rowIndex} is invalid!! Number of rows from DB Query = ${queryResponse.length}`,
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
if(!queryResponse[rowIndex].hasOwnProperty(columnName)) {
|
|
2421
|
+
this.failureLog(logLabel);
|
|
2422
|
+
throw {
|
|
2423
|
+
message: `Column '${columnName}' is not present!! Available columns = [${Object.keys(queryResponse[rowIndex])?.join(', ')}]`,
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
const value = queryResponse[rowIndex][columnName];
|
|
2427
|
+
this.verify_text(value, comparator, expected);
|
|
2428
|
+
this.endLog(logLabel);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
async verify_db_query_row_count(queryResponse, comparator, expected) {
|
|
2432
|
+
const logLabel = `Validate DB Query row count, ${comparator}, expectedValue: ${expected}`;
|
|
2433
|
+
this.startLog(logLabel);
|
|
2434
|
+
const rowCount = queryResponse == undefined? -1 : queryResponse.length;
|
|
2435
|
+
this.verify_number(rowCount, comparator, expected);
|
|
2436
|
+
this.endLog(logLabel);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
verify_text(text, comparator, expected) {
|
|
2440
|
+
const logLabel = `Validate text, text: ${text?.toString()?.replaceAll("\n", " ")}, ${comparator}, expected: ${expected}`;
|
|
2441
|
+
this.startLog(logLabel);
|
|
2442
|
+
if(!compare(text, comparator, expected)) {
|
|
2443
|
+
this.failureLog(logLabel);
|
|
2444
|
+
throw {
|
|
2445
|
+
message: `Text is not as expected. actual: ${text} ---- comparator: ${comparator} ---- expected: ${expected}`,
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
this.endLog(logLabel);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
verify_number(number, comparator, expected) {
|
|
2452
|
+
const logLabel = `Validate number, number: ${number}, ${comparator}, expected: ${expected}`;
|
|
2453
|
+
this.startLog(logLabel);
|
|
2454
|
+
if(!compare(number, comparator, expected)) {
|
|
2455
|
+
this.failureLog(logLabel);
|
|
2456
|
+
throw {
|
|
2457
|
+
message: `Number is not as expected. actual: ${number} ---- comparator: ${comparator} ---- expected: ${expected}`,
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
this.endLog(logLabel);
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
verify_boolean(value, expected) {
|
|
2464
|
+
const logLabel = `Validate boolean, value: ${value}, expected: ${expected}`;
|
|
2465
|
+
this.startLog(logLabel);
|
|
2466
|
+
if(!!value != !!expected) {
|
|
2467
|
+
this.failureLog(logLabel);
|
|
2468
|
+
throw {
|
|
2469
|
+
message: `Boolean value is not as expected. actual: ${value} ---- expected: ${expected}`,
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
this.endLog(logLabel);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
async take_screenshot(label) {
|
|
2476
|
+
const logLabel = `Take Screenshot with label ${label}`;
|
|
2477
|
+
this.startLog(logLabel);
|
|
2478
|
+
await this.addScreenshot(label);
|
|
2479
|
+
this.endLog(logLabel);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
async dumpWebPageData(url) {
|
|
2483
|
+
await this.open_url(url);
|
|
2484
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
2485
|
+
|
|
2486
|
+
let html;
|
|
2487
|
+
let retry = 0;
|
|
2488
|
+
|
|
2489
|
+
while (!html && retry < 5) {
|
|
2490
|
+
try {
|
|
2491
|
+
html = await this.page.evaluate(() => {
|
|
2492
|
+
const bannedPatterns = ['style', 'src', 'value', 'data-tpp-background-color', 'data-tpp-cursor'];
|
|
2493
|
+
|
|
2494
|
+
function shouldRemove(attrName, attrValue) {
|
|
2495
|
+
return bannedPatterns.some((pattern) => pattern === attrName) || /\d/.test(attrValue);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function cleanNode(node) {
|
|
2499
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2500
|
+
// Remove banned attributes
|
|
2501
|
+
for (const attr of Array.from(node.attributes)) {
|
|
2502
|
+
if (shouldRemove(attr.name)) {
|
|
2503
|
+
node.removeAttribute(attr.name, attr.value);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
// Recurse into children
|
|
2508
|
+
for (const child of Array.from(node.childNodes)) {
|
|
2509
|
+
cleanNode(child);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// Remove unwanted nodes entirely
|
|
2514
|
+
document.querySelectorAll("script, style, iframe, object, link[rel=preload], link[rel=prefetch]")
|
|
2515
|
+
.forEach(el => el.remove());
|
|
2516
|
+
|
|
2517
|
+
cleanNode(document.body);
|
|
2518
|
+
|
|
2519
|
+
// Return the full document HTML after cleaning
|
|
2520
|
+
return document.documentElement.outerHTML;
|
|
2521
|
+
});
|
|
2522
|
+
} catch (e) {
|
|
2523
|
+
retry++;
|
|
2524
|
+
await this.page.waitForTimeout(1000);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
let data = { html };
|
|
2529
|
+
this.log('', `[WEB-DUMP-DATA-RESULT] ${JSON.stringify(data)}`);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
|
|
2533
|
+
async click_at_center_of_element(element) {
|
|
2534
|
+
const logLabel = `Click at center of element - ${element.label}`;
|
|
2535
|
+
this.startLog(logLabel);
|
|
2536
|
+
let callback = async (x, y) => await this.autoheal.click_on_element(x, y);
|
|
2537
|
+
const { autoheal, element: tppElement } = await this.tpp_element_or_backup(element, logLabel, 1, undefined, callback);
|
|
2538
|
+
if (autoheal) {
|
|
2539
|
+
this.endLog(logLabel);
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
try {
|
|
2543
|
+
await tppElement.locator().waitFor({state: "visible"});
|
|
2544
|
+
const box = await tppElement.locator().boundingBox();
|
|
2545
|
+
if(!box) {
|
|
2546
|
+
throw {message: 'Unable to find element position!'}
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
const x = box.x + box.width / 2;
|
|
2550
|
+
const y = box.y + box.height / 2;
|
|
2551
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
2552
|
+
await page.mouse.click(x, y);
|
|
2553
|
+
|
|
2554
|
+
} catch (e) {
|
|
2555
|
+
await this.handleElementException(e, tppElement, logLabel, true);
|
|
2556
|
+
}
|
|
2557
|
+
this.endLog(logLabel);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
async click_on_xy(x, y, options) {
|
|
2561
|
+
const logLabel = options?.button === 'right'
|
|
2562
|
+
? `Right Clicking at ${x}, ${y}`
|
|
2563
|
+
: options?.clickCount === 2
|
|
2564
|
+
? `Double Clicking at ${x}, ${y}`
|
|
2565
|
+
: `Clicking at ${x}, ${y}`;
|
|
2566
|
+
this.startLog(logLabel);
|
|
2567
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
2568
|
+
await page.mouse.click(x, y, options);
|
|
2569
|
+
this.endLog(logLabel);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
async input_on_xy(x, y, data) {
|
|
2573
|
+
const logLabel = `Inputting ${data} on ${x}, ${y}`;
|
|
2574
|
+
this.startLog(logLabel);
|
|
2575
|
+
const targetSelector = `${Date.now()}`;
|
|
2576
|
+
// find the elements at x,y and check if any one of them is of type input/textarea or has roles combobox/textbox or has contentEditableSetto true
|
|
2577
|
+
const element = await this.page.evaluate(([x, y, targetSelector]) => {
|
|
2578
|
+
const element = document.elementsFromPoint(x, y);
|
|
2579
|
+
for(let i=0; i<element.length; i++) {
|
|
2580
|
+
const tagName = element[i].tagName.toLowerCase();
|
|
2581
|
+
const type = element[i].getAttribute('type');
|
|
2582
|
+
const role = element[i].getAttribute('role');
|
|
2583
|
+
const nonTextTypes = ['checkbox', 'radio', 'button', 'submit', 'reset', 'file', 'image', 'color', 'range'];
|
|
2584
|
+
const contentEditable = element[i].getAttribute('contentEditable');
|
|
2585
|
+
if((tagName === 'input' && !nonTextTypes.includes(type)) || tagName === 'textarea' || (role && role.toLowerCase() === 'combobox') || (role && role.toLowerCase() === 'textbox') || (contentEditable && contentEditable.toLowerCase() === 'true')) {
|
|
2586
|
+
element[i].setAttribute('tpp-data-target', targetSelector);
|
|
2587
|
+
return element[i];
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}, [x, y, targetSelector]);
|
|
2591
|
+
if(!element) {
|
|
2592
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
2593
|
+
await page.mouse.click(x, y);
|
|
2594
|
+
await this.page.waitForTimeout(100);
|
|
2595
|
+
const fallbackElement = await this.page.evaluate(([x, y, targetSelector]) => {
|
|
2596
|
+
const element = document.activeElement;
|
|
2597
|
+
const tagName = element.tagName.toLowerCase();
|
|
2598
|
+
const type = element.getAttribute('type');
|
|
2599
|
+
const role = element.getAttribute('role');
|
|
2600
|
+
const nonTextTypes = ['checkbox', 'radio', 'button', 'submit', 'reset', 'file', 'image', 'color', 'range'];
|
|
2601
|
+
const contentEditable = element.getAttribute('contentEditable');
|
|
2602
|
+
if((tagName === 'input' && !nonTextTypes.includes(type)) || tagName === 'textarea' || (role && role.toLowerCase() === 'combobox') || (role && role.toLowerCase() === 'textbox') || (contentEditable && contentEditable.toLowerCase() === 'true')) {
|
|
2603
|
+
element.setAttribute('tpp-data-target', targetSelector);
|
|
2604
|
+
return element;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
, [x, y, targetSelector]);
|
|
2608
|
+
// If there is no fallback element, then simply type the data
|
|
2609
|
+
if (!fallbackElement) {
|
|
2610
|
+
this.endLog(logLabel);
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
const locator = await this.page.locator(`[tpp-data-target="${targetSelector}"]`);
|
|
2615
|
+
await locator.fill(data);
|
|
2616
|
+
|
|
2617
|
+
this.endLog(logLabel);
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
async clear_input(x, y) {
|
|
2621
|
+
const logLabel = `Clearing input on ${x}, ${y}`;
|
|
2622
|
+
this.startLog(logLabel);
|
|
2623
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
2624
|
+
await page.mouse.click(x, y);
|
|
2625
|
+
const selectAllKey = process.platform === 'darwin' ? 'Meta' : 'Control';
|
|
2626
|
+
await page.keyboard.down(selectAllKey);
|
|
2627
|
+
await page.keyboard.press('A');
|
|
2628
|
+
await page.keyboard.up(selectAllKey);
|
|
2629
|
+
await page.keyboard.press('Delete');
|
|
2630
|
+
await page.keyboard.press('Backspace');
|
|
2631
|
+
this.endLog(logLabel);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
async hover_xy(x, y) {
|
|
2635
|
+
const logLabel = `Hovering over ${x}, ${y}`;
|
|
2636
|
+
this.startLog(logLabel);
|
|
2637
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
2638
|
+
await page.mouse.move(x, y);
|
|
2639
|
+
this.endLog(logLabel);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
async key_down_xy(value) {
|
|
2643
|
+
const logLabel = `Key Down on ${value}`;
|
|
2644
|
+
this.startLog(logLabel);
|
|
2645
|
+
let page = this.page.page && this.page.page() ? this.page.page() : this.page;
|
|
2646
|
+
await page.keyboard.down(value);
|
|
2647
|
+
this.endLog(logLabel);
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
sleep(millis) {
|
|
2651
|
+
return new Promise((resolve) => setTimeout(resolve, millis));
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
async wait_time(millis) {
|
|
2655
|
+
const logLabel = `Wait for ${millis} milliseconds`;
|
|
2656
|
+
this.startLog(logLabel);
|
|
2657
|
+
await sleep(millis);
|
|
2658
|
+
this.endLog(logLabel);
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
async get_clipboard_content() {
|
|
2662
|
+
const logLabel = `Get clipboard content`;
|
|
2663
|
+
this.startLog(logLabel);
|
|
2664
|
+
var ncp = require('node-clipboardy');
|
|
2665
|
+
const text = await ncp.read();
|
|
2666
|
+
this.endLog(logLabel);
|
|
2667
|
+
return text || '';
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
async wait_for_breakpoint(testFile, stepIndex, fnKey) {
|
|
2671
|
+
let content = {
|
|
2672
|
+
filePath: testFile,
|
|
2673
|
+
stepIndex: stepIndex
|
|
2674
|
+
};
|
|
2675
|
+
|
|
2676
|
+
if(fnKey?.trim()) {
|
|
2677
|
+
const secSuffix = "__secondary";
|
|
2678
|
+
if(fnKey.endsWith(secSuffix)) {
|
|
2679
|
+
content.fnKey = fnKey.slice(0, -secSuffix.length);
|
|
2680
|
+
content.secondaryFn = true;
|
|
2681
|
+
} else {
|
|
2682
|
+
content.fnKey = fnKey;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
const breakpointFilePath = `.breakpoint/breakpoint_${Date.now()}`;
|
|
2687
|
+
(new FileUtils()).writeTextContent(breakpointFilePath, JSON.stringify(content));
|
|
2688
|
+
|
|
2689
|
+
this.log(undefined, `[WAIT_FOR_BREAKPOINT] ${path.resolve(breakpointFilePath)}`);
|
|
2690
|
+
|
|
2691
|
+
//Wait for breakpoint file to be removed
|
|
2692
|
+
return new Promise((resolve, reject) => {
|
|
2693
|
+
const interval = setInterval(() => {
|
|
2694
|
+
if (!fs.existsSync(breakpointFilePath)) {
|
|
2695
|
+
clearInterval(interval);
|
|
2696
|
+
resolve();
|
|
2697
|
+
}
|
|
2698
|
+
}, 500);
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
action_log(log) {
|
|
2703
|
+
new Logger(this.caller, this.blacklistedLogs).action_log(log);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
print_log(log) {
|
|
2707
|
+
if(typeof log === 'object') {
|
|
2708
|
+
try {
|
|
2709
|
+
log = JSON.stringify(log, null, 4);
|
|
2710
|
+
} catch (_e) {
|
|
2711
|
+
this.log(undefined, log);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
} else if(typeof log !== 'string') {
|
|
2715
|
+
this.log(undefined, log);
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
log.split('\n').forEach(line => this.log(undefined, line));
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
log(stepName, msg) {
|
|
2722
|
+
new Logger(this.caller, this.blacklistedLogs).log(stepName, msg);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
startLog(stepName) {
|
|
2726
|
+
new Logger(this.caller, this.blacklistedLogs).startLog(stepName);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
endLog(stepName) {
|
|
2730
|
+
new Logger(this.caller, this.blacklistedLogs).endLog(stepName);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
failureLog(stepName) {
|
|
2734
|
+
new Logger(this.caller, this.blacklistedLogs).failureLog(stepName);
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
async addAllPageScreenshots() {
|
|
2738
|
+
const browser = this.page.context()?.browser();
|
|
2739
|
+
let windowI = 1;
|
|
2740
|
+
for (const context of browser.contexts()) {
|
|
2741
|
+
let tabI = 0;
|
|
2742
|
+
for (const page of context.pages()) {
|
|
2743
|
+
tabI += 1
|
|
2744
|
+
const imageName = windowI === 1
|
|
2745
|
+
? (tabI === 1 ? undefined : `tab${tabI}`)
|
|
2746
|
+
: `window${windowI}_tab${tabI}`;
|
|
2747
|
+
const filePath = getScreenshotPath(imageName, this.caller, this.retry);
|
|
2748
|
+
try {
|
|
2749
|
+
await page.screenshot({ animations: 'disabled', path: filePath, timeout: 5000 });
|
|
2750
|
+
} catch (_e) {
|
|
2751
|
+
await await page.waitForTimeout(5000);
|
|
2752
|
+
try {
|
|
2753
|
+
await page.screenshot({ animations: 'disabled', path: filePath, timeout: 5000 });
|
|
2754
|
+
} catch (ex) {
|
|
2755
|
+
this.exceptionLog(`Error capturing screenshot - ${ex?.message || 'screenshot failed'}.`);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
this.action_log(getScreenshotLog(imageName, filePath, this.caller));
|
|
2759
|
+
}
|
|
2760
|
+
if(tabI > 0) windowI += 1;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
async addScreenshot(imageName, allPages) {
|
|
2765
|
+
if(this.testType === 'api') {
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
if(allPages && this.testType === 'web') {
|
|
2770
|
+
await this.addAllPageScreenshots();
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
const filePath = getScreenshotPath(imageName, this.caller, this.retry);
|
|
2775
|
+
let frame = undefined;
|
|
2776
|
+
if(this.page.page && this.page.page()) {
|
|
2777
|
+
frame = this.page;
|
|
2778
|
+
this.page = this.page.page();
|
|
2779
|
+
}
|
|
2780
|
+
try {
|
|
2781
|
+
if(this.testType === 'mobile') {
|
|
2782
|
+
if(!this.mobile?.driver) {
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
await this.mobile.addScreenshot(filePath);
|
|
2786
|
+
} else {
|
|
2787
|
+
try {
|
|
2788
|
+
await this.page.screenshot({ animations: 'disabled', path: filePath, timeout: 5000 });
|
|
2789
|
+
} catch (_e) {
|
|
2790
|
+
await await this.page.waitForTimeout(5000);
|
|
2791
|
+
await this.page.screenshot({ animations: 'disabled', path: filePath, timeout: 5000 });
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
this.action_log(getScreenshotLog(imageName, filePath, this.caller));
|
|
2795
|
+
} catch (ex) {
|
|
2796
|
+
this.exceptionLog(`Error capturing screenshot - ${ex.message || 'screenshot failed'}.`);
|
|
2797
|
+
} finally {
|
|
2798
|
+
if(frame) {
|
|
2799
|
+
this.page = frame;
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
exceptionLog(message) {
|
|
2805
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: [EXCEPTION] ${message}`);
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
testStartLog() {
|
|
2809
|
+
if(this.retry) {
|
|
2810
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: [RETRY-${this.retry}] :: [BEGIN]`);
|
|
2811
|
+
} else {
|
|
2812
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: [START]`);
|
|
2813
|
+
}
|
|
2814
|
+
if(this.extensionId === 'TPP_EXTN_NOT_LOADED') {
|
|
2815
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: Chrome extension ID not found. Extension will still work. If you want to Open UI html and test this extension, please raise a request to DevAssure support!`);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
async getAiAction(element, label, exception, autohealCallback) {
|
|
2820
|
+
const screenshots = element.snaps;
|
|
2821
|
+
let config = await this.initializeConfig();
|
|
2822
|
+
if (!config.access_token) {
|
|
2823
|
+
return { success: false, message: 'AI access token is not configured' };
|
|
2824
|
+
}
|
|
2825
|
+
let image = screenshots.filter(snap => snap.testLabel === this.testLabel);
|
|
2826
|
+
if (!image.length) {
|
|
2827
|
+
image = screenshots;
|
|
2828
|
+
}
|
|
2829
|
+
if (!image.length) {
|
|
2830
|
+
return { success: false, message: 'No image found for auto-healing' };
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
let oldScreenshotPath = element.label.split(/ > /g);
|
|
2834
|
+
oldScreenshotPath.pop()
|
|
2835
|
+
const screenshotFolder = oldScreenshotPath.pop();
|
|
2836
|
+
let completed = undefined;
|
|
2837
|
+
let count = 0;
|
|
2838
|
+
while (!completed && count < 4) {
|
|
2839
|
+
let webState;
|
|
2840
|
+
try {
|
|
2841
|
+
webState = await fetchWebState(this.page);
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
return { success: false, message: `Error fetching web state: ${error.message}` };
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
const request = {};
|
|
2847
|
+
let oldScreenshotBase64, webStateBase64;
|
|
2848
|
+
oldScreenshotBase64 = []
|
|
2849
|
+
try {
|
|
2850
|
+
for (let screenshot of image) {
|
|
2851
|
+
const screenshotPath = path.join(this.customConfig.projPath, 'page', oldScreenshotPath.join(path.sep), 'da__autoheal', screenshotFolder, screenshot.snapshot);
|
|
2852
|
+
if (!fs.existsSync(screenshotPath)) {
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
oldScreenshotBase64.push("data:image/png;base64," + await new FileUtils().pngToBase64(screenshotPath));
|
|
2856
|
+
}
|
|
2857
|
+
webStateBase64 = await new FileUtils().pngToBase64(webState.screenshot);
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
if (oldScreenshotBase64.length === 0) {
|
|
2863
|
+
return { success: false, message: 'No valid screenshot found for auto-healing' };
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
request.images = [...oldScreenshotBase64, "data:image/png;base64," + webStateBase64];
|
|
2867
|
+
request.elements = webState.elementsData.map(el => {
|
|
2868
|
+
let attributes = '';
|
|
2869
|
+
for (const [key, value] of Object.entries(el)) {
|
|
2870
|
+
if (!['x', 'y', 'index', 'width', 'height', 'top', 'left', 'right', 'bottom'].includes(key)) {
|
|
2871
|
+
if (key && value) {
|
|
2872
|
+
attributes += ` ${key}="${value}"`;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return `<${el.type} ${attributes}>${el.text || ''}</${el.type}>`;
|
|
2877
|
+
});
|
|
2878
|
+
request.step = label;
|
|
2879
|
+
request.request_id = `${this.caller} - ${Date.now()}`;
|
|
2880
|
+
request.project_id = this.customConfig?.projectId || 'default_project';
|
|
2881
|
+
request.exception = exception;
|
|
2882
|
+
request.attrs = [
|
|
2883
|
+
...(image.map(snap => snap.attrs || "")),
|
|
2884
|
+
];
|
|
2885
|
+
const res = await aiWebSocketCall(AI_ENDPOINTS.AUTOHEAL, config, request);
|
|
2886
|
+
if (!res.success) {
|
|
2887
|
+
return { success: false, message: `Error in auto-heal request: ${res.message || res.error}` };
|
|
2888
|
+
}
|
|
2889
|
+
if (!res.data.success && !res.data.followupPrompt) {
|
|
2890
|
+
return { success: false, message: `${res.data.error}` };
|
|
2891
|
+
}
|
|
2892
|
+
if (res?.data?.verification) {
|
|
2893
|
+
if (res?.data?.verification?.response) {
|
|
2894
|
+
return { success: true, verify: true };
|
|
2895
|
+
} else {
|
|
2896
|
+
return { success: false, verify: true, };
|
|
2897
|
+
}
|
|
2898
|
+
} else if (res?.data?.index) {
|
|
2899
|
+
const ele = webState.elementsData[res?.data?.index];
|
|
2900
|
+
if (!ele) {
|
|
2901
|
+
return { success: false, message: `Element not found at index ${res.index}` };
|
|
2902
|
+
}
|
|
2903
|
+
let autohealRes;
|
|
2904
|
+
try {
|
|
2905
|
+
autohealRes = await autohealCallback(ele.x, ele.y);
|
|
2906
|
+
return { success: true, message: `Element auto-heal at coordinates (${ele.x}, ${ele.y})`, message: autohealRes };
|
|
2907
|
+
} catch (e) {
|
|
2908
|
+
if (!e?.message.toLowerCase().includes('navigation')) {
|
|
2909
|
+
return { success: true, message: `Error auto-healing element at index ${res.data.index}: ${e.message || e}` };
|
|
2910
|
+
}
|
|
2911
|
+
return { success: true, message: `Element auto-heal at coordinates (${ele.x}, ${ele.y})`, message: autohealRes };
|
|
2912
|
+
}
|
|
2913
|
+
} else {
|
|
2914
|
+
for (let action of (res.data?.actions || [])) {
|
|
2915
|
+
let element = webState.elementsData[action.index];
|
|
2916
|
+
if (!element) return { success: false, message: `Element not found at index ${action.index}` };
|
|
2917
|
+
const command = `${action.action} { "x": ${element.x}, "y": ${element.y}, "value": "${action.value}" }`;
|
|
2918
|
+
try {
|
|
2919
|
+
await new WebExecutorAgent(this).processWebExecutorCommand(command);
|
|
2920
|
+
} catch (e) {
|
|
2921
|
+
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
if (!res.data.followupPrompt) {
|
|
2926
|
+
completed = true;
|
|
2927
|
+
break;
|
|
2928
|
+
}
|
|
2929
|
+
count ++;
|
|
2930
|
+
}
|
|
2931
|
+
if (!completed) {
|
|
2932
|
+
return { success: false, message: 'Auto-healing failed after maximum retries' };
|
|
2933
|
+
}
|
|
2934
|
+
return { success: true, message: "Auto-healing completed successfully" };
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
async handleElementException(ex, tppElement, label, expectSingle) {
|
|
2938
|
+
if(ex.source === 'tpp') {
|
|
2939
|
+
this.failureLog(label);
|
|
2940
|
+
throw ex;
|
|
2941
|
+
}
|
|
2942
|
+
const regex = /^elements_\d+\.(\w+) is not a function(?:.*)?$/;
|
|
2943
|
+
const match = ex.message.match(regex);
|
|
2944
|
+
if(match){
|
|
2945
|
+
const missingElement = match[1];
|
|
2946
|
+
this.failureLog(`Element : ${missingElement} not defined in Page file`);
|
|
2947
|
+
throw {message : `Element : ${missingElement} not defined in Page file`};
|
|
2948
|
+
}
|
|
2949
|
+
if(expectSingle && await tppElement.locator().count() > 1) {
|
|
2950
|
+
throw {message: "More than one element is matching in the DOM"};
|
|
2951
|
+
}
|
|
2952
|
+
this.action_log("Element exception: ", ex);
|
|
2953
|
+
this.failureLog(label);
|
|
2954
|
+
const exceptionLines = (ex?.message || ex.toString()).split('\n');
|
|
2955
|
+
const message = exceptionLines[0].replace(ansiRegex(), '');
|
|
2956
|
+
if(message.includes('Target closed')) {
|
|
2957
|
+
throw {
|
|
2958
|
+
message: `Unable to find element : ${tppElement.element.label} : ${tppElement.getSelector()}${new Logger().getIndexLog(tppElement)}`,
|
|
2959
|
+
};
|
|
2960
|
+
} else {
|
|
2961
|
+
throw ex;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
async getSpecialExceptionMessage(tppElement, ignoreCount) {
|
|
2966
|
+
const eleCount = await tppElement.locator().count();
|
|
2967
|
+
if(eleCount <= 0) {
|
|
2968
|
+
return undefined;
|
|
2969
|
+
}
|
|
2970
|
+
if(!ignoreCount && eleCount > 1) {
|
|
2971
|
+
return {message: "More than one element is matching in the DOM"};
|
|
2972
|
+
} else if(!await tppElement.locator().isVisible()) {
|
|
2973
|
+
return {message: "Element is present in DOM but not Visible"};
|
|
2974
|
+
} else if(!await tppElement.locator().isEnabled()) {
|
|
2975
|
+
return {message: "Element is present in DOM but not Enabled"};
|
|
2976
|
+
} else {
|
|
2977
|
+
return {message: "Element is present in DOM but not Clickable"};
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
async handleClickException(e, tppElement, logLabel, ignoreCount) {
|
|
2982
|
+
let ex = e;
|
|
2983
|
+
console.log("Click exception: ", ex);
|
|
2984
|
+
const specilExcpetion = await this.getSpecialExceptionMessage(tppElement, ignoreCount);
|
|
2985
|
+
if(specilExcpetion) ex = specilExcpetion;
|
|
2986
|
+
await this.handleElementException(ex, tppElement, logLabel);
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
async throwElementNotPresent(element, label, expectSingle) {
|
|
2990
|
+
const tppElement = new TppElement(element, this.page, this.fromAi)
|
|
2991
|
+
const specilExcpetion = await this.getSpecialExceptionMessage(tppElement, !expectSingle);
|
|
2992
|
+
let ex = specilExcpetion || {message: `Unable to find element : ${element.label} : ${tppElement.getSelector()}${new Logger().getIndexLog(tppElement)}`}
|
|
2993
|
+
return ex;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
async takeAutohealScreenshot(tppElement) {
|
|
2997
|
+
const shouldTakeScreenshot = this.webAppPreferences?.autoheal && this.executionPreferences?.collectAutohealData && !tppElement.element.snaps?.filter(s => s.testLabel === this.testLabel).length && this.customConfig?.isIde;
|
|
2998
|
+
if (!shouldTakeScreenshot) return;
|
|
2999
|
+
await this.page.waitForTimeout(200);
|
|
3000
|
+
await takeElementScreenshot(this.page, tppElement.element, path.resolve("./"), Date.now(), this);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
handleExpectException(ex, label) {
|
|
3004
|
+
console.log("Expect exception: ", ex);
|
|
3005
|
+
this.failureLog(label);
|
|
3006
|
+
const exceptionLines = ex.toString().split('\n');
|
|
3007
|
+
const message = exceptionLines[0].replace(ansiRegex(), '');
|
|
3008
|
+
if(message.includes('toHaveText')) {
|
|
3009
|
+
throw {
|
|
3010
|
+
message: `Text is not as expected. actual: ${ex.matcherResult.actual.replaceAll('\n', '')} ---- expected: ${ex.matcherResult.expected}`,
|
|
3011
|
+
};
|
|
3012
|
+
} else if(message.includes('toHaveCount')) {
|
|
3013
|
+
throw {
|
|
3014
|
+
message: `Count is not as expected -- ${exceptionLines[2]} -- ${exceptionLines[3]}`,
|
|
3015
|
+
};
|
|
3016
|
+
} else if(message.includes('toHaveTitle')) {
|
|
3017
|
+
throw {
|
|
3018
|
+
message: `Page title is not as expected -- ${exceptionLines[2]} -- ${exceptionLines[3]}`,
|
|
3019
|
+
};
|
|
3020
|
+
} else {
|
|
3021
|
+
throw ex;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
async getFallbackLocators(element) {
|
|
3026
|
+
if (this.fallbackLocatorCache[element.label]) {
|
|
3027
|
+
return this.fallbackLocatorCache[element.label];
|
|
3028
|
+
}
|
|
3029
|
+
this.fallbackLocatorCache = await new FileUtils().readJSONContent(getAutohealPath(), true);
|
|
3030
|
+
return this.fallbackLocatorCache[element.label];
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
async setFallbackLocators(element, locator) {
|
|
3034
|
+
this.fallbackLocatorCache = await new FileUtils().readJSONContent(getAutohealPath());
|
|
3035
|
+
this.fallbackLocatorCache[element.label] = locator;
|
|
3036
|
+
await new FileUtils().writeJSONContent(getAutohealPath(), this.fallbackLocatorCache);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
async saveVideos(testPass) {
|
|
3040
|
+
if(this.testType === 'web' && this.customConfig?.keepBrowserOpen) {
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
const recordVideo = this.customConfig?.advancedRunConfig?.recordVideo;
|
|
3044
|
+
if(!recordVideo || recordVideo === 'OFF') {
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
if(recordVideo !== 'ALWAYS' && testPass) {
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
let windowI = 1;
|
|
3053
|
+
const browser = this.page.context()?.browser();
|
|
3054
|
+
for (const context of browser.contexts()) {
|
|
3055
|
+
let tabI = 0;
|
|
3056
|
+
for (const page of context.pages()) {
|
|
3057
|
+
tabI += 1;
|
|
3058
|
+
try {
|
|
3059
|
+
await page.close();
|
|
3060
|
+
} catch (ex) {
|
|
3061
|
+
console.log("Exception closing page for saving video : ", ex);
|
|
3062
|
+
} finally {
|
|
3063
|
+
const video = page.video();
|
|
3064
|
+
if (video) {
|
|
3065
|
+
const videoName = windowI === 1
|
|
3066
|
+
? (tabI === 1 ? undefined : `tab${tabI}`)
|
|
3067
|
+
: `window${windowI}_tab${tabI}`;
|
|
3068
|
+
const dest = getScreenshotPath(videoName, this.caller, this.retry, true);
|
|
3069
|
+
try {
|
|
3070
|
+
await video.saveAs(dest);
|
|
3071
|
+
if(this.customConfig?.isIde && os.platform() === 'linux') {
|
|
3072
|
+
try {
|
|
3073
|
+
const tempFileName = `${Date.now()}_${generateRandomString(4)}.webm`;
|
|
3074
|
+
const linuxTempFile = path.join(os.homedir() || '', 'devassure', 'temp_video_recordings', tempFileName);
|
|
3075
|
+
new FileUtils().copyFile(dest, linuxTempFile);
|
|
3076
|
+
this.action_log(getScreenshotLog(videoName, linuxTempFile, this.caller, true));
|
|
3077
|
+
} catch (_error) {
|
|
3078
|
+
this.action_log(getScreenshotLog(videoName, dest, this.caller, true));
|
|
3079
|
+
}
|
|
3080
|
+
} else {
|
|
3081
|
+
this.action_log(getScreenshotLog(videoName, dest, this.caller, true));
|
|
3082
|
+
}
|
|
3083
|
+
} catch (error) {
|
|
3084
|
+
console.log(error);
|
|
3085
|
+
this.log(undefined, "Browser is unable to create video recording!");
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
if(tabI > 0) windowI += 1;
|
|
3091
|
+
if(this.totalRetries && this.retry !== this.totalRetries) {
|
|
3092
|
+
continue;
|
|
3093
|
+
}
|
|
3094
|
+
try{ context.close(); } catch (_ex) {}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
|
|
3099
|
+
async handleResult(e, tppTest) {
|
|
3100
|
+
if(this.page.page && this.page.page()) {
|
|
3101
|
+
this.page = this.page.page(); //Handle if current context is frame
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
tppTest && checkA11yAttachments(tppTest, this.caller);
|
|
3105
|
+
let status;
|
|
3106
|
+
let parsedError;
|
|
3107
|
+
if(!e) {
|
|
3108
|
+
if(this.retry) {
|
|
3109
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: [RETRY-${this.retry}] :: [END]`);
|
|
3110
|
+
}
|
|
3111
|
+
if(this.customConfig) {
|
|
3112
|
+
this.customConfig.testInfo = {
|
|
3113
|
+
...this.customConfig.testInfo || {},
|
|
3114
|
+
status: 'passed',
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
tppTest && await new EachTestHook(this).runAfterEach();
|
|
3118
|
+
|
|
3119
|
+
await this.saveVideos(true);
|
|
3120
|
+
|
|
3121
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: [PASS]`);
|
|
3122
|
+
status = {pass: true};
|
|
3123
|
+
} else {
|
|
3124
|
+
const timedout = tppTest?.info()?.status === "timedOut";
|
|
3125
|
+
if(timedout) {
|
|
3126
|
+
const durationMsg = tppTest.info().duration ? ` ${tppTest.info().duration} ms` : '';
|
|
3127
|
+
e = {message: `Test timed out!`, logs: [`Time out${durationMsg} exceeded!`, 'You can increase test timeout in run profiles.']};
|
|
3128
|
+
}
|
|
3129
|
+
if(typeof e === 'object' && e.stack) {
|
|
3130
|
+
parsedError = {message: e.stack.split("\n").filter(line => !line.includes('node_modules')).join(" --> ")};
|
|
3131
|
+
} else {
|
|
3132
|
+
parsedError = e;
|
|
3133
|
+
}
|
|
3134
|
+
if(this.customConfig) {
|
|
3135
|
+
this.customConfig.testInfo = {
|
|
3136
|
+
...this.customConfig.testInfo || {},
|
|
3137
|
+
status: 'failed',
|
|
3138
|
+
error: parsedError?.message || parsedError,
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
console.error("Result exception: ", e);
|
|
3142
|
+
this.exceptionLog(parsedError.message);
|
|
3143
|
+
status = {pass: false, message: parsedError.message};
|
|
3144
|
+
e.logs?.forEach(log => {
|
|
3145
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: ${log}`);
|
|
3146
|
+
});
|
|
3147
|
+
if(this.testType !== 'api' && !timedout) {
|
|
3148
|
+
await this.addScreenshot(undefined, true);
|
|
3149
|
+
}
|
|
3150
|
+
tppTest && await new EachTestHook(this).runAfterEach();
|
|
3151
|
+
|
|
3152
|
+
await this.saveVideos(false);
|
|
3153
|
+
|
|
3154
|
+
if(this.totalRetries && this.retry !== this.totalRetries) {
|
|
3155
|
+
const tryLabel = this.retry ? `[RETRY-${this.retry}]` : '[FIRST_TRY]'
|
|
3156
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: ${tryLabel} :: [ERROR]`);
|
|
3157
|
+
} else {
|
|
3158
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: [FAIL]`);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
try {
|
|
3163
|
+
if(
|
|
3164
|
+
(this.testType === 'web' && this.customConfig?.keepBrowserOpen)
|
|
3165
|
+
|| (this.testType === 'mobile' && this.customConfig?.keepMobileAppOpen)
|
|
3166
|
+
) {
|
|
3167
|
+
const entity = this.testType === 'mobile' ? 'mobile app session' : 'browser'
|
|
3168
|
+
this.action_log(`${LOG_PREFIX}${this.caller} :: Keeping ${entity} open!`);
|
|
3169
|
+
for (let i = 0; i < 2 * 60 * 60; i++) { //Sleep for 2 hours
|
|
3170
|
+
await sleep(1000);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
if(this.testType === 'mobile') {
|
|
3174
|
+
await this.mobile.shutdown(status);
|
|
3175
|
+
}
|
|
3176
|
+
await this.page?.context()?.close();
|
|
3177
|
+
} catch (_e) {
|
|
3178
|
+
try {await this.page?.context()?.close();} catch (_e) {}
|
|
3179
|
+
};
|
|
3180
|
+
|
|
3181
|
+
if(e && status?.pass === false) {
|
|
3182
|
+
if(tppTest) {
|
|
3183
|
+
this.logAutohealSkip();
|
|
3184
|
+
tppTest.fail(true, parsedError?.message || parsedError || e);
|
|
3185
|
+
} else {
|
|
3186
|
+
this.logAutohealSkip();
|
|
3187
|
+
throw e
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
logAutohealSkip() {
|
|
3193
|
+
if (this.webAppPreferences?.autoheal) {
|
|
3194
|
+
this.log(undefined, `Skipped autoheal for the rest of the actions due to test failure.`);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
get_constant_value = (val) => val;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
exports.Actions = Actions;
|